Merge PR #125 into 14.0

Signed-off-by DarioLodeiros
This commit is contained in:
OCA-git-bot
2022-04-03 11:47:41 +00:00
57 changed files with 3777 additions and 598 deletions

View File

@@ -42,6 +42,7 @@
"report/pms_folio.xml",
"report/pms_folio_templates.xml",
"report/traveller_report_action.xml",
"report/invoice.xml",
# "templates/pms_email_template.xml",
"data/menus.xml",
"wizards/wizard_payment_folio.xml",
@@ -49,6 +50,7 @@
"wizards/pms_booking_engine_views.xml",
"wizards/wizard_folio_changes.xml",
"wizards/wizard_several_partners.xml",
"wizards/pms_booking_duplicate_views.xml",
"views/pms_amenity_views.xml",
"views/pms_amenity_type_views.xml",
"views/pms_board_service_views.xml",
@@ -88,6 +90,7 @@
"views/precheckin_portal_templates.xml",
"wizards/wizard_massive_changes.xml",
"wizards/wizard_advanced_filters.xml",
"views/res_partner_id_category.xml",
"views/payment_transaction_views.xml",
"views/account_move_line_views.xml",
],

View File

@@ -363,6 +363,10 @@ class PortalPrecheckin(CustomerPortal):
error, error_message = self.form_validate(kw, None)
if not kw.get("first") and not kw.get("back") and not error:
kw.update({"checkin_partner_id": checkin_partner_id})
if kw.get("residence_state_id") == "placeholder":
kw["residence_state_id"] = False
if kw.get("residence_country_id") == "placeholder":
kw["residence_country_id"] = False
request.env["pms.checkin.partner"]._save_data_from_portal(kw)
if error:
checkin_pos = checkin_pos - 1

View File

@@ -101,5 +101,21 @@
<field name="nextcall" eval="DateTime.now()" />
<field name="code">model.send_cancelation_mail()</field>
</record>
<record model="ir.cron" id="autoinvoicing_folios">
<field name="name">Auto Invoicing Folios</field>
<field name="interval_number">5</field>
<field name="user_id" ref="base.user_root" />
<field name="active" eval="False" />
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="state">code</field>
<field name="model_id" ref="model_pms_property" />
<field
name="nextcall"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 04:30:00')"
/>
<field name="code">model.autoinvoicing()</field>
</record>
</data>
</odoo>

View File

@@ -20,6 +20,13 @@
<field name="checkin_sequence_id" ref="pms.seq_pms_checkin" />
<field name="reservation_sequence_id" ref="pms.seq_pms_reservation" />
</record>
<!-- default partner simplified invoices -->
<record id="various_pms_partner" model="res.partner">
<field name="name">Various Clients</field>
<field
name="comment"
>Contact used for simplified invoices where no customer is available</field>
</record>
<!-- pms.users -->
<record id="base.user_root" model="res.users">
<field name="company_id" ref="base.main_company" />

View File

@@ -2920,11 +2920,6 @@ msgstr ""
msgid "Customer Ref"
msgstr ""
#. module: pms
#: model:ir.model.fields,field_description:pms.field_pms_folio__client_order_ref
msgid "Customer Reference"
msgstr ""
#. module: pms
#: model:ir.model.fields,field_description:pms.field_folio_advance_payment_inv__deposit_taxes_id
#: model:ir.model.fields,field_description:pms.field_pms_room_type__taxes_id

View File

@@ -46,3 +46,4 @@ from . import pms_availability
from . import res_partner_id_number
from . import pms_automated_mails
from . import payment_transaction
from . import res_partner_id_category

View File

@@ -34,40 +34,11 @@ class AccountBankStatement(models.Model):
)
super(AccountBankStatement, self).button_post()
for line in lines_of_moves_to_post:
folio_ids = line.folio_ids.ids
if folio_ids:
to_reconcile_ids = self.env["account.move.line"].search(
[
("move_id.folio_ids", "in", folio_ids),
("reconciled", "=", False),
"|",
(
"account_id",
"=",
self.journal_id.payment_debit_account_id.id,
),
(
"account_id",
"=",
self.journal_id.payment_credit_account_id.id,
),
("journal_id", "=", self.journal_id.id),
]
)
if to_reconcile_ids:
statement_move_line = line.move_id.line_ids.filtered(
lambda line: line.account_id.reconcile
)
payment_lines = self.env["account.move.line"].browse(
to_reconcile_ids.ids
)
# We try to reconcile by amount
payment_line = False
for record in payment_lines:
payment_line = (
record if abs(record.balance) == line.amount else False
)
if payment_line and statement_move_line:
statement_move_line.account_id = payment_line.account_id
lines_to_reconcile = payment_line + statement_move_line
lines_to_reconcile.reconcile()
payment_move_line = line._get_payment_move_lines_to_reconcile()
statement_move_line = line.move_id.line_ids.filtered(
lambda line: line.account_id.reconcile
)
if payment_move_line and statement_move_line:
statement_move_line.account_id = payment_move_line.account_id
lines_to_reconcile = payment_move_line + statement_move_line
lines_to_reconcile.reconcile()

View File

@@ -48,3 +48,33 @@ class AccountBankStatementLine(models.Model):
}
)
return line_vals_list
def _get_payment_move_lines_to_reconcile(self):
self.ensure_one()
payment_move_line = False
folio_ids = self.folio_ids and self.folio_ids.ids or False
domain = [("move_id.folio_ids", "in", folio_ids)] if folio_ids else []
domain.extend(
[
("move_id.ref", "=", self.payment_ref),
("date", "=", self.date),
("reconciled", "=", False),
"|",
(
"account_id",
"=",
self.journal_id.payment_debit_account_id.id,
),
(
"account_id",
"=",
self.journal_id.payment_credit_account_id.id,
),
("journal_id", "=", self.journal_id.id),
]
)
to_reconcile_move_lines = self.env["account.move.line"].search(domain)
# We try to reconcile by amount
for record in to_reconcile_move_lines:
payment_move_line = record if record.balance == self.amount else False
return payment_move_line

View File

@@ -1,4 +1,4 @@
from odoo import fields, models
from odoo import _, api, fields, models
class AccountJournal(models.Model):
@@ -20,3 +20,51 @@ class AccountJournal(models.Model):
string="For manual payments",
help="Use to pay for reservations",
)
is_simplified_invoice = fields.Boolean(
string="Simplified invoice",
help="Use to simplified invoice",
compute="_compute_is_simplified_invoice",
readonly=False,
store=True,
)
@api.depends("pms_property_ids", "pms_property_ids.journal_simplified_invoice_id")
def _compute_is_simplified_invoice(self):
self.is_simplified_invoice = False
for journal in self:
if journal.id in journal.pms_property_ids.mapped(
"journal_simplified_invoice_id.id"
):
journal.is_simplified_invoice = True
@api.constrains("is_simplified_invoice")
def _check_pms_properties_simplified_invoice(self):
for journal in self:
if (
journal.is_simplified_invoice
and journal.id
in journal.pms_property_ids.mapped("journal_normal_invoice_id.id")
):
raise models.ValidationError(
_(
"The journal %s is used for normal invoices in the properties: %s"
% (
journal.name,
", ".join(journal.pms_property_ids.mapped("name")),
)
)
)
if (
not journal.is_simplified_invoice
and journal.id
in journal.pms_property_ids.mapped("journal_simplified_invoice_id.id")
):
raise models.ValidationError(
_(
"The journal %s is used for simplified invoices in the properties: %s"
% (
journal.name,
", ".join(journal.pms_property_ids.mapped("name")),
)
)
)

View File

@@ -4,7 +4,7 @@ import itertools as it
import json
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError
class AccountMove(models.Model):
@@ -32,6 +32,20 @@ class AccountMove(models.Model):
# check_pms_properties=True,
)
# journal_id = fields.Many2one(check_pms_properties=True)
is_simplified_invoice = fields.Boolean(
help="Technical field to know if the invoice is simplified",
related="journal_id.is_simplified_invoice",
store=True,
)
origin_agency_id = fields.Many2one(
string="Origin Agency",
help="The agency where the folio account move originates",
comodel_name="res.partner",
domain="[('is_agency', '=', True)]",
compute="_compute_origin_agency_id",
store=True,
readonly=False,
)
@api.onchange("pms_property_id")
def _onchange_pms_property_id(self):
@@ -64,6 +78,19 @@ class AccountMove(models.Model):
move.folio_ids = False
move.folio_ids = move.mapped("line_ids.folio_ids.id")
@api.depends("line_ids", "line_ids.origin_agency_id")
def _compute_origin_agency_id(self):
"""
Compute the origin agency of the account move
if the move has multiple agencies in origin,
the first one is returned (REVIEW: is this correct?)
"""
self.origin_agency_id = False
for move in self:
agencies = move.mapped("line_ids.origin_agency_id")
if agencies:
move.origin_agency_id = agencies[0]
def _compute_payments_widget_to_reconcile_info(self):
for move in self:
if not move.line_ids.folio_line_ids:
@@ -84,22 +111,6 @@ class AccountMove(models.Model):
in ("receivable", "payable")
)
domain = [
("account_id", "in", pay_term_lines.account_id.ids),
("parent_state", "=", "posted"),
("reconciled", "=", False),
"|",
("amount_residual", "!=", 0.0),
("amount_residual_currency", "!=", 0.0),
"|",
(
"folio_ids",
"in",
move.line_ids.mapped("folio_line_ids.folio_id.id"),
),
("partner_id", "=", move.commercial_partner_id.id),
]
payments_widget_vals = {
"outstanding": True,
"content": [],
@@ -107,13 +118,31 @@ class AccountMove(models.Model):
}
if move.is_inbound():
domain.append(("balance", "<", 0.0))
domain = [("balance", "<", 0.0)]
payments_widget_vals["title"] = _("Outstanding credits")
else:
domain.append(("balance", ">", 0.0))
domain = [("balance", ">", 0.0)]
payments_widget_vals["title"] = _("Outstanding debits")
for line in self.env["account.move.line"].search(domain):
domain.extend(
[
("account_id", "in", pay_term_lines.account_id.ids),
("parent_state", "=", "posted"),
("reconciled", "=", False),
"|",
("amount_residual", "!=", 0.0),
("amount_residual_currency", "!=", 0.0),
"|",
(
"folio_ids",
"in",
move.line_ids.mapped("folio_line_ids.folio_id.id"),
),
("partner_id", "=", move.commercial_partner_id.id),
]
)
for line in self.env["account.move.line"].search(domain):
if line.currency_id == move.currency_id:
# Same foreign currency.
amount = abs(line.amount_residual_currency)
@@ -157,13 +186,18 @@ class AccountMove(models.Model):
default_pms_property_id is set in context
"""
journal = super(AccountMove, self)._search_default_journal(journal_types)
if self._context.get("default_pms_property_id"):
property_id = self._context.get("default_pms_property_id")
pms_property = self.env["pms.property"].browse(property_id)
company_id = self._context.get("default_company_id", self.env.company.id)
company = self.env["res.company"].browse(company_id)
pms_property_id = self.pms_property_id.id or (
self.env.user.get_active_property_ids
and self.env.user.get_active_property_ids()[0]
)
pms_property = self.env["pms.property"].browse(pms_property_id)
if pms_property:
domain = [
("company_id", "=", pms_property.company_id.id),
("type", "in", journal_types),
("pms_property_ids", "in", property_id),
("pms_property_ids", "in", pms_property.id),
]
journal = self.env["account.journal"].search(domain, limit=1)
if not journal:
@@ -173,16 +207,41 @@ class AccountMove(models.Model):
("pms_property_ids", "=", False),
]
journal = self.env["account.journal"].search(domain, limit=1)
if not journal:
else:
domain = [
("company_id", "=", company_id),
("type", "in", journal_types),
("pms_property_ids", "=", False),
]
journal = self.env["account.journal"].search(domain, limit=1)
if not journal:
if pms_property:
error_msg = _(
"""No journal could be found in property %(property_name)s
for any of those types: %(journal_types)s""",
property_name=pms_property.display_name,
journal_types=", ".join(journal_types),
)
raise UserError(error_msg)
else:
error_msg = _(
"""No journal could be found in company %(company_name)s
for any of those types: %(journal_types)s""",
company_name=company.display_name,
journal_types=", ".join(journal_types),
)
raise UserError(error_msg)
return journal
@api.depends("pms_property_id")
def _compute_suitable_journal_ids(self):
super(AccountMove, self)._compute_suitable_journal_ids()
for move in self:
if move.pms_property_id:
move.suitable_journal_ids = move.suitable_journal_ids.filtered(
lambda j: not j.pms_property_ids
or move.pms_property_id.id in j.pms_property_ids.ids
)
def _autoreconcile_folio_payments(self):
"""
Reconcile payments with the invoice
@@ -235,6 +294,8 @@ class AccountMove(models.Model):
"""
Overwrite the original method to add the folio_ids to the invoice
"""
for record in self:
record._check_pms_valid_invoice(record)
res = super(AccountMove, self)._post(soft)
self._autoreconcile_folio_payments()
return res
@@ -252,3 +313,39 @@ class AccountMove(models.Model):
lambda p: p.id in [item.id for item in combi]
)
return []
@api.model
def _check_pms_valid_invoice(self, move):
"""
Check invoice and receipts legal status
"""
if (
move.is_invoice(include_receipts=True)
and not move.journal_id.is_simplified_invoice
and (
not move.partner_id or not move.partner_id._check_enought_invoice_data()
)
):
raise UserError(
_(
"You cannot validate this invoice. Please check the "
" partner has the complete information required."
)
)
if move.journal_id.is_simplified_invoice:
move._check_simplified_restrictions()
return True
def _check_simplified_restrictions(self):
self.ensure_one()
if (
self.pms_property_id
and self.amount_total > self.pms_property_id.max_amount_simplified_invoice
):
mens = _(
"The total amount of the simplified invoice is higher than the "
"maximum amount allowed for simplified invoices."
)
self.folio_ids.message_post(body=mens)
raise ValidationError(mens)
return True

View File

@@ -10,6 +10,11 @@ class AccountMoveLine(models.Model):
# Fields declaration
# TODO: REVIEW why not a Many2one?
name = fields.Char(
compute="_compute_name",
store=True,
readonly=False,
)
folio_line_ids = fields.Many2many(
string="Folio Lines",
help="The folio lines in the account move lines",
@@ -41,47 +46,16 @@ class AccountMoveLine(models.Model):
readonly=False,
check_pms_properties=True,
)
move_id = fields.Many2one(check_pms_properties=True)
@api.depends("move_id")
def _compute_pms_property_id(self):
for rec in self:
if rec.move_id and rec.move_id.pms_property_id:
rec.pms_property_id = rec.move_id.pms_property_id
elif not rec.pms_property_id:
rec.pms_property_id = False
@api.depends("name")
def _compute_name_changed_by_user(self):
for record in self:
# if not record._context.get("auto_name"):
if not self._context.get("auto_name"):
record.name_changed_by_user = True
else:
record.name_changed_by_user = False
name = fields.Char(
compute="_compute_name",
origin_agency_id = fields.Many2one(
string="Origin Agency",
help="The agency where the folio account move originates",
comodel_name="res.partner",
domain="[('is_agency', '=', True)]",
compute="_compute_origin_agency_id",
store=True,
readonly=False,
)
@api.depends(
"folio_line_ids",
"payment_id",
"payment_id.folio_ids",
"statement_line_id",
"statement_line_id.folio_ids",
)
def _compute_folio_ids(self):
if self.folio_line_ids:
self.folio_ids = self.folio_line_ids.mapped("folio_id")
elif self.payment_id:
self.folio_ids = self.payment_id.folio_ids
elif self.statement_line_id:
self.folio_ids = self.statement_line_id.folio_ids
else:
self.folio_ids = False
move_id = fields.Many2one(check_pms_properties=True)
@api.depends("quantity")
def _compute_name(self):
@@ -104,3 +78,51 @@ class AccountMoveLine(models.Model):
# qty=record.quantity)
# record.with_context(auto_name=True)
# ._compute_name_changed_by_user()
@api.depends("move_id")
def _compute_pms_property_id(self):
for rec in self:
if rec.move_id and rec.move_id.pms_property_id:
rec.pms_property_id = rec.move_id.pms_property_id
elif not rec.pms_property_id:
rec.pms_property_id = False
@api.depends("name")
def _compute_name_changed_by_user(self):
for record in self:
# if not record._context.get("auto_name"):
if not self._context.get("auto_name"):
record.name_changed_by_user = True
else:
record.name_changed_by_user = False
@api.depends(
"folio_line_ids",
"payment_id",
"payment_id.folio_ids",
"statement_line_id",
"statement_line_id.folio_ids",
)
def _compute_folio_ids(self):
if self.folio_line_ids:
self.folio_ids = self.folio_line_ids.mapped("folio_id")
elif self.payment_id:
self.folio_ids = self.payment_id.folio_ids
elif self.statement_line_id:
self.folio_ids = self.statement_line_id.folio_ids
else:
self.folio_ids = False
@api.depends("folio_line_ids")
def _compute_origin_agency_id(self):
"""
Compute the origin agency of the account move line,
if the line has multiple agencies in origin,
(p.e. nights with different agencies in origin),
the first one is returned (REVIEW: is this correct?)
"""
self.origin_agency_id = False
for line in self:
agencies = line.mapped("folio_line_ids.origin_agency_id")
if agencies:
line.origin_agency_id = agencies[0]

View File

@@ -17,6 +17,46 @@ class AccountPayment(models.Model):
column1="payment_id",
column2="folio_id",
)
origin_agency_id = fields.Many2one(
string="Origin Agency",
help="The agency where the folio account move originates",
comodel_name="res.partner",
domain="[('is_agency', '=', True)]",
compute="_compute_origin_agency_id",
store=True,
readonly=True,
)
origin_reference = fields.Char(
string="Origin Reference",
help="The reference of the payment origin",
)
@api.depends("reconciled_invoice_ids", "reconciled_bill_ids")
def _compute_origin_agency_id(self):
"""
Compute the origin agency of the sale line,
if the line has multiple agencies in origin,
(p.e. nights with different agencies in origin),
the first one is returned (REVIEW: is this correct?)
"""
for rec in self:
inv_agency_ids = rec.reconciled_invoice_ids.mapped(
"line_ids.folio_line_ids.origin_agency_id.id"
)
bill_agency_ids = rec.reconciled_bill_ids.mapped(
"line_ids.folio_line_ids.origin_agency_id.id"
)
agency_ids = list(set(inv_agency_ids + bill_agency_ids))
if agency_ids:
rec.write({"origin_agency_id": agency_ids[0]})
elif (
not rec.reconciled_invoice_ids
and not rec.reconciled_bill_ids
and rec.folio_ids
):
rec.origin_agency_id = rec.origin_agency_id
else:
rec.origin_agency_id = False
@api.depends("reconciled_invoice_ids", "reconciled_bill_ids")
def _compute_folio_ids(self):

View File

@@ -264,6 +264,15 @@ class FolioSaleLine(models.Model):
store=True,
related="folio_id.partner_id",
)
origin_agency_id = fields.Many2one(
string="Origin Agency",
help="The agency where the folio sale line originates",
comodel_name="res.partner",
domain="[('is_agency', '=', True)]",
compute="_compute_origin_agency_id",
store=True,
readonly=False,
)
analytic_tag_ids = fields.Many2many(
string="Analytic Tags",
comodel_name="account.analytic.tag",
@@ -320,6 +329,14 @@ class FolioSaleLine(models.Model):
compute="_compute_date_order",
)
@api.depends(
"reservation_line_ids",
"reservation_id.agency_id",
)
def _compute_origin_agency_id(self):
for rec in self:
rec.origin_agency_id = rec.folio_id.agency_id
@api.depends("qty_to_invoice")
def _compute_service_order(self):
for record in self:
@@ -395,7 +412,7 @@ class FolioSaleLine(models.Model):
invoice_date = (
invoice_line.move_id.invoice_date or fields.Date.today()
)
if invoice_line.move_id.move_type == "out_invoice":
if invoice_line.move_id.move_type in ["out_invoice", "out_receipt"]:
amount_invoiced += invoice_line.currency_id._convert(
invoice_line.price_subtotal,
line.currency_id,
@@ -506,7 +523,7 @@ class FolioSaleLine(models.Model):
"Product Unit of Measure"
)
for line in self:
if line.state == "draft":
if line.state == "draft" or line.price_total == 0.0:
line.invoice_status = "no"
# REVIEW: if qty_to_invoice < 0 (invoice qty > sale qty),
# why status to_invoice?? this behavior is copied from sale order
@@ -625,7 +642,7 @@ class FolioSaleLine(models.Model):
Otherwise, the quantity delivered is used.
"""
for line in self:
if line.folio_id.state not in ["draft"]:
if line.folio_id.state not in ["draft"] and line.price_total > 0.0:
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
else:
line.qty_to_invoice = 0
@@ -650,7 +667,7 @@ class FolioSaleLine(models.Model):
qty_invoiced = 0.0
for invoice_line in line.invoice_lines:
if invoice_line.move_id.state != "cancel":
if invoice_line.move_id.move_type == "out_invoice":
if invoice_line.move_id.move_type in ["out_invoice", "out_receipt"]:
qty_invoiced += invoice_line.product_uom_id._compute_quantity(
invoice_line.quantity, line.product_uom
)

View File

@@ -81,13 +81,16 @@ class PmsAvailabilityPlan(models.Model):
)
@api.model
def update_quota(self, pricelist_id, room_type_id, date, impacts_quota_id=False):
def update_quota(
self, pricelist_id, room_type_id, date, pms_property_id, impacts_quota_id=False
):
if pricelist_id and room_type_id and date:
rule = self.env["pms.availability.plan.rule"].search(
[
("availability_plan_id.pms_pricelist_ids", "=", pricelist_id.id),
("room_type_id", "=", room_type_id.id),
("availability_plan_id.pms_pricelist_ids", "=", pricelist_id),
("room_type_id", "=", room_type_id),
("date", "=", date),
("pms_property_id", "=", pms_property_id),
]
)
# applies a rule

View File

@@ -76,6 +76,13 @@ class PmsCheckinPartner(models.Model):
store=True,
compute="_compute_mobile",
)
phone = fields.Char(
string="Phone",
help="Checkin Partner Phone",
readonly=False,
store=True,
compute="_compute_phone",
)
image_128 = fields.Image(
string="Image",
help="Checkin Partner Image, it corresponds with Partner Image associated",
@@ -136,18 +143,52 @@ class PmsCheckinPartner(models.Model):
compute="_compute_nationality_id",
comodel_name="res.country",
)
# TODO: Use new partner contact "other or "private" with
# personal contact address complete??
# to avoid user country_id on companies contacts.
# View to res.partner state_id inherit
state_id = fields.Many2one(
string="Country State",
help="host state",
residence_street = fields.Char(
string="Street",
help="Street of the guest's residence",
readonly=False,
store=True,
compute="_compute_state_id",
compute="_compute_residence_street",
)
residence_street2 = fields.Char(
string="Street2",
help="Second street of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_street2",
)
residence_zip = fields.Char(
string="Zip",
help="Zip of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_zip",
change_default=True,
)
residence_city = fields.Char(
string="City",
help="City of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_city",
)
residence_country_id = fields.Many2one(
string="Country of residence",
help="Country of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_country_id",
comodel_name="res.country",
)
residence_state_id = fields.Many2one(
string="State of residence",
help="State of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_state_id",
comodel_name="res.country.state",
)
firstname = fields.Char(
string="First Name",
help="host firstname",
@@ -224,6 +265,10 @@ class PmsCheckinPartner(models.Model):
inverse_name="checkin_partner_possible_customer_id",
)
partner_relationship = fields.Char(
string="Partner relationship", help="Family relationship between travelers"
)
@api.depends("partner_id")
def _compute_document_number(self):
for record in self:
@@ -304,12 +349,55 @@ class PmsCheckinPartner(models.Model):
record.nationality_id = False
@api.depends("partner_id")
def _compute_state_id(self):
def _compute_residence_street(self):
for record in self:
if not record.state_id and record.partner_id.state_id:
record.state_id = record.partner_id.state_id
elif not record.state_id:
record.state_id = False
if not record.residence_street and record.partner_id.residence_street:
record.residence_street = record.partner_id.residence_street
elif not record.residence_street:
record.residence_street = False
@api.depends("partner_id")
def _compute_residence_street2(self):
for record in self:
if not record.residence_street2 and record.partner_id.residence_street2:
record.residence_street2 = record.partner_id.residence_street2
elif not record.residence_street2:
record.residence_street2 = False
@api.depends("partner_id")
def _compute_residence_zip(self):
for record in self:
if not record.residence_zip and record.partner_id.residence_zip:
record.residence_zip = record.partner_id.residence_zip
elif not record.residence_zip:
record.residence_zip = False
@api.depends("partner_id")
def _compute_residence_city(self):
for record in self:
if not record.residence_city and record.partner_id.residence_city:
record.residence_city = record.partner_id.residence_city
elif not record.residence_city:
record.residence_city = False
@api.depends("partner_id")
def _compute_residence_country_id(self):
for record in self:
if (
not record.residence_country_id
and record.partner_id.residence_country_id
):
record.residence_country_id = record.partner_id.residence_country_id
elif not record.residence_country_id:
record.residence_country_id = False
@api.depends("partner_id")
def _compute_residence_state_id(self):
for record in self:
if not record.residence_state_id and record.partner_id.residence_state_id:
record.residence_state_id = record.partner_id.residence_state_id
elif not record.residence_state_id:
record.residence_state_id = False
@api.depends("reservation_id", "reservation_id.folio_id")
def _compute_folio_id(self):
@@ -348,14 +436,26 @@ class PmsCheckinPartner(models.Model):
@api.depends("partner_id")
def _compute_email(self):
for record in self:
if not record.email or record.partner_id.email:
if not record.email and record.partner_id.email:
record.email = record.partner_id.email
elif not record.email:
record.email = False
@api.depends("partner_id")
def _compute_mobile(self):
for record in self:
if not record.mobile or record.partner_id.mobile:
if not record.mobile and record.partner_id.mobile:
record.mobile = record.partner_id.mobile
elif not record.mobile:
record.mobile = False
@api.depends("partner_id")
def _compute_phone(self):
for record in self:
if not record.phone and record.partner_id.phone:
record.phone = record.partner_id.phone
elif not record.phone:
record.phone = False
@api.depends("partner_id")
def _compute_document_id(self):
@@ -756,10 +856,10 @@ class PmsCheckinPartner(models.Model):
if not values.get("document_type"):
values.update({"document_type": False})
if values.get("state"):
state_id = self.env["res.country.state"].search(
residence_state_id = self.env["res.country.state"].search(
[("id", "=", values.get("state"))]
)
values.update({"state_id": state_id})
values.update({"residence_state_id": residence_state_id})
values.pop("state")
if values.get("document_expedition_date"):
doc_type = values.get("document_type")

View File

@@ -4,8 +4,11 @@
import datetime
import logging
from datetime import timedelta
from itertools import groupby
from dateutil import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_compare, float_is_zero
@@ -32,6 +35,10 @@ class PmsFolio(models.Model):
index=True,
default=lambda self: _("New"),
)
external_reference = fields.Char(
string="External Reference",
help="Reference of this folio in an external system",
)
pms_property_id = fields.Many2one(
string="Property",
help="The property for folios",
@@ -293,7 +300,6 @@ class PmsFolio(models.Model):
comodel_name="res.partner.category",
ondelete="restrict",
)
client_order_ref = fields.Char(string="Customer Reference", help="", copy=False)
reservation_type = fields.Selection(
string="Type",
help="The type of the reservation. "
@@ -505,6 +511,11 @@ class PmsFolio(models.Model):
store=True,
compute="_compute_last_checkout",
)
autoinvoice_date = fields.Date(
string="Autoinvoice Date",
compute="_compute_autoinvoice_date",
store=True,
)
def name_get(self):
result = []
@@ -539,72 +550,138 @@ class PmsFolio(models.Model):
)
invoice_vals_list = []
invoice_item_sequence = 0
for order in self:
order = order.with_company(order.company_id)
current_section_vals = None
down_payments = order.env["folio.sale.line"]
# Invoice values.
invoice_vals = order._prepare_invoice(partner_invoice_id=partner_invoice_id)
# Invoice line values (keep only necessary sections).
invoice_lines_vals = []
for line in order.sale_line_ids.filtered(
for folio in self:
folio_lines_to_invoice = folio.sale_line_ids.filtered(
lambda l: l.id in list(lines_to_invoice.keys())
):
if line.display_type == "line_section":
current_section_vals = line._prepare_invoice_line(
sequence=invoice_item_sequence + 1
)
continue
if line.display_type != "line_note" and float_is_zero(
line.qty_to_invoice, precision_digits=precision
):
continue
if (
line.qty_to_invoice > 0
or (line.qty_to_invoice < 0 and final)
or line.display_type == "line_note"
):
if line.is_downpayment:
down_payments += line
continue
if current_section_vals:
invoice_item_sequence += 1
invoice_lines_vals.append(current_section_vals)
current_section_vals = None
invoice_item_sequence += 1
prepared_line = line._prepare_invoice_line(
sequence=invoice_item_sequence, qty=lines_to_invoice[line.id]
)
invoice_lines_vals.append(prepared_line)
)
folio_partner_invoice_id = partner_invoice_id
if not folio_partner_invoice_id:
folio_partner_invoice_id = folio._get_default_partner_invoice_id()
# If down payments are present in SO, group them under common section
if down_payments:
invoice_item_sequence += 1
down_payments_section = order._prepare_down_payment_section_line(
sequence=invoice_item_sequence
groups_invoice_lines = folio._get_groups_invoice_lines(
lines_to_invoice=folio_lines_to_invoice,
partner_invoice_id=folio_partner_invoice_id,
)
for group in groups_invoice_lines:
folio = folio.with_company(folio.company_id)
down_payments = folio.env["folio.sale.line"]
# Invoice values.
invoice_vals = folio._prepare_invoice(
partner_invoice_id=group["partner_id"]
)
invoice_lines_vals.append(down_payments_section)
for down_payment in down_payments:
# Invoice line values (keep only necessary sections).
current_section_vals = None
invoice_lines_vals = []
for line in group["lines"]:
if line.display_type == "line_section":
current_section_vals = line._prepare_invoice_line(
sequence=invoice_item_sequence + 1
)
continue
if line.display_type != "line_note" and float_is_zero(
line.qty_to_invoice, precision_digits=precision
):
continue
if (
line.qty_to_invoice > 0
or (line.qty_to_invoice < 0 and final)
or line.display_type == "line_note"
):
if line.is_downpayment:
down_payments += line
continue
if current_section_vals:
invoice_item_sequence += 1
invoice_lines_vals.append(current_section_vals)
current_section_vals = None
invoice_item_sequence += 1
prepared_line = line._prepare_invoice_line(
sequence=invoice_item_sequence,
qty=lines_to_invoice[line.id],
)
invoice_lines_vals.append(prepared_line)
# If down payments are present in SO, group them under common section
if down_payments:
invoice_item_sequence += 1
invoice_down_payment_vals = down_payment._prepare_invoice_line(
down_payments_section = folio._prepare_down_payment_section_line(
sequence=invoice_item_sequence
)
invoice_lines_vals.append(invoice_down_payment_vals)
invoice_lines_vals.append(down_payments_section)
for down_payment in down_payments:
invoice_item_sequence += 1
invoice_down_payment_vals = down_payment._prepare_invoice_line(
sequence=invoice_item_sequence
)
invoice_lines_vals.append(invoice_down_payment_vals)
if not any(
new_line["display_type"] is False for new_line in invoice_lines_vals
):
raise self._nothing_to_invoice_error()
if not any(
new_line["display_type"] is False for new_line in invoice_lines_vals
):
raise self._nothing_to_invoice_error()
invoice_vals["invoice_line_ids"] = [
(0, 0, invoice_line_id) for invoice_line_id in invoice_lines_vals
]
invoice_vals["invoice_line_ids"] = [
(0, 0, invoice_line_id) for invoice_line_id in invoice_lines_vals
]
invoice_vals_list.append(invoice_vals)
invoice_vals_list.append(invoice_vals)
return invoice_vals_list
def _get_groups_invoice_lines(self, lines_to_invoice, partner_invoice_id):
self.ensure_one()
target_lines = lines_to_invoice
if self._context.get("lines_auto_add") and partner_invoice_id:
folio_partner_invoice = self.env["res.partner"].browse(partner_invoice_id)
if folio_partner_invoice.default_invoice_lines == "overnights":
target_lines = target_lines.filtered(
lambda r: r.is_board_service
or (r.reservation_line_ids and r.reservation_id.overnight_room)
)
elif folio_partner_invoice.default_invoice_lines == "reservations":
target_lines = target_lines.filtered(
lambda r: r.is_board_service or r.reservation_line_ids
)
elif folio_partner_invoice.default_invoice_lines == "services":
target_lines = target_lines.filtered(
lambda r: not r.is_board_service or r.service_line_ids
)
groups_invoice_lines = [
{
"partner_id": partner_invoice_id,
"lines": target_lines,
}
]
if (
self.autoinvoice_date
and self.autoinvoice_date <= fields.Date.today()
and len(target_lines) < len(lines_to_invoice)
):
other_partner_to_invoice = self.partner_invoice_ids.filtered(
lambda p: p.id != partner_invoice_id
)
if not other_partner_to_invoice:
other_partner_to_invoice = self.env.ref("pms.various_pms_partner")
groups_invoice_lines.append(
{
"partner_id": other_partner_to_invoice.id,
"lines": lines_to_invoice - target_lines,
}
)
return groups_invoice_lines
def _get_default_partner_invoice_id(self):
self.ensure_one()
folio_partner_invoice_id = False
if self.partner_id and self.partner_id.vat:
folio_partner_invoice_id = self.partner_id.id
if not folio_partner_invoice_id:
folio_partner_invoice_id = (
self.partner_invoice_ids[0].id if self.partner_invoice_ids else False
)
if not folio_partner_invoice_id:
folio_partner_invoice_id = self.env.ref("pms.various_pms_partner").id
return folio_partner_invoice_id
def _get_tax_amount_by_group(self):
self.ensure_one()
res = {}
@@ -627,6 +704,42 @@ class PmsFolio(models.Model):
]
return res
@api.depends("partner_id", "invoice_status", "last_checkout", "partner_invoice_ids")
def _compute_autoinvoice_date(self):
self.autoinvoice_date = False
for record in self.filtered(lambda r: r.invoice_status == "to_invoice"):
record.autoinvoice_date = record._get_to_invoice_date()
def _get_to_invoice_date(self):
self.ensure_one()
partner = self.partner_id
invoicing_policy = (
self.pms_property_id.default_invoicing_policy
if not partner or partner.invoicing_policy == "property"
else partner.invoicing_policy
)
if invoicing_policy == "manual":
return False
if invoicing_policy == "checkout":
margin_days = (
self.pms_property_id.margin_days_autoinvoice
if not partner or partner.invoicing_policy == "property"
else partner.margin_days_autoinvoice
)
return self.last_checkout + timedelta(days=margin_days)
if invoicing_policy == "month_day":
month_day = (
self.pms_property_id.invoicing_month_day
if not partner or partner.invoicing_policy == "property"
else partner.invoicing_month_day
)
if self.last_checkout.day <= month_day:
self.autoinvoice_date = self.last_checkout.replace(day=month_day)
else:
self.autoinvoice_date = (
self.last_checkout + relativedelta.relativedelta(months=1)
).replace(day=month_day)
@api.depends("reservation_ids", "reservation_ids.state")
def _compute_number_of_rooms(self):
for folio in self:
@@ -782,7 +895,8 @@ class PmsFolio(models.Model):
# directly linked to the SO.
for order in self:
invoices = order.sale_line_ids.invoice_lines.move_id.filtered(
lambda r: r.move_type in ("out_invoice", "out_refund")
lambda r: r.move_type
in ("out_invoice", "out_refund", "out_receipt", "in_receipt")
)
order.move_ids = invoices
order.invoice_count = len(invoices)
@@ -814,9 +928,10 @@ class PmsFolio(models.Model):
- to_invoice: if any SO line is 'to_invoice', the whole SO is 'to_invoice'
- invoiced: if all SO lines are invoiced, the SO is invoiced.
"""
unconfirmed_orders = self.filtered(lambda so: so.state in ["draft"])
unconfirmed_orders = self.filtered(lambda folio: folio.state in ["draft"])
unconfirmed_orders.invoice_status = "no"
confirmed_orders = self - unconfirmed_orders
zero_orders = self.filtered(lambda folio: folio.amount_total == 0)
confirmed_orders = self - unconfirmed_orders - zero_orders
if not confirmed_orders:
return
line_invoice_status_all = [
@@ -1105,7 +1220,7 @@ class PmsFolio(models.Model):
JOIN account_move_line aml ON aml.id = soli_rel.invoice_line_id
JOIN account_move am ON am.id = aml.move_id
WHERE
am.move_type in ('out_invoice', 'out_refund') AND
am.move_type in ('out_invoice', 'out_refund', 'in_receipt') AND
am.id = ANY(%s)
""",
(list(value),),
@@ -1117,7 +1232,7 @@ class PmsFolio(models.Model):
(
"sale_line_ids.invoice_lines.move_id.move_type",
"in",
("out_invoice", "out_refund"),
("out_invoice", "out_refund", "in_receipt"),
),
("sale_line_ids.invoice_lines.move_id", operator, value),
]
@@ -1507,6 +1622,7 @@ class PmsFolio(models.Model):
return self.env["account.move"]
# 1) Create invoices.
if not lines_to_invoice:
self = self.with_context(lines_auto_add=True)
lines_to_invoice = dict()
for line in self.sale_line_ids:
lines_to_invoice[line.id] = (
@@ -1517,45 +1633,12 @@ class PmsFolio(models.Model):
lines_to_invoice=lines_to_invoice,
partner_invoice_id=partner_invoice_id,
)
if not invoice_vals_list:
raise self._nothing_to_invoice_error()
# 2) Manage 'grouped' parameter: group by (partner_id, currency_id).
if not grouped:
new_invoice_vals_list = []
invoice_grouping_keys = self._get_invoice_grouping_keys()
for _grouping_keys, invoices in groupby(
invoice_vals_list,
key=lambda x: [
x.get(grouping_key) for grouping_key in invoice_grouping_keys
],
):
origins = set()
payment_refs = set()
refs = set()
ref_invoice_vals = None
for invoice_vals in invoices:
if not ref_invoice_vals:
ref_invoice_vals = invoice_vals
else:
ref_invoice_vals["invoice_line_ids"] += invoice_vals[
"invoice_line_ids"
]
origins.add(invoice_vals["invoice_origin"])
payment_refs.add(invoice_vals["payment_reference"])
refs.add(invoice_vals["ref"])
ref_invoice_vals.update(
{
"ref": ", ".join(refs)[:2000],
"invoice_origin": ", ".join(origins),
"payment_reference": len(payment_refs) == 1
and payment_refs.pop()
or False,
}
)
new_invoice_vals_list.append(ref_invoice_vals)
invoice_vals_list = new_invoice_vals_list
invoice_vals_list = self._get_group_vals_list(invoice_vals_list)
# 3) Create invoices.
@@ -1595,12 +1678,7 @@ class PmsFolio(models.Model):
# a salesperson must be able to generate an invoice from a
# sale order without "billing" access rights.
# However, he should not be able to create an invoice from scratch.
moves = (
self.env["account.move"]
.sudo()
.with_context(default_move_type="out_invoice", auto_name=True)
.create(invoice_vals_list)
)
moves = self._create_account_moves(invoice_vals_list)
# 4) Some moves might actually be refunds: convert
# them if the total amount is negative
@@ -1622,6 +1700,54 @@ class PmsFolio(models.Model):
)
return moves
def _create_account_moves(self, invoice_vals_list):
moves = self.env["account.move"]
for invoice_vals in invoice_vals_list:
if invoice_vals["move_type"] == "out_invoice":
move = (
self.env["account.move"]
.sudo()
.with_context(default_move_type="out_invoice", auto_name=True)
.create(invoice_vals)
)
moves += move
return moves
def _get_group_vals_list(self, invoice_vals_list):
new_invoice_vals_list = []
invoice_grouping_keys = self._get_invoice_grouping_keys()
for _grouping_keys, invoices in groupby(
invoice_vals_list,
key=lambda x: [
x.get(grouping_key) for grouping_key in invoice_grouping_keys
],
):
origins = set()
payment_refs = set()
refs = set()
ref_invoice_vals = None
for invoice_vals in invoices:
if not ref_invoice_vals:
ref_invoice_vals = invoice_vals
else:
ref_invoice_vals["invoice_line_ids"] += invoice_vals[
"invoice_line_ids"
]
origins.add(invoice_vals["invoice_origin"])
payment_refs.add(invoice_vals["payment_reference"])
refs.add(invoice_vals["ref"])
ref_invoice_vals.update(
{
"ref": ", ".join(refs)[:2000],
"invoice_origin": ", ".join(origins),
"payment_reference": len(payment_refs) == 1
and payment_refs.pop()
or False,
}
)
new_invoice_vals_list.append(ref_invoice_vals)
return new_invoice_vals_list
def _prepare_invoice(self, partner_invoice_id=False):
"""
Prepare the dict of values to create the new invoice for a folio.
@@ -1629,28 +1755,25 @@ class PmsFolio(models.Model):
(making sure to call super() to establish a clean extension chain).
"""
self.ensure_one()
journal = (
self.env["account.move"]
.with_context(
default_move_type="out_invoice",
default_company_id=self.company_id.id,
default_pms_property_id=self.pms_property_id.id,
journal = self._get_folio_default_journal(partner_invoice_id)
if not journal:
journal = (
self.env["account.move"]
.with_context(
default_move_type="out_invoice",
default_company_id=self.company_id.id,
default_pms_property_id=self.pms_property_id.id,
)
._get_default_journal()
)
._get_default_journal()
)
if not journal:
raise UserError(
_("Please define an accounting sales journal for the company %s (%s).")
% (self.company_id.name, self.company_id.id)
)
if not partner_invoice_id:
partner_invoice_id = (
self.partner_invoice_ids[0].id if self.partner_invoice_ids else False
)
invoice_vals = {
"ref": self.client_order_ref or "",
"ref": self.name or "",
"move_type": "out_invoice",
"narration": self.note,
"currency_id": self.pricelist_id.currency_id.id,
@@ -1663,14 +1786,25 @@ class PmsFolio(models.Model):
"journal_id": journal.id, # company comes from the journal
"invoice_origin": self.name,
"invoice_payment_term_id": self.payment_term_id.id,
"payment_reference": self.reference,
"transaction_ids": [(6, 0, self.transaction_ids.ids)],
"folio_ids": [(6, 0, [self.id])],
"invoice_line_ids": [],
"company_id": self.company_id.id,
"payment_reference": self.external_reference or self.reference,
}
return invoice_vals
def _get_folio_default_journal(self, partner_invoice_id):
self.ensure_one()
pms_property = self.pms_property_id
partner = self.env["res.partner"].browse(partner_invoice_id)
if not partner or (
not partner._check_enought_invoice_data()
and self._context.get("autoinvoice")
):
return pms_property.journal_simplified_invoice_id
return pms_property.journal_normal_invoice_id
def do_payment(
self,
journal,
@@ -1691,18 +1825,31 @@ class PmsFolio(models.Model):
"""
if not pay_type:
pay_type = journal.type
reference = folio.name
if folio.external_reference:
reference += " - " + folio.external_reference
vals = {
"journal_id": journal.id,
"partner_id": partner.id,
"amount": amount,
"date": date or fields.Date.today(),
"ref": folio.name,
"ref": reference,
"folio_ids": [(6, 0, [folio.id])],
"payment_type": "inbound",
"partner_type": "customer",
"state": "draft",
"origin_reference": folio.external_reference,
}
pay = self.env["account.payment"].create(vals)
pay.message_post_with_view(
"mail.message_origin_link",
values={
"self": pay,
"origin": folio,
},
subtype_id=self.env.ref("mail.mt_note").id,
)
pay.action_post()
# Review: force to autoreconcile payment with invoices already created
@@ -1762,19 +1909,29 @@ class PmsFolio(models.Model):
"""
if not pay_type:
pay_type = journal.type
reference = folio.name
if folio.external_reference:
reference += " - " + folio.external_reference
vals = {
"journal_id": journal.id,
"partner_id": partner.id,
"amount": amount if amount > 0 else -amount,
"date": date or fields.Date.today(),
"ref": folio.name,
"ref": reference,
"folio_ids": [(6, 0, [folio.id])],
"payment_type": "outbound",
"partner_type": "customer",
"state": "draft",
}
pay = self.env["account.payment"].create(vals)
pay.message_post_with_view(
"mail.message_origin_link",
values={
"self": pay,
"origin": folio,
},
subtype_id=self.env.ref("mail.mt_note").id,
)
pay.action_post()
# Automatic register refund in cash register
@@ -2176,7 +2333,6 @@ class PmsFolio(models.Model):
"partner_id": record.partner_id.id,
"name": record.document_number,
"category_id": record.document_type.id,
"valid_from": record.document_expedition_date,
}
)

View File

@@ -142,6 +142,55 @@ class PmsProperty(models.Model):
is_modified_auto_mail = fields.Boolean(string="Auto Send Modification Mail")
is_canceled_auto_mail = fields.Boolean(string="Auto Send Cancellation Mail")
default_invoicing_policy = fields.Selection(
string="Default Invoicing Policy",
selection=[
("manual", "Manual"),
("checkout", "Checkout"),
("month_day", "Month Day Invoice"),
],
default="manual",
)
margin_days_autoinvoice = fields.Integer(
string="Margin Days",
help="Days from Checkout to generate the invoice",
)
invoicing_month_day = fields.Integer(
string="Invoicing Month Day",
help="The day of the month to invoice",
)
journal_simplified_invoice_id = fields.Many2one(
string="Simplified Invoice Journal",
comodel_name="account.journal",
domain=[
("type", "=", "sale"),
],
help="Journal used to create the simplified invoice",
check_company=True,
check_pms_properties=True,
)
journal_normal_invoice_id = fields.Many2one(
string="Normal Invoice Journal",
comodel_name="account.journal",
domain=[
("type", "=", "sale"),
("is_simplified_invoice", "=", False),
],
help="Journal used to create the normal invoice",
check_company=True,
check_pms_properties=True,
)
max_amount_simplified_invoice = fields.Float(
string="Max Amount Simplified Invoice",
help="Maximum amount to create the simplified invoice",
default=400.0,
)
@api.depends_context(
"checkin",
"checkout",
@@ -551,5 +600,37 @@ class PmsProperty(models.Model):
"closed": True,
}
)
return True
@api.model
def autoinvoicing(self):
"""
This method is used to autoinvoicing the folios
"""
folios = self.env["pms.folio"].search(
[
("autoinvoice_date", "=", fields.date.today()),
]
)
if folios:
invoices = folios.with_context(autoinvoice=True)._create_invoices(
grouped=True,
)
if invoices:
invoices.action_post()
return True
@api.constrains("journal_normal_invoice_id")
def _check_journal_normal_invoice(self):
for pms_property in self.filtered("journal_normal_invoice_id"):
if pms_property.journal_normal_invoice_id.is_simplified_invoice:
raise ValidationError(
_("Journal %s is not allowed to be used for normal invoices")
% pms_property.journal_normal_invoice_id.name
)
@api.constrains("journal_simplified_invoice_id")
def _check_journal_simplified_invoice(self):
for pms_property in self.filtered("journal_simplified_invoice_id"):
if not pms_property.journal_simplified_invoice_id.is_simplified_invoice:
pms_property.journal_simplified_invoice_id.is_simplified_invoice = True

View File

@@ -1597,13 +1597,33 @@ class PmsReservation(models.Model):
)
@api.constrains("reservation_line_ids")
def check_consecutive_dates(self):
def checkin_checkout_consecutive_dates(self):
"""
simply convert date objects to integers using the .toordinal() method
of datetime objects. The difference between the maximum and minimum value
of the set of ordinal dates is one more than the length of the set
"""
for record in self:
if min(record.reservation_line_ids.mapped("date")) != record.checkin:
raise UserError(
_(
"""
Compute error: The first room line date should
be the same as the checkin date!
"""
)
)
if max(
record.reservation_line_ids.mapped("date")
) != record.checkout - datetime.timedelta(days=1):
raise UserError(
_(
"""
Compute error: The last room line date should
be the previous day of the checkout date!
"""
)
)
if record.reservation_line_ids and len(record.reservation_line_ids) > 1:
dates = record.reservation_line_ids.mapped("date")
date_ints = {d.toordinal() for d in dates}
@@ -1874,8 +1894,23 @@ class PmsReservation(models.Model):
record = super(PmsReservation, self).create(vals)
if record.preconfirm and record.state == "draft":
record.confirm()
record._check_services(vals)
return record
def write(self, vals):
asset = super(PmsReservation, self).write(vals)
self._check_services(vals)
return asset
def _check_services(self, vals):
# If we create a reservation with board service and other service at the same time,
# compute_service_ids dont run (compute with readonly to False),
# and we must force it to compute the services linked with the board service:
if "board_service_room_id" in vals and "service_ids" in vals:
self._compute_service_ids()
def update_prices(self):
self.ensure_one()
for line in self.reservation_line_ids:

View File

@@ -346,9 +346,10 @@ class PmsReservationLine(models.Model):
else:
impacts_quota = line.impacts_quota
line.impacts_quota = self.env["pms.availability.plan"].update_quota(
pricelist_id=reservation.pricelist_id,
room_type_id=reservation.room_type_id,
pricelist_id=reservation.pricelist_id.id,
room_type_id=reservation.room_type_id.id,
date=line.date,
pms_property_id=reservation.pms_property_id.id,
impacts_quota_id=impacts_quota,
)

View File

@@ -14,6 +14,8 @@ class ResCompany(models.Model):
inverse_name="company_id",
)
url_advert = fields.Char(string="Url Advert", help="Url to identify the ad")
privacy_policy = fields.Text(
string="Privacy Policy",
help="Authorization by the user for the" "manage of their personal data",

View File

@@ -4,6 +4,7 @@
import logging
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
@@ -42,7 +43,7 @@ class ResPartner(models.Model):
)
pms_property_ids = fields.Many2many(
string="Properties",
help="Properties with access to the element;"
help="Properties with access to the element"
" if not set, all properties can access",
required=False,
comodel_name="pms.property",
@@ -85,15 +86,6 @@ class ResPartner(models.Model):
store=True,
compute="_compute_nationality_id",
)
# TODO: Use new partner contact "other or "private" with
# personal contact address complete??
# to avoid user country_id on companies contacts.
# view to checkin partner state_id field
state_id = fields.Many2one(
readonly=False,
store=True,
compute="_compute_state_id",
)
email = fields.Char(
readonly=False,
store=True,
@@ -104,6 +96,11 @@ class ResPartner(models.Model):
store=True,
compute="_compute_mobile",
)
phone = fields.Char(
readonly=False,
store=True,
compute="_compute_phone",
)
firstname = fields.Char(
readonly=False,
store=True,
@@ -120,6 +117,11 @@ class ResPartner(models.Model):
store=True,
compute="_compute_lastname2",
)
vat = fields.Char(
readonly=False,
store=True,
compute="_compute_vat",
)
comment = fields.Text(
tracking=True,
)
@@ -133,6 +135,109 @@ class ResPartner(models.Model):
string="Possible Customer In Checkin Partner",
comodel_name="pms.checkin.partner",
)
invoicing_policy = fields.Selection(
string="Invoicing Policy",
help="""The invoicing policy of the partner,
set Property to user the policy configured in the Property""",
selection=[
("property", "Property Policy Invoice"),
("manual", "Manual"),
("checkout", "From Checkout"),
("month_day", "Month Day Invoice"),
],
default="property",
)
invoicing_month_day = fields.Integer(
string="Invoicing Month Day",
help="The day of the month to invoice",
)
margin_days_autoinvoice = fields.Integer(
string="Days from Checkout",
help="Days from Checkout to generate the invoice",
)
default_invoice_lines = fields.Selection(
string="Invoice...",
help="""Use to preconfigure the sale lines to autoinvoice
for this partner. All (invoice reservations and services),
Only overnights to invoice only the reservations
with overnight and board services(exclude parkings, salon, etc...),
All reservations to include all reservations,
and Services only include services not boards""",
selection=[
("all", "All"),
("overnights", "Only Overnights"),
("reservations", "All reservations"),
("services", "Services"),
],
default="all",
)
vat_document_type = fields.Selection(
string="Document Type",
help="""The vat document type of the partner,
set if is a fiscal document, passport, etc...""",
selection=lambda self: self._selection_vat_document_type(),
compute="_compute_vat_document_type",
store=True,
)
residence_street = fields.Char(
string="Street of residence",
help="Street of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_street",
)
residence_street2 = fields.Char(
string="Second street of residence",
help="Second street of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_street2",
)
residence_zip = fields.Char(
string="Zip of residence",
help="Zip of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_zip",
change_default=True,
)
residence_city = fields.Char(
string="city of residence",
help="City of the guest's residence",
readonly=False,
store=True,
compute="_compute_residence_city",
)
residence_country_id = fields.Many2one(
string="Country of residence",
help="Partner country of residence",
readonly=False,
store=True,
compute="_compute_residence_country_id",
comodel_name="res.country",
)
residence_state_id = fields.Many2one(
string="State of residence",
help="Partner state of residence",
readonly=False,
store=True,
compute="_compute_residence_state_id",
comodel_name="res.country.state",
)
@api.model
def _selection_vat_document_type(self):
vat_document_types = [
("vat", _("VAT")),
]
document_categories = self.env["res.partner.id_category"].search(
[
("is_vat_equivalent", "=", False),
]
)
for doc_type in document_categories:
vat_document_types.append((doc_type.name, doc_type.name))
return vat_document_types
@api.depends("pms_checkin_partner_ids", "pms_checkin_partner_ids.gender")
def _compute_gender(self):
@@ -188,24 +293,145 @@ class ResPartner(models.Model):
elif not record.nationality_id:
record.nationality_id = False
@api.depends("pms_checkin_partner_ids", "pms_checkin_partner_ids.state_id")
def _compute_state_id(self):
if hasattr(super(), "_compute_state_id"):
super()._compute_state_id()
@api.depends("pms_checkin_partner_ids", "pms_checkin_partner_ids.phone")
def _compute_phone(self):
if hasattr(super(), "_compute_phone"):
super()._compute_phone()
for record in self:
if not record.state_id and record.pms_checkin_partner_ids:
state_id = list(
if not record.phone and record.pms_checkin_partner_ids:
phone = list(
filter(None, set(record.pms_checkin_partner_ids.mapped("phone")))
)
if len(phone) == 1:
record.phone = phone[0]
else:
record.phone = False
elif not record.phone:
record.phone = False
@api.depends("pms_checkin_partner_ids", "pms_checkin_partner_ids.residence_street")
def _compute_residence_street(self):
if hasattr(super(), "_compute_residence_street"):
super()._compute_residence_street()
for record in self:
if not record.residence_street and record.pms_checkin_partner_ids:
residence_street = list(
filter(
None,
set(record.pms_checkin_partner_ids.mapped("state_id")),
set(record.pms_checkin_partner_ids.mapped("residence_street")),
)
)
if len(state_id) == 1:
record.state_id = state_id[0]
if len(residence_street) == 1:
record.residence_street = residence_street[0]
else:
record.state_id = False
elif not record.state_id:
record.state_id = False
record.residence_street = False
elif not record.residence_street:
record.residence_street = False
@api.depends("pms_checkin_partner_ids", "pms_checkin_partner_ids.residence_street2")
def _compute_residence_street2(self):
if hasattr(super(), "_compute_residence_street2"):
super()._compute_residence_street2()
for record in self:
if not record.residence_street2 and record.pms_checkin_partner_ids:
residence_street2 = list(
filter(
None,
set(record.pms_checkin_partner_ids.mapped("residence_street2")),
)
)
if len(residence_street2) == 1:
record.residence_street2 = residence_street2[0]
else:
record.residence_street2 = False
elif not record.residence_street2:
record.residence_street2 = False
@api.depends("pms_checkin_partner_ids", "pms_checkin_partner_ids.residence_zip")
def _compute_residence_zip(self):
if hasattr(super(), "_compute_residence_zip"):
super()._compute_residence_zip()
for record in self:
if not record.residence_zip and record.pms_checkin_partner_ids:
residence_zip = list(
filter(
None,
set(record.pms_checkin_partner_ids.mapped("residence_zip")),
)
)
if len(residence_zip) == 1:
record.residence_zip = residence_zip[0]
else:
record.residence_zip = False
elif not record.residence_zip:
record.residence_zip = False
@api.depends("pms_checkin_partner_ids", "pms_checkin_partner_ids.residence_city")
def _compute_residence_city(self):
if hasattr(super(), "_compute_residence_city"):
super()._compute_residence_city()
for record in self:
if not record.residence_city and record.pms_checkin_partner_ids:
residence_city = list(
filter(
None,
set(record.pms_checkin_partner_ids.mapped("residence_city")),
)
)
if len(residence_city) == 1:
record.residence_city = residence_city[0]
else:
record.residence_city = False
elif not record.residence_city:
record.residence_city = False
@api.depends(
"pms_checkin_partner_ids", "pms_checkin_partner_ids.residence_country_id"
)
def _compute_residence_country_id(self):
if hasattr(super(), "_compute_residence_country_id"):
super()._compute_residence_country_id()
for record in self:
if not record.residence_country_id and record.pms_checkin_partner_ids:
residence_country_id = list(
filter(
None,
set(
record.pms_checkin_partner_ids.mapped(
"residence_country_id"
)
),
)
)
if len(residence_country_id) == 1:
record.residence_country_id = residence_country_id[0]
else:
record.residence_country_id = False
elif not record.residence_country_id:
record.residence_country_id = False
@api.depends(
"pms_checkin_partner_ids", "pms_checkin_partner_ids.residence_state_id"
)
def _compute_residence_state_id(self):
if hasattr(super(), "_compute_residence_state_id"):
super()._compute_residence_state_id()
for record in self:
if not record.residence_state_id and record.pms_checkin_partner_ids:
residence_state_id = list(
filter(
None,
set(
record.pms_checkin_partner_ids.mapped("residence_state_id")
),
)
)
if len(residence_state_id) == 1:
record.residence_state_id = residence_state_id[0]
else:
record.residence_state_id = False
elif not record.residence_state_id:
record.residence_state_id = False
@api.depends(
"pms_checkin_partner_ids",
@@ -327,6 +553,17 @@ class ResPartner(models.Model):
elif not record.lastname2:
record.lastname2 = False
@api.depends("id_numbers", "id_numbers.name")
def _compute_vat(self):
if hasattr(super(), "_compute_vat"):
super()._compute_vat()
for record in self:
if not record.vat and record.id_numbers:
vat = list(filter(None, set(record.id_numbers.mapped("name"))))
record.vat = vat[0]
elif not record.vat:
record.vat = False
def _compute_reservations_count(self):
# Return reservation with partner included in reservation and/or checkin
pms_reservation_obj = self.env["pms.reservation"]
@@ -348,6 +585,27 @@ class ResPartner(models.Model):
]
)
@api.depends(
"vat", "id_numbers", "id_numbers.category_id", "id_numbers.vat_syncronized"
)
def _compute_vat_document_type(self):
self.vat_document_type = False
for record in self.filtered("vat"):
document = record.id_numbers.filtered("vat_syncronized")
if document:
if len(document) > 1:
raise ValidationError(
_("There is more than one document with vat syncronized")
)
if record.vat:
record.vat_document_type = (
document.category_id.name
if not document.category_id.is_vat_equivalent
else "vat"
)
else:
record.vat_document_type = "vat"
def action_partner_reservations(self):
self.ensure_one()
checkin_reservation_ids = (
@@ -499,3 +757,29 @@ class ResPartner(models.Model):
key_fields = super(ResPartner, self)._get_key_fields()
key_fields.extend(["document_number"])
return key_fields
def _check_enought_invoice_data(self):
self.ensure_one()
if self.vat and self.country_id and self.city and self.street:
return True
return False
@api.constrains("vat_document_type")
def check_vat(self):
"""
Inherit constrain to allow set vat in
document ids like passport, etc...
"""
for partner in self:
if not partner.vat_document_type or partner.vat_document_type != "vat":
continue
else:
super(ResPartner, partner).check_vat()
def unlink(self):
dummy, various_partner_id = self.env["ir.model.data"].get_object_reference(
"pms", "various_pms_partner"
)
if various_partner_id in self.ids:
raise ValidationError(_("The partner 'Various Clients' cannot be deleted"))
return super().unlink()

View File

@@ -0,0 +1,11 @@
from odoo import fields, models
class ResPartnerIdCategory(models.Model):
_inherit = "res.partner.id_category"
is_vat_equivalent = fields.Boolean(
string="Is VAT Equivalent",
help="If true, this document type is check by vat number",
default=False,
)

View File

@@ -19,6 +19,11 @@ class ResPartnerIdNumber(models.Model):
store=True,
compute="_compute_valid_from",
)
vat_syncronized = fields.Boolean(
help="Technical field to know if vat partner is syncronized with this document",
compute="_compute_vat_syncronized",
store=True,
)
@api.depends(
"partner_id", "partner_id.pms_checkin_partner_ids.document_expedition_date"
@@ -53,3 +58,15 @@ class ResPartnerIdNumber(models.Model):
)
if len(id_number) > 1:
raise ValidationError(_("Partner already has this document type"))
@api.depends("partner_id", "partner_id.vat", "name")
def _compute_vat_syncronized(self):
self.vat_syncronized = False
for record in self:
if record.partner_id and record.partner_id.vat and record.name:
if record.name.upper() == record.partner_id.vat.upper():
record.vat_syncronized = True
elif not record.partner_id.vat and record.name:
record.vat_syncronized = True
else:
record.vat_syncronized = False

31
pms/report/invoice.xml Normal file
View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="report_invoice_document" inherit_id="account.report_invoice_document">
<xpath expr="//div[@class='page']//h2" position="replace">
<h2>
<span
t-if="o.move_type == 'out_invoice' and o.is_simplified_invoice and o.state == 'posted'"
>Simplified Invoice</span>
<span
t-if="o.move_type == 'out_invoice' and o.is_simplified_invoice and o.state == 'draft'"
>Draft Simplified Invoice</span>
<span
t-if="o.move_type == 'out_invoice' and o.is_simplified_invoice and o.state == 'cancel'"
>Cancelled Simplified Invoice</span>
<span
t-if="o.move_type == 'out_invoice' and not o.is_simplified_invoice and o.state == 'posted'"
>Invoice</span>
<span
t-if="o.move_type == 'out_invoice' and not o.is_simplified_invoice and o.state == 'draft'"
>Draft Invoice</span>
<span
t-if="o.move_type == 'out_invoice' and not o.is_simplified_invoice and o.state == 'cancel'"
>Cancelled Invoice</span>
<span t-if="o.move_type == 'out_refund'">Credit Note</span>
<span t-if="o.move_type == 'in_refund'">Vendor Credit Note</span>
<span t-if="o.move_type == 'in_invoice'">Vendor Bill</span>
<span t-if="o.name != '/'" t-field="o.name" />
</h2>
</xpath>
</template>
</odoo>

View File

@@ -43,9 +43,9 @@
</h2>
<div class="row mt32 mb32" id="informations">
<div t-if="doc.client_order_ref" class="col-auto col-3 mw-100 mb-2">
<div t-if="doc.name" class="col-auto col-3 mw-100 mb-2">
<strong>Your Reference:</strong>
<p class="m-0" t-field="doc.client_order_ref" />
<p class="m-0" t-field="doc.name" />
</div>
<div
t-if="doc.date_order and doc.state not in ['draft','sent']"

View File

@@ -64,3 +64,5 @@ user_access_pms_automated_mails,user_access_pms_automated_mails,model_pms_automa
access_pms_several_partners_wizard,access_pms_several_partners_wizard,model_pms_several_partners_wizard,base.group_user,1,1,1,1
user_access_res_partner_portal,user_access_res_partner_portal,model_res_partner,base.group_portal,1,1,1,1
user_access_pms_precheckin_portal,user_access_pms_precheckin_portal,model_pms_checkin_partner,base.group_portal,1,1,1,1
user_access_pms_booking_duplicate,user_access_pms_booking_duplicate,model_pms_booking_duplicate,pms.group_pms_user,1,1,1,1
user_access_pms_reservation_duplicate,user_access_pms_reservation_duplicate,model_pms_reservation_duplicate,pms.group_pms_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
64 access_pms_several_partners_wizard access_pms_several_partners_wizard model_pms_several_partners_wizard base.group_user 1 1 1 1
65 user_access_res_partner_portal user_access_res_partner_portal model_res_partner base.group_portal 1 1 1 1
66 user_access_pms_precheckin_portal user_access_pms_precheckin_portal model_pms_checkin_partner base.group_portal 1 1 1 1
67 user_access_pms_booking_duplicate user_access_pms_booking_duplicate model_pms_booking_duplicate pms.group_pms_user 1 1 1 1
68 user_access_pms_reservation_duplicate user_access_pms_reservation_duplicate model_pms_reservation_duplicate pms.group_pms_user 1 1 1 1

View File

@@ -1201,7 +1201,6 @@ class TestPmsCheckinPartner(TestPms):
is = 20 years old and document_date = today + 1 year. The expected
expedition date has to be doc_date - 5 years
"""
doc_type_id = self.env["res.partner.id_category"].search([("code", "=", "D")])
doc_date = fields.date.today() + datetime.timedelta(days=366)
doc_date_str = str(doc_date)
@@ -1213,7 +1212,7 @@ class TestPmsCheckinPartner(TestPms):
expected_exp_date = doc_date - datetime.timedelta(days=1826.25)
expedition_date = (
self.checkin1.calculate_doc_type_expedition_date_from_validity_date(
doc_type_id, doc_date_str, birthdate_str
self.id_category, doc_date_str, birthdate_str
)
)
date_expedition_date = datetime.date(
@@ -1238,7 +1237,6 @@ class TestPmsCheckinPartner(TestPms):
is = 40 years old and document_date = today + 1 year. The expected
expedition date has to be doc_date - 10 years
"""
doc_type_id = self.env["res.partner.id_category"].search([("code", "=", "D")])
doc_date = fields.date.today() + datetime.timedelta(days=366)
doc_date_str = str(doc_date)
@@ -1250,7 +1248,7 @@ class TestPmsCheckinPartner(TestPms):
expected_exp_date = doc_date - datetime.timedelta(days=3652.5)
expedition_date = (
self.checkin1.calculate_doc_type_expedition_date_from_validity_date(
doc_type_id, doc_date_str, birthdate_str
self.id_category, doc_date_str, birthdate_str
)
)
date_expedition_date = datetime.date(
@@ -1275,7 +1273,6 @@ class TestPmsCheckinPartner(TestPms):
is = 20 years old and document_date = today + 1 year. The expected
expedition date has to be doc_date - 5 years
"""
doc_type_id = self.env["res.partner.id_category"].search([("code", "=", "P")])
doc_date = fields.date.today() + datetime.timedelta(days=366)
doc_date_str = str(doc_date)
@@ -1287,7 +1284,7 @@ class TestPmsCheckinPartner(TestPms):
expected_exp_date = doc_date - datetime.timedelta(days=1826.25)
expedition_date = (
self.checkin1.calculate_doc_type_expedition_date_from_validity_date(
doc_type_id, doc_date_str, birthdate_str
self.id_category, doc_date_str, birthdate_str
)
)
date_expedition_date = datetime.date(
@@ -1436,7 +1433,7 @@ class TestPmsCheckinPartner(TestPms):
"firstname": "Serafín",
"lastname": "Rivas",
"lastname2": "Gonzalez",
"document_type": self.id_category.name,
"document_type": self.id_category.code,
"document_number": "18038946T",
"document_expedition_date": "2015-10-07",
"birthdate_date": "1983-10-05",
@@ -1464,3 +1461,65 @@ class TestPmsCheckinPartner(TestPms):
checkin_partner_vals[key],
"The value of " + key + " is not correctly established",
)
def test_compute_partner_fields(self):
"""
Check that the computes of the checkin_partner fields related to your partner correctly
add these fields to the checkin_partner.
---------------------------------------
A reservation is created with an adult (checkin_partner) ql which is saved in the
checkin_partner_id variable, a partner is also created with all the fields that are
related to the checkin_partner fields. The partner is added to the partner_id field
of the checkin_partner and, through subtests, it is verified that the fields of the
partner and the associated checkin_partner match.
"""
self.reservation = self.env["pms.reservation"].create(
{
"checkin": datetime.date.today() + datetime.timedelta(days=1),
"checkout": datetime.date.today() + datetime.timedelta(days=2),
"room_type_id": self.room_type1.id,
"partner_id": self.host1.id,
"adults": 1,
"pms_property_id": self.pms_property1.id,
}
)
checkin_partner_id = self.reservation.checkin_partner_ids[0]
nationality_id = self.env["res.country"].browse(1)
state_id = self.env["res.country.state"].browse(1)
partner_vals = {
"firstname": "Paz",
"lastname": "Valenzuela",
"lastname2": "Soto",
"email": "paz@example.com",
"birthdate_date": datetime.date(1980, 10, 5),
"gender": "female",
"mobile": "666555444",
"phone": "123456789",
"nationality_id": nationality_id.id,
"residence_street": "Calle 123",
"residence_street2": "Avda. Constitución 123",
"residence_zip": "15700",
"residence_city": "City Residence",
"residence_country_id": nationality_id.id,
"residence_state_id": state_id.id,
# "pms_checkin_partner_ids": checkin_partner_id,
}
self.partner_id = self.env["res.partner"].create(partner_vals)
partner_vals.update(
{
"nationality_id": nationality_id,
"residence_country_id": nationality_id,
"residence_state_id": state_id,
}
)
checkin_partner_id.partner_id = self.partner_id.id
for key in partner_vals:
if key != "pms_checkin_partner_ids":
with self.subTest(k=key):
self.assertEqual(
self.reservation.checkin_partner_ids[0][key],
self.partner_id[key],
"The value of " + key + " is not correctly established",
)

View File

@@ -10,12 +10,24 @@ class TestPmsFolioInvoice(TestPms):
self.room_type_availability = self.env["pms.availability.plan"].create(
{"name": "Availability plan for TEST"}
)
# journal to simplified invoices
self.simplified_journal = self.env["account.journal"].create(
{
"name": "Simplified journal",
"code": "SMP",
"type": "sale",
"company_id": self.env.ref("base.main_company").id,
}
)
# create a property
self.property = self.env["pms.property"].create(
{
"name": "MY PMS TEST",
"company_id": self.env.ref("base.main_company").id,
"default_pricelist_id": self.pricelist1.id,
"journal_simplified_invoice_id": self.simplified_journal.id,
}
)
@@ -62,6 +74,11 @@ class TestPmsFolioInvoice(TestPms):
self.partner_id = self.env["res.partner"].create(
{
"name": "Miguel",
"vat": "ES123456789",
"country_id": self.env.ref("base.es").id,
"city": "Madrid",
"zip": "28013",
"street": "Calle de la calle",
}
)
@@ -147,7 +164,7 @@ class TestPmsFolioInvoice(TestPms):
] = 1
r1.folio_id._create_invoices(
lines_to_invoice=dict_lines,
partner_invoice_id=self.env.ref("base.res_partner_1"),
partner_invoice_id=self.env.ref("base.res_partner_1").id,
)
# test does not work without invalidating cache
@@ -165,7 +182,7 @@ class TestPmsFolioInvoice(TestPms):
] = 2
r1.folio_id._create_invoices(
lines_to_invoice=dict_lines,
partner_invoice_id=self.env.ref("base.res_partner_12"),
partner_invoice_id=self.env.ref("base.res_partner_12").id,
)
self.assertNotEqual(
r1.folio_id.move_ids.mapped("partner_id")[0],
@@ -298,13 +315,6 @@ class TestPmsFolioInvoice(TestPms):
{"name": "Test Product 1", "per_day": True, "list_price": 10}
)
self.service1 = self.env["pms.service"].create(
{
"is_board_service": False,
"product_id": self.product1.id,
}
)
self.reservation1 = self.env["pms.reservation"].create(
{
"pms_property_id": self.property.id,
@@ -313,13 +323,21 @@ class TestPmsFolioInvoice(TestPms):
"adults": 2,
"room_type_id": self.room_type_double.id,
"partner_id": self.partner_id.id,
"service_ids": [(6, 0, [self.service1.id])],
}
)
self.service1 = self.env["pms.service"].create(
{
"is_board_service": False,
"product_id": self.product1.id,
"reservation_id": self.reservation1.id,
}
)
dict_lines = dict()
dict_lines[
self.reservation1.folio_id.sale_line_ids.filtered("service_id")[0].id
] = 1
] = 3
self.reservation1.folio_id._create_invoices(lines_to_invoice=dict_lines)
self.assertEqual(
self.reservation1.folio_id.sale_line_ids.filtered("service_id")[
@@ -339,13 +357,6 @@ class TestPmsFolioInvoice(TestPms):
{"name": "Test Product 1", "per_day": True, "list_price": 10}
)
self.service1 = self.env["pms.service"].create(
{
"is_board_service": False,
"product_id": self.product1.id,
}
)
self.reservation1 = self.env["pms.reservation"].create(
{
"pms_property_id": self.property.id,
@@ -354,9 +365,17 @@ class TestPmsFolioInvoice(TestPms):
"adults": 2,
"room_type_id": self.room_type_double.id,
"partner_id": self.partner_id.id,
"service_ids": [(6, 0, [self.service1.id])],
}
)
self.service1 = self.env["pms.service"].create(
{
"is_board_service": False,
"product_id": self.product1.id,
"reservation_id": self.reservation1.id,
}
)
dict_lines = dict()
service_lines = self.reservation1.folio_id.sale_line_ids.filtered("service_id")
for line in service_lines:
@@ -381,13 +400,6 @@ class TestPmsFolioInvoice(TestPms):
{"name": "Test Product 1", "per_day": True, "list_price": 10}
)
self.service1 = self.env["pms.service"].create(
{
"is_board_service": False,
"product_id": self.product1.id,
}
)
self.reservation1 = self.env["pms.reservation"].create(
{
"pms_property_id": self.property.id,
@@ -396,9 +408,17 @@ class TestPmsFolioInvoice(TestPms):
"adults": 2,
"room_type_id": self.room_type_double.id,
"partner_id": self.partner_id.id,
"service_ids": [(6, 0, [self.service1.id])],
}
)
self.service1 = self.env["pms.service"].create(
{
"is_board_service": False,
"product_id": self.product1.id,
"reservation_id": self.reservation1.id,
}
)
expected_qty_to_invoice = sum(
self.reservation1.folio_id.sale_line_ids.filtered("service_id").mapped(
"qty_to_invoice"
@@ -579,14 +599,157 @@ class TestPmsFolioInvoice(TestPms):
"The quantity of board services to be invoice is wrong",
)
def test_autoinvoice_folio_checkout_property_policy(self):
"""
Test create and invoice the cron by property preconfig automation
--------------------------------------
Set property default_invoicing_policy to checkout with 0 days with
margin, and check that the folio autoinvoice date is set to last checkout
folio date
"""
# ARRANGE
self.property.default_invoicing_policy = "checkout"
self.property.margin_days_autoinvoice = 0
# ACT
self.reservation1 = self.env["pms.reservation"].create(
{
"pms_property_id": self.property.id,
"checkin": datetime.date.today(),
"checkout": datetime.date.today() + datetime.timedelta(days=3),
"adults": 2,
"room_type_id": self.room_type_double.id,
"partner_id": self.partner_id.id,
}
)
# ASSERT
self.assertEqual(
datetime.date.today() + datetime.timedelta(days=3),
self.reservation1.folio_id.autoinvoice_date,
"The autoinvoice date in folio with property checkout policy is wrong",
)
def test_autoinvoice_folio_checkout_partner_policy(self):
"""
Test create and invoice the cron by partner preconfig automation
--------------------------------------
Set partner invoicing_policy to checkout with 2 days with
margin, and check that the folio autoinvoice date is set to last checkout
folio date + 2 days
"""
# ARRANGE
self.partner_id.invoicing_policy = "checkout"
self.partner_id.margin_days_autoinvoice = 2
# ACT
self.reservation1 = self.env["pms.reservation"].create(
{
"pms_property_id": self.property.id,
"checkin": datetime.date.today(),
"checkout": datetime.date.today() + datetime.timedelta(days=3),
"adults": 2,
"room_type_id": self.room_type_double.id,
"partner_id": self.partner_id.id,
}
)
# ASSERT
self.assertEqual(
datetime.date.today() + datetime.timedelta(days=5),
self.reservation1.folio_id.autoinvoice_date,
"The autoinvoice date in folio with property checkout policy is wrong",
)
def test_autoinvoice_folio_overnights_partner_policy(self):
"""
Test create and invoice the cron by partner preconfig automation
with only overnights reservations (included board services)
--------------------------------------
Set partner invoicing_policy to checkout, create a reservation
with room, board service and normal service, run autoinvoicing
method and check that only room and board service was invoiced
in partner1,
"""
# ARRANGE
self.partner_id.invoicing_policy = "checkout"
self.partner_id.margin_days_autoinvoice = 0
self.partner_id.default_invoice_lines = "overnights"
self.product1 = self.env["product.product"].create(
{
"name": "Test Product 1",
}
)
self.product2 = self.env["product.product"].create(
{
"name": "Test Product 2",
"lst_price": 100,
}
)
self.board_service1 = self.env["pms.board.service"].create(
{
"name": "Test Board Service 1",
"default_code": "CB1",
"amount": 10,
}
)
self.board_service_line1 = self.env["pms.board.service.line"].create(
{
"product_id": self.product1.id,
"pms_board_service_id": self.board_service1.id,
"amount": 10,
}
)
self.board_service_room_type1 = self.env["pms.board.service.room.type"].create(
{
"pms_room_type_id": self.room_type_double.id,
"pms_board_service_id": self.board_service1.id,
}
)
# ACT
self.reservation1 = self.env["pms.reservation"].create(
{
"pms_property_id": self.property.id,
"checkin": datetime.date.today() - datetime.timedelta(days=3),
"checkout": datetime.date.today(),
"adults": 2,
"room_type_id": self.room_type_double.id,
"partner_id": self.partner_id.id,
"board_service_room_id": self.board_service_room_type1.id,
}
)
self.service = self.env["pms.service"].create(
{
"is_board_service": False,
"product_id": self.product2.id,
"reservation_id": self.reservation1.id,
}
)
self.property.autoinvoicing()
# ASSERT
overnight_sale_lines = self.reservation1.folio_id.sale_line_ids.filtered(
lambda line: line.reservation_line_ids or line.is_board_service
)
partner_invoice = self.reservation1.folio_id.move_ids.filtered(
lambda inv: inv.partner_id == self.partner_id
)
self.assertEqual(
partner_invoice.mapped("line_ids.folio_line_ids.id"),
overnight_sale_lines.ids,
"Billed services and overnights invoicing wrong compute",
)
def _test_invoice_line_group_by_room_type_sections(self):
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
and the grouped invoice lines by room type, by one
line by unit prices/qty with nights"""
def _test_autoinvoice_folio(self):
"""Test create and invoice the cron by partner preconfig automation"""
def _test_downpayment(self):
"""Test invoice qith a way of downpaument and check dowpayment's
folio line is created and also check a total amount of invoice is

View File

@@ -4,9 +4,10 @@
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='ref']" position="after">
<xpath expr="//field[@name='company_id']" position="after">
<field name="pms_property_id" />
<field name="pms_property_id" invisible="1" />
<field name="folio_ids" widget="many2many_tags" optional="hidden" />
</xpath>
<xpath expr="//field[@name='ref']" position="after">

View File

@@ -5,17 +5,41 @@
<field name="inherit_id" ref="account.view_move_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_date']" position="after">
<field name="folio_ids" widget="many2many_tags" />
<field name="pms_property_id" />
<field
name="folio_ids"
widget="many2many_tags"
readonly="1"
attrs="{'invisible':[('move_type','not in',('out_invoice','out_refund','out_receipt'))]}"
/>
<field
name="pms_property_id"
domain="[('company_id','=',company_id)]"
/>
</xpath>
<xpath expr="//field[@name='quantity']" position="before">
<xpath expr="//field[@name='invoice_user_id']" position="before">
<field name="origin_agency_id" />
</xpath>
<xpath
expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']"
position="before"
>
<field name="name_changed_by_user" invisible="1" />
<field
name="pms_property_id"
attrs="{'column_invisible':[('parent.pms_property_id','!=',False)]}"
/>
</xpath>
<xpath
expr="//notebook//field[@name='line_ids']/tree/field[@name='date_maturity']"
position="after"
>
<field name="name_changed_by_user" invisible="1" />
<field
name="pms_property_id"
attrs="{'column_invisible':[('parent.pms_property_id','!=',False)]}"
optional="hide"
/>
</xpath>
</field>
</record>
@@ -25,6 +49,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="after">
<field name="pms_property_id" optional="show" />
<field name="origin_agency_id" optional="show" />
</xpath>
</field>
</record>
@@ -42,6 +67,11 @@
string="Property"
context="{'group_by':'pms_property_id'}"
/>
<filter
name="origin_agency_id"
string="Originating agency"
context="{'group_by':'origin_agency_id'}"
/>
</xpath>
</field>
</record>
@@ -59,6 +89,11 @@
string="Property"
context="{'group_by':'pms_property_id'}"
/>
<filter
name="by_origin_agency"
string="Originating agency"
context="{'group_by':'origin_agency_id'}"
/>
</xpath>
</field>
</record>

View File

@@ -5,7 +5,56 @@
<field name="inherit_id" ref="account.view_account_payment_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<field name="folio_ids" />
<field
name="folio_ids"
widget="many2many_tags"
attrs="{'invisible':[('partner_type','!=','customer')]}"
/>
<field
name="origin_agency_id"
attrs="{'invisible':[('origin_agency_id','=',False)]}"
/>
<field
name="origin_reference"
attrs="{'invisible':[('origin_agency_id','=',False)]}"
/>
</xpath>
</field>
</record>
<record id="view_account_payment_tree" model="ir.ui.view">
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_tree" />
<field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="before">
<field name="folio_ids" optional="hidden" />
<field name="pms_property_id" optional="hidden" />
<field name="origin_agency_id" optional="hidden" />
<field name="origin_reference" optional="hidden" />
</xpath>
</field>
</record>
<record id="view_account_payment_search" model="ir.ui.view">
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_search" />
<field name="arch" type="xml">
<xpath expr="//field[@name='journal_id']" position="after">
<field name="pms_property_id" />
<field name="origin_agency_id" />
<field name="origin_reference" />
</xpath>
<xpath expr="//filter[@name='company']" position="after">
<filter
name="by_property"
string="Property"
context="{'group_by':'pms_property_id'}"
/>
<filter
name="by_origin_agency"
string="Originating agency"
context="{'group_by':'origin_agency_id'}"
/>
</xpath>
</field>
</record>

View File

@@ -67,11 +67,46 @@
<field name="document_number" />
<field name="document_expedition_date" />
<field name="nationality_id" />
<field name="state_id" />
<field name="email" />
<field name="mobile" />
<field name="phone" />
<field name="document_id" invisible="1" />
<field name="pms_property_id" invisible="1" />
<label for="residence_street" string="Residence Address" />
<div class="o_address_format">
<field
name="residence_street"
placeholder="Street..."
class="o_address_street"
/>
<field
name="residence_street2"
placeholder="Street 2..."
class="o_address_street"
/>
<field
name="residence_city"
placeholder="City"
class="o_address_city"
/>
<field
name="residence_state_id"
class="o_address_state"
placeholder="State"
options="{'no_open': True, 'no_quick_create': True}"
/>
<field
name="residence_zip"
placeholder="ZIP"
class="o_address_zip"
/>
<field
name="residence_country_id"
placeholder="Country"
class="o_address_country"
options='{"no_open": True, "no_create": True}'
/>
</div>
<field name="arrival" />
<field name="departure" />
</group>
@@ -79,6 +114,7 @@
<field name="reservation_id" />
<field name="folio_id" />
<field name="identifier" />
<field name="partner_relationship" />
<field name="pms_property_id" invisible="1" />
</group>
</group>
@@ -126,7 +162,7 @@
<field name="document_number" />
<field name="document_expedition_date" />
<field name="nationality_id" />
<field name="state_id" />
<field name="residence_state_id" />
<field name="mobile" />
<field name="email" />
<field name="arrival" />
@@ -172,7 +208,7 @@
<field name="document_number" />
<field name="document_expedition_date" />
<field name="nationality_id" />
<field name="state_id" />
<field name="residence_state_id" />
<field name="mobile" />
<field name="email" />
<field name="arrival" invisible="1" />
@@ -215,7 +251,7 @@
<field name="document_number" />
<field name="document_expedition_date" />
<field name="nationality_id" />
<field name="state_id" />
<field name="residence_state_id" />
<field name="mobile" />
<field name="email" />
<field name="arrival" />
@@ -254,7 +290,7 @@
<field name="document_number" />
<field name="document_expedition_date" />
<field name="nationality_id" />
<field name="state_id" />
<field name="residence_state_id" />
<field name="email" />
<field name="mobile" />
<field name="arrival" />

View File

@@ -176,6 +176,15 @@
>
<span class="o_stat_text">New Booking Group</span>
</button>
<button
type="action"
class="oe_stat_button"
name="%(action_booking_duplicate)d"
icon="fa-clone"
context="{'default_reference_folio_id': id}"
>
<span class="o_stat_text">Duplicate Booking</span>
</button>
<button
name="preview_folio"
type="object"
@@ -213,16 +222,48 @@
bg_color="bg-dark"
attrs="{'invisible': [('reservation_type', 'not in', 'out')]}"
/>
<h2>
<field name="name" />
</h2>
<group col="8">
<group>
<group>
<h2>
<field name="name" />
</h2>
</group>
<group
colspan="2"
col="3"
string="General Info"
name="contact_details"
class="oe_subtotal_footer oe_right oe_inline"
name="folio_total"
attrs="{'invisible':[('reservation_type', '!=', 'normal')]}"
>
<field
name="amount_untaxed"
widget="monetary"
options="{'currency_field': 'currency_id'}"
/>
<field
name="amount_tax"
widget="monetary"
options="{'currency_field': 'currency_id'}"
/>
<div
class="oe_subtotal_footer_separator oe_inline o_td_label"
>
<label for="amount_total" />
</div>
<field
name="amount_total"
nolabel="1"
class="oe_subtotal_footer_separator"
widget="monetary"
options="{'currency_field': 'currency_id'}"
/>
<field
name="commission"
widget='monetary'
options="{'currency_field': 'currency_id'}"
/>
</group>
</group>
<group>
<group string="General Info" name="contact_details">
<field
name="document_type"
attrs="{'invisible':[('reservation_type','in',('out'))]}"
@@ -269,13 +310,8 @@
/>
<field name="internal_comment" />
</group>
<group
colspan="2"
col="3"
string="Sale Details"
name="sale_details"
>
<field name="pms_property_id" invisible="0" />
<group string="Sale Details" name="sale_details">
<field name="pms_property_id" />
<field
name="pricelist_id"
attrs="{'invisible': [('reservation_type', 'not in', 'normal')]}"
@@ -293,45 +329,15 @@
name="agency_id"
attrs="{'invisible': [('reservation_type', 'not in', 'normal')]}"
/>
<field
name="external_reference"
attrs="{'invisible': [('agency_id', '=', False)]}"
/>
<field
name="channel_type_id"
attrs="{'readonly':[('agency_id','!=', False)], 'invisible':[('reservation_type', 'not in', 'normal')]}"
/>
</group>
<group
class="oe_subtotal_footer oe_right"
colspan="2"
name="folio_total"
attrs="{'invisible':[('reservation_type', '!=', 'normal')]}"
>
<field
name="amount_untaxed"
widget="monetary"
options="{'currency_field': 'currency_id'}"
/>
<field
name="amount_tax"
widget="monetary"
options="{'currency_field': 'currency_id'}"
/>
<div
class="oe_subtotal_footer_separator oe_inline o_td_label"
>
<label for="amount_total" />
</div>
<field
name="amount_total"
nolabel="1"
class="oe_subtotal_footer_separator"
widget="monetary"
options="{'currency_field': 'currency_id'}"
/>
<field
name="commission"
widget='monetary'
options="{'currency_field': 'currency_id'}"
/>
</group>
<div class="oe_clear" />
</group>
@@ -562,7 +568,6 @@
<page string="Other data">
<group>
<field name="user_id" />
<field name="client_order_ref" />
</group>
</page>
<!-- <page string="Foreign Exchange" name="foreign exchange" invisible="1">

View File

@@ -85,6 +85,32 @@
<field name="room_ids" />
</group>
</page>
<page string="Invoicing" name="property_invoicing">
<group>
<field name="default_invoicing_policy" />
<field
name="margin_days_autoinvoice"
attrs="{'invisible': [('default_invoicing_policy', '=', 'manual')]}"
/>
<field
name="journal_normal_invoice_id"
attrs="{'required': [('default_invoicing_policy', '!=', 'manual')]}"
domain="[
('type', '=', 'sale'),
('company_id','=',company_id),
]"
/>
<field
name="journal_simplified_invoice_id"
attrs="{'required': [('default_invoicing_policy', '!=', 'manual')]}"
domain="[
('type', '=', 'sale'),
('company_id','=',company_id),
]"
/>
<field name="max_amount_simplified_invoice" />
</group>
</page>
<page string="Email Configuration">
<group>
<div class="row">

View File

@@ -126,7 +126,10 @@
</option>
</select>
</div>
<div t-attf-class="form-group col-12 col-md-6">
<div
name="document_type_div"
t-attf-class="form-group col-12 col-md-6"
>
<label
class="col-form-label"
for="document_type"
@@ -156,7 +159,10 @@
/>
</t>
</div>
<div t-attf-class="col-12 col-md-6">
<div
name="document_number_div"
t-attf-class="col-12 col-md-6"
>
<label
class="col-form-label"
for="document_number"
@@ -236,24 +242,6 @@
</t>
</select>
</div>
<div class="form-group col-12 col-md-6">
<label
class="col-form-label"
for="state"
>Country State</label>
<select class="form-control" id="state" name='state'>
<option value="">Select an option</option>
<t t-foreach="state_ids" t-as='state'>
<option
t-att-value="state.id"
t-att-country_id="state.country_id.id"
t-att-selected="state.id == checkin_partner_id.state_id.id"
>
<t t-esc="state.name" />
</option>
</t>
</select>
</div>
<div t-attf-class="form-group col-12 col-md-6 pt-5">
<label
id="label_mobile"
@@ -274,6 +262,25 @@
</t>
</div>
<div t-attf-class="form-group col-12 col-md-6 pt-md-5">
<label
id="label_phone"
class="col-form-label"
for="phone"
>Phone</label>
<input
type="phone"
name="phone"
t-attf-class="form-control #{error.get('email') and 'is-invalid' or ''}"
t-att-value="checkin_partner_id.phone"
/>
<t t-if="error_message">
<span
class="text-danger"
t-esc="error_message.get('phone')"
/>
</t>
</div>
<div t-attf-class="form-group col-12 col-md-6">
<label
id="label_email"
class="col-form-label"
@@ -292,6 +299,104 @@
/>
</t>
</div>
<div t-attf-class="form-group col-12 col-md-6 ">
<label
id="label_relationship"
class="col-form-label"
for="partner_relationship"
>Partner Relationship</label>
<input
type="text"
name="partner_relationship"
t-attf-class="form-control #{error.get('email') and 'is-invalid' or ''}"
t-att-value="checkin_partner_id.partner_relationship"
/>
</div>
<div t-attf-class="form-group col-md-12 pt-md-5">
<label
class="col-form-label"
for="residence_street"
>Residence Address</label>
<input
type="text"
placeholder="Street"
name="residence_street"
t-attf-class="form-control #{error.get('email') and 'is-invalid' or ''}"
t-att-value="checkin_partner_id.residence_street"
/>
</div>
<div t-attf-class="form-group col-md-12">
<input
type="text"
placeholder="Second Street"
name="residence_street2"
t-attf-class="form-control #{error.get('email') and 'is-invalid' or ''}"
t-att-value="checkin_partner_id.residence_street2"
/>
</div>
<div t-attf-class="form-group col-md-4">
<input
type="text"
placeholder="City"
name="residence_city"
t-attf-class="form-control #{error.get('email') and 'is-invalid' or ''}"
t-att-value="checkin_partner_id.residence_city"
/>
</div>
<div t-attf-class="form-group col-md-4">
<input
type="text"
placeholder="Zip"
name="residence_zip"
t-attf-class="form-control #{error.get('email') and 'is-invalid' or ''}"
t-att-value="checkin_partner_id.residence_zip"
/>
</div>
<div t-attf-class="form-group col-md-4">
<select
class="form-control"
id="residence-country"
name='residence_country_id'
onclick="changeCountryFormClass()"
>
<option
id="country_placeholder"
hidden="1"
value="placeholder"
>Country</option>
<t t-foreach="country_ids" t-as='country_id'>
<option
t-att-value="country_id.id"
t-att-selected="country_id.id == checkin_partner_id.residence_country_id.id if checkin_partner_id.residence_country_id.id else placeholder"
>
<t t-esc='country_id.name' />
</option>
</t>
</select>
</div>
<div t-attf-class="form-group col-md-12 pb-md-3">
<select
class="form-control"
id="residence-state"
name='residence_state_id'
onclick="changeFormClass()"
>
<option
id="placeholder"
hidden="1"
value="placeholder"
>State</option>
<t t-foreach="state_ids" t-as='state'>
<option
t-att-value="state.id"
t-att-country_id="state.country_id.id"
t-att-selected="state.id == checkin_partner_id.residence_state_id.id if checkin_partner_id.residence_state_id.id else placeholder"
>
<t t-esc="state.name" />
</option>
</t>
</select>
</div>
<t t-if="checkin_pos >= 0">
<div class="col-4">
<t t-if="not error">
@@ -410,8 +515,69 @@
from {top: 100px; opacity: 1;}
to {top: 0; opacity: 0;}
}
.placeholder-class{
display: block;
width: 100%;
height: calc(1.5em + 0.75rem + 2px);
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-weight: 400;
line-height: 1.5;
color: grey;
background-color: #FFFFFF;
background-clip: padding-box;
border: 1px solid #CED4DA;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
</style>
<script>
var placeholder_country = document.getElementById("country_placeholder")
function changeCountryFormClass() {
let select_value = placeholder_country.parentNode.value
if (placeholder_country.parentNode.value == 'placeholder'){
placeholder_country.parentNode.classList.add('placeholder-class')
}else{
placeholder_country.parentNode.classList.remove('placeholder-class')
}
};
this.changeCountryFormClass()
var placeholder = document.getElementById("placeholder")
function changeFormClass() {
let select_value = placeholder.parentNode.value
if (placeholder.parentNode.value == 'placeholder'){
placeholder.parentNode.classList.remove('form-control')
placeholder.parentNode.classList.add('placeholder-class')
}else{
placeholder.parentNode.classList.remove('placeholder-class')
placeholder.parentNode.classList.add('form-control')
}
};
this.changeFormClass()
let select_residence_country = document.getElementById('residence-country')
select_residence_country.addEventListener("change", () => {
let country_value = select_residence_country.value
Array.from(document.getElementById('residence-state').options).forEach(element => {
if (element.getAttribute('country_id') == country_value) {
element.style="";
} else {
element.style="display:none";
}
});
});
var select_doc_type = document.getElementById("doc_type")
var document_type_value = document.getElementById("docTypeId").textContent
for (let i=0;i&lt;select_doc_type.length;i++){
if (select_doc_type[i].value == document_type_value){
select_doc_type[i].setAttribute("selected","True")
}
}
var folio_id = document.getElementById("folio").value
var checkin_partner_id = document.getElementById("checkin").value
var access_token = document.getElementById("input_access_token").value
@@ -999,6 +1165,7 @@
</span>
</div>
<div
name="birthdate_div"
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
Birthdate:
@@ -1024,6 +1191,19 @@
/>
</span>
</div>
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
Phone:
<br />
<span
class="font-weight-normal ml-3"
>
<t
t-esc="checkin_partner.phone"
/>
</span>
</div>
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
@@ -1053,13 +1233,112 @@
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
Country State:
Partner Relationship:
<br />
<span
class="font-weight-normal ml-3"
>
<t
t-esc="checkin_partner.state_id.name"
t-esc="checkin_partner.partner_relationship"
/>
</span>
</div>
<div
t-attf-class="form-group col-12 mt-3 font-italic font-weight-bold"
>
Residence Address
</div>
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
Street:
<br />
<span
class="font-weight-normal ml-3"
>
<t
t-esc="checkin_partner.residence_street"
/>
</span>
</div>
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
Second Street:
<br />
<span
class="font-weight-normal ml-3"
>
<t
t-esc="checkin_partner.residence_street2"
/>
</span>
</div>
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
City:
<br />
<span
class="font-weight-normal ml-3"
>
<t
t-esc="checkin_partner.residence_city"
/>
</span>
</div>
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
ZIP:
<br />
<span
class="font-weight-normal ml-3"
>
<t
t-esc="checkin_partner.residence_zip"
/>
</span>
</div>
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
State:
<br />
<span
class="font-weight-normal ml-3"
>
<t
t-set="residence_state_id"
t-value="int(checkin_partner.residence_state_id.id)"
/>
<t
t-set="residence_state"
t-value="folio.env['res.country.state'].browse(residence_state_id)"
/>
<t
t-esc="residence_state.name"
/>
</span>
</div>
<div
t-attf-class="form-group col-12 col-md-6 font-weight-bold"
>
Country:
<br />
<span
class="font-weight-normal ml-3"
>
<t
t-set="residence_country_id"
t-value="int(checkin_partner.residence_country_id.id)"
/>
<t
t-set="residence_country"
t-value="folio.env['res.country'].browse(residence_country_id)"
/>
<t
t-esc="residence_country.name"
/>
</span>
</div>

View File

@@ -11,6 +11,9 @@
</group>
</page>
</xpath>
<xpath expr="//field[@name='website']" position="after">
<field name="url_advert" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_partner_id_category_form" model="ir.ui.view">
<field name="model">res.partner.id_category</field>
<field
name="inherit_id"
ref="partner_identification.view_partner_id_category_form"
/>
<field name="arch" type="xml">
<xpath expr="//field[@name='code']" position="after">
<field name="is_vat_equivalent" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -64,10 +64,17 @@
<field name="is_agency" />
</group>
</xpath>
<xpath expr="//field[@name='company_id']" position="after">
<group>
<field name="pms_property_ids" />
</group>
<xpath expr="//field[@name='vat']" position="after">
<field
name="vat_document_type"
attrs="{'invisible':[('vat','=',False)]}"
/>
</xpath>
<xpath
expr="//notebook/page[@name='sales_purchases']/group/group/field[@name='company_id']"
position="after"
>
<field name="pms_property_ids" widget="many2many_tags" />
</xpath>
<xpath expr="//page[@name='internal_notes']" position="after">
<page
@@ -91,8 +98,70 @@
</group>
</page>
</xpath>
<xpath expr="//page[@name='personal_information_page']">
<group>
<label for="residence_street" string="Residence Address" />
<div class="o_address_format">
<field
name="residence_street"
placeholder="Street..."
class="o_address_street"
/>
<field
name="residence_street2"
placeholder="Street 2..."
class="o_address_street"
/>
<field
name="residence_city"
placeholder="City"
class="o_address_city"
/>
<field
name="residence_state_id"
class="o_address_state"
placeholder="State"
options="{'no_open': True, 'no_quick_create': True}"
/>
<field
name="residence_zip"
placeholder="ZIP"
class="o_address_zip"
/>
<field
name="residence_country_id"
placeholder="Country"
class="o_address_country"
options='{"no_open": True, "no_create": True}'
/>
</div>
</group>
</xpath>
</field>
</record>
<record id="view_partner_property_form" model="ir.ui.view">
<field name="name">view.partner.property.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form" />
<field name="arch" type="xml">
<xpath expr='//group[@name="accounting_entries"]' position='after'>
<group string="PMS Invoice policy" name="pms_invoice_policy">
<field name="invoicing_policy" />
<field
name="invoicing_month_day"
attrs="{'invisible': [('invoicing_policy', '!=', 'month_day')]}"
/>
<field
name="margin_days_autoinvoice"
attrs="{'invisible': [('invoicing_policy', '!=', 'checkout')]}"
/>
<field name="default_invoice_lines" />
</group>
</xpath>
</field>
</record>
<record id="view_partner_data_form" model="ir.ui.view">
<field name="name">res.partner.form.data</field>
<field name="model">res.partner</field>

View File

@@ -6,3 +6,4 @@ from . import folio_make_invoice_advance
from . import wizard_payment_folio
from . import wizard_folio_changes
from . import wizard_several_partners
from . import pms_booking_duplicate

View File

@@ -153,7 +153,7 @@ class FolioAdvancePaymentInv(models.TransientModel):
def _prepare_invoice_values(self, order, name, amount, line):
invoice_vals = {
"ref": order.client_order_ref,
"ref": order.name,
"move_type": "out_invoice",
"invoice_origin": order.name,
"invoice_user_id": order.user_id.id,
@@ -243,7 +243,7 @@ class FolioAdvancePaymentInv(models.TransientModel):
folios._create_invoices(
final=self.deduct_down_payments,
lines_to_invoice=lines_to_invoice,
partner_invoice_id=self.partner_invoice_id
partner_invoice_id=self.partner_invoice_id.id
if self.partner_invoice_id
else False,
)

View File

@@ -0,0 +1,563 @@
import datetime
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class BookingDuplicate(models.TransientModel):
_name = "pms.booking.duplicate"
_description = "Duplicate Booking"
_check_pms_properties_auto = True
reference_folio_id = fields.Many2one(
string="Folio Reference",
help="Folio to copy data",
comodel_name="pms.folio",
check_pms_properties=True,
)
start_date = fields.Date(
string="From:",
help="Date from first copy Checkin (reference min checkin folio reservation)",
required=True,
)
used_room_ids = fields.Many2many(
string="Used Rooms",
comodel_name="pms.room",
compute="_compute_used_room_ids",
)
pricelist_id = fields.Many2one(
string="Pricelist",
help="Pricelist applied in folio",
readonly=False,
store=True,
comodel_name="product.pricelist",
compute="_compute_pricelist_id",
check_pms_properties=True,
)
pms_property_id = fields.Many2one(
related="reference_folio_id.pms_property_id",
string="Property",
help="Property to which the folio belongs",
comodel_name="pms.property",
check_pms_properties=True,
)
segmentation_ids = fields.Many2many(
string="Segmentation",
help="Partner Tags",
ondelete="restrict",
comodel_name="res.partner.category",
compute="_compute_segmentation_ids",
store=True,
readonly=False,
)
partner_name = fields.Char(
string="Partner name",
help="In whose name is the reservation",
compute="_compute_partner_name",
readonly=False,
store=True,
)
partner_id = fields.Many2one(
string="Partner",
help="Partner who made the reservation",
comodel_name="res.partner",
compute="_compute_partner_id",
readonly=False,
store=True,
check_pms_properties=True,
)
reservation_type = fields.Selection(
string="Type",
help="The type of the reservation. "
"Can be 'Normal', 'Staff' or 'Out of Service'",
selection=[("normal", "Normal"), ("staff", "Staff"), ("out", "Out of Service")],
compute="_compute_reservation_type",
readonly=False,
store=True,
)
agency_id = fields.Many2one(
string="Agency",
help="Agency that made the reservation",
comodel_name="res.partner",
domain=[("is_agency", "=", True)],
ondelete="restrict",
)
channel_type_id = fields.Many2one(
string="Direct Sale Channel",
help="Sales Channel through which the reservation was managed",
readonly=False,
store=True,
comodel_name="pms.sale.channel",
domain=[("channel_type", "=", "direct")],
ondelete="restrict",
compute="_compute_channel_type_id",
)
total_price_folio = fields.Float(
string="Total Price",
help="Total price of folio with taxes",
compute="_compute_total_price_folio",
)
discount = fields.Float(
string="Discount",
help="Discount that be applied in total price",
default=0,
)
internal_comment = fields.Text(
string="Internal Folio Notes",
help="Internal Folio notes for Staff",
)
created_folio_ids = fields.Many2many(
string="Folios",
help="Folios already created",
comodel_name="pms.folio",
)
line_ids = fields.One2many(
string="Rooms",
help="Rooms to create",
readonly=False,
store=True,
comodel_name="pms.reservation.duplicate",
inverse_name="booking_duplicate_id",
compute="_compute_line_ids",
check_pms_properties=True,
)
recompute_prices = fields.Boolean(
string="Recompute Price",
help="""Leave unchecked if you want to respect
the price of the original reservation regardless
of what is marked in the rate""",
default=False,
)
@api.depends("line_ids", "line_ids.preferred_room_id")
def _compute_used_room_ids(self):
for record in self:
record.used_room_ids = record.line_ids.mapped("preferred_room_id.id")
@api.depends("reference_folio_id")
def _compute_pricelist_id(self):
for record in self.filtered("reference_folio_id"):
if not record.pricelist_id:
record.pricelist_id = record.reference_folio_id.pricelist_id.id
@api.depends("reference_folio_id", "agency_id")
def _compute_channel_type_id(self):
for record in self.filtered("reference_folio_id"):
if record.reference_folio_id.agency_id == record.agency_id:
record.channel_type_id = record.reference_folio_id.channel_type_id
elif record.agency_id:
record.channel_type_id = record.agency_id.sale_channel_id.id
@api.depends("reference_folio_id")
def _compute_segmentation_ids(self):
for record in self:
record.segmentation_ids = record.reference_folio_id.segmentation_ids
@api.depends("agency_id", "reference_folio_id")
def _compute_partner_id(self):
for record in self:
if record.reference_folio_id.agency_id == record.agency_id:
record.partner_id = record.reference_folio_id.partner_id
elif record.agency_id and record.agency_id.invoice_to_agency:
record.partner_id = record.agency_id.id
elif not record.partner_id:
record.partner_id = False
@api.depends("reference_folio_id")
def _compute_reservation_type(self):
self.reservation_type = "normal"
for record in self:
record.reservation_type = record.reference_folio_id.reservation_type
@api.depends("partner_id")
def _compute_partner_name(self):
for record in self:
if record.reference_folio_id.partner_id == record.partner_id:
record.partner_name = record.reference_folio_id.partner_name
elif record.partner_id:
record.partner_name = record.partner_id.name
if (
record.agency_id
and not record.agency_id.invoice_to_agency
and not record.partner_name
):
record.partner_name = _("Reservation from ") + record.agency_id.name
elif not record.partner_name:
record.partner_name = False
@api.depends("line_ids.price_total")
def _compute_total_price_folio(self):
for record in self:
record.total_price_folio = 0
for line in record.line_ids:
record.total_price_folio += line.price_total
record.total_price_folio = record.total_price_folio
@api.depends(
"reference_folio_id",
)
def _compute_line_ids(self):
self.ensure_one()
reference_folio = self.reference_folio_id
if not reference_folio:
self.line_ids = False
return
cmds = [(5, 0)]
for reservation in reference_folio.reservation_ids.filtered(
lambda r: r.state != "cancel"
):
cmds.append(
(
0,
0,
{
"reference_reservation_id": reservation.id,
"booking_duplicate_id": self.id,
"checkin": False,
"checkout": False,
"preferred_room_id": reservation.preferred_room_id.id,
"room_type_id": reservation.room_type_id.id,
"pricelist_id": reservation.pricelist_id.id,
# "arrival_hour": reservation.arrival_hour,
# "departure_hour": reservation.departure_hour,
# "partner_internal_comment": reservation.partner_internal_comment,
"board_service_room_id": reservation.board_service_room_id.id,
"adults": reservation.adults,
},
)
)
self.line_ids = cmds
def create_and_new(self):
self.create_folio()
return {
"name": _("Duplicate Folios"),
"res_model": "pms.booking.duplicate",
"type": "ir.actions.act_window",
"view_id": self.env.ref("pms.booking_duplicate").id,
"target": "new",
"view_mode": "form",
"context": {
"default_reference_folio_id": self.reference_folio_id.id,
"default_created_folio_ids": [(6, 0, self.created_folio_ids.ids)],
"default_start_date": self.start_date,
},
}
def create_and_close(self):
self.create_folio()
folio_ids = self.mapped("created_folio_ids.id")
action = self.env.ref("pms.open_pms_folio1_form_tree_all").read()[0]
if len(folio_ids) > 1:
action["domain"] = [("id", "in", folio_ids)]
elif len(folio_ids) == 1:
form_view = [(self.env.ref("pms.pms_folio_view_form").id, "form")]
if "views" in action:
action["views"] = form_view + [
(state, view) for state, view in action["views"] if view != "form"
]
else:
action["views"] = form_view
action["res_id"] = folio_ids[0]
else:
action = {"type": "ir.actions.act_window_close"}
return action
def view_folios(self):
folio_ids = self.mapped("created_folio_ids.id")
action = self.env.ref("pms.open_pms_folio1_form_tree_all").read()[0]
if len(folio_ids) > 1:
action["domain"] = [("id", "in", folio_ids)]
elif len(folio_ids) == 1:
form_view = [(self.env.ref("pms.pms_folio_view_form").id, "form")]
if "views" in action:
action["views"] = form_view + [
(state, view) for state, view in action["views"] if view != "form"
]
else:
action["views"] = form_view
action["res_id"] = folio_ids[0]
else:
action = {"type": "ir.actions.act_window_close"}
return action
def create_folio(self):
if any(room.occupied_room for room in self.line_ids):
raise UserError(
_(
"""You can not create a new folio because there are rooms already occupied.
Please, check the rooms marked in red and try again."""
)
)
folio = self.env["pms.folio"].create(
{
"reservation_type": self.reservation_type,
"pricelist_id": self.pricelist_id.id,
"partner_id": self.partner_id.id if self.partner_id else False,
"partner_name": self.partner_name,
"pms_property_id": self.pms_property_id.id,
"agency_id": self.agency_id.id,
"channel_type_id": self.channel_type_id.id,
"segmentation_ids": [(6, 0, self.segmentation_ids.ids)],
"internal_comment": self.internal_comment,
}
)
for res in self.line_ids:
displacement_days = (
res.checkin - res.reference_reservation_id.checkin
).days
res_vals = {
"folio_id": folio.id,
"checkin": res.checkin,
"checkout": res.checkout,
"room_type_id": res.room_type_id.id,
"partner_id": self.partner_id.id if self.partner_id else False,
"partner_name": self.partner_name,
"pricelist_id": res.pricelist_id.id,
"pms_property_id": folio.pms_property_id.id,
"board_service_room_id": res.board_service_room_id.id,
"adults": res.adults,
"preferred_room_id": res.preferred_room_id.id,
}
ser_vals = [(5, 0)]
for service in res.reference_reservation_id.service_ids.filtered(
lambda s: not s.is_board_service
):
ser_line_vals = [(5, 0)]
if service.product_id.id in res.service_ids.ids:
for ser_line in service.service_line_ids:
ser_line_vals.append(
(
0,
0,
{
"day_qty": ser_line.day_qty,
"price_unit": ser_line.price_unit,
"discount": ser_line.discount,
"date": ser_line.date
+ datetime.timedelta(days=displacement_days)
if service.per_day
else fields.Date.today(),
},
)
)
ser_vals.append(
(
0,
0,
{
"product_id": service.product_id.id,
"is_board_service": service.is_board_service,
"service_line_ids": ser_line_vals,
},
)
)
res_vals["service_ids"] = ser_vals
if not self.recompute_prices:
line_vals = [(5, 0)]
for line in res.reference_reservation_id.reservation_line_ids:
line_vals.append(
(
0,
0,
{
"price": line.price,
"discount": line.discount,
"room_id": res.preferred_room_id.id,
"date": line.date
+ datetime.timedelta(days=displacement_days),
},
)
)
res_vals["reservation_line_ids"] = line_vals
new_reservation = self.env["pms.reservation"].create(res_vals)
# REVIEW: Board service overwrite prices
for service in new_reservation.service_ids.filtered("is_board_service"):
origin_services_board = (
res.reference_reservation_id.service_ids.filtered(
"is_board_service"
)
)
if origin_services_board:
service.service_line_ids.price_unit = (
origin_services_board.service_line_ids[0].price_unit
)
self.created_folio_ids = [(4, folio.id)]
class PmsReservationDuplicate(models.TransientModel):
_name = "pms.reservation.duplicate"
_description = "Rooms in Duplicate Folio"
_check_pms_properties_auto = True
reference_reservation_id = fields.Many2one(
string="Reservation Reference",
help="Reservation to copy data",
comodel_name="pms.reservation",
check_pms_properties=True,
)
adults = fields.Integer(string="Adults")
booking_duplicate_id = fields.Many2one(
string="Folio Wizard ID",
comodel_name="pms.booking.duplicate",
)
checkin = fields.Date(
string="From:", help="Date Reservation starts ", compute="_compute_checkin"
)
checkout = fields.Date(
string="To:",
help="Date Reservation ends",
compute="_compute_checkout",
)
room_type_id = fields.Many2one(
string="Room Type",
help="Room Type reserved",
comodel_name="pms.room.type",
check_pms_properties=True,
)
preferred_room_id = fields.Many2one(
string="Room",
help="Room reserved",
comodel_name="pms.room",
check_pms_properties=True,
domain="["
"('id', 'in', allowed_room_ids),"
"('id', 'not in', used_room_ids),"
"('pms_property_id', '=', pms_property_id),"
"]",
)
used_room_ids = fields.Many2many(
string="Used Rooms",
comodel_name="pms.room",
compute="_compute_used_room_ids",
)
allowed_room_ids = fields.Many2many(
string="Allowed Rooms",
help="It contains all available rooms for this reservation",
comodel_name="pms.room",
compute="_compute_allowed_room_ids",
)
occupied_room = fields.Boolean(
string="Occupied Room",
help="Check if the room is occupied",
compute="_compute_occupied_room",
)
pricelist_id = fields.Many2one(
string="Pricelist",
help="Pricelist used for this reservation",
comodel_name="product.pricelist",
check_pms_properties=True,
)
price_total = fields.Float(
string="Total price",
help="The total price in the folio",
compute="_compute_price_total",
)
pms_property_id = fields.Many2one(
string="Property",
help="Propertiy with access to the element;",
related="booking_duplicate_id.pms_property_id",
)
board_service_room_id = fields.Many2one(
string="Board Service",
help="Board Service included in the room",
comodel_name="pms.board.service.room.type",
domain="[('pms_room_type_id','=',room_type_id)]",
check_pms_properties=True,
)
service_ids = fields.Many2many(
string="Services",
comodel_name="product.product",
relation="reservation_duplicate_product_rel",
column1="reservation_duplicate_id",
column2="product_id",
compute="_compute_service_ids",
readonly=False,
store=True,
)
@api.depends("booking_duplicate_id.start_date")
def _compute_checkin(self):
self.checkin = False
start_date = self.booking_duplicate_id.start_date
if start_date:
checkin_ref = min(
self.booking_duplicate_id.mapped(
"reference_folio_id.reservation_ids.checkin"
)
)
for record in self:
if record.reference_reservation_id.checkin == checkin_ref:
record.checkin = start_date
else:
dif_days = (
record.reference_reservation_id.checkin - checkin_ref
).days
record.checkin = start_date + datetime.timedelta(days=dif_days)
@api.depends("checkin")
def _compute_checkout(self):
self.checkout = False
for record in self.filtered("checkin"):
res_days = record.reference_reservation_id.nights
record.checkout = record.checkin + datetime.timedelta(days=res_days)
@api.depends(
"checkin",
"checkout",
"preferred_room_id",
"pms_property_id",
)
def _compute_allowed_room_ids(self):
self.allowed_room_ids = False
for reservation in self.filtered(lambda r: r.checkin and r.checkout):
pms_property = reservation.pms_property_id
pms_property = pms_property.with_context(
checkin=reservation.checkin,
checkout=reservation.checkout,
room_type_id=False, # Allows to choose any available room
pricelist_id=reservation.pricelist_id.id,
class_id=reservation.room_type_id.class_id.id
if reservation.room_type_id
else False,
real_avail=True,
)
allowed_room_ids = pms_property.free_room_ids.ids
reservation.allowed_room_ids = self.env["pms.room"].browse(allowed_room_ids)
@api.depends("allowed_room_ids", "preferred_room_id")
def _compute_occupied_room(self):
self.occupied_room = False
for record in self.filtered("preferred_room_id"):
if (
record.preferred_room_id.id not in record.allowed_room_ids.ids
or record.preferred_room_id.id in record.used_room_ids.ids
):
record.occupied_room = True
@api.depends("preferred_room_id", "booking_duplicate_id.used_room_ids")
def _compute_used_room_ids(self):
self.used_room_ids = False
for record in self:
record.used_room_ids = list(
set(record.booking_duplicate_id.used_room_ids.ids)
- {record.preferred_room_id.id}
)
@api.depends("room_type_id", "board_service_room_id", "checkin", "checkout")
def _compute_price_total(self):
self.price_total = 0
for record in self.filtered("checkout"):
record.price_total = record.reference_reservation_id.price_room_services_set
@api.depends("reference_reservation_id")
def _compute_service_ids(self):
for record in self:
record.service_ids = list(
set(record.reference_reservation_id.service_ids.mapped("product_id.id"))
)

View File

@@ -0,0 +1,194 @@
<?xml version="1.0" ?>
<odoo>
<record id="booking_duplicate" model="ir.ui.view">
<field name="name">Duplicate Folio</field>
<field name="model">pms.booking.duplicate</field>
<field name="arch" type="xml">
<form class="pt-1">
<h2>
<field name="reference_folio_id" required="1" readonly="1" />
</h2>
<div class="row">
<div class="col-6 ">
<group>
<field name="pms_property_id" invisible="0" />
<field name="used_room_ids" invisible="1" />
<field name="reservation_type" />
<field
name="agency_id"
attrs="{'invisible': [('reservation_type','!=','normal')]}"
/>
<field
name="segmentation_ids"
widget="many2many_tags"
attrs="{'invisible': [('reservation_type','!=','normal')]}"
/>
</group>
</div>
<div class="col-6">
<group>
<field
name="partner_id"
string="Partner"
options="{'no_create': True,'no_open': True}"
attrs="{'invisible': [('reservation_type','=','out')]}"
/>
<field
name="partner_name"
string="Partner"
required="1"
attrs="{'invisible': [('reservation_type','=','out')]}"
/>
<field
name="partner_name"
string="Reason"
required="1"
attrs="{'invisible': [('reservation_type','!=','out')]}"
/>
<field
default_focus="1"
name="pricelist_id"
string="Pricelist"
options="{'no_create': True,'no_open': True}"
attrs="{'required': [('reservation_type','=','normal')], 'invisible': [('reservation_type','!=','normal')]}"
/>
<field
name="channel_type_id"
attrs="{'required': [('reservation_type','=','normal')], 'invisible': [('reservation_type','!=','normal')]}"
/>
</group>
<div>
<group>
<field
name="internal_comment"
placeholder="Internal comment Folio"
nolabel="1"
/>
</group>
</div>
</div>
</div>
<group>
<field name="start_date" />
<field name="recompute_prices" />
</group>
<div class="row">
<div class="col-12">
<group>
<field name="line_ids" nolabel="1">
<tree
editable="bottom"
create="false"
delete="false"
decoration-muted="checkin == False"
decoration-danger="checkin != False and occupied_room == True"
decoration-success="checkin != False and occupied_room == False"
>
<field
name="reference_reservation_id"
invisible="1"
/>
<field name="booking_duplicate_id" invisible="1" />
<field name="pms_property_id" invisible="1" />
<field name="adults" />
<field name="allowed_room_ids" invisible="1" />
<field
name="room_type_id"
readonly="1"
options="{'no_open': True}"
force_save="1"
/>
<field
name="pricelist_id"
optional="hidden"
options="{'no_create': True, 'no_edit': True, 'no_open': True}"
/>
<field
name="preferred_room_id"
options="{'no_create': True, 'no_edit': True, 'no_open': True}"
/>
<field name="occupied_room" invisible="1" />
<field name="used_room_ids" invisible="1" />
<field name="checkin" force_save="1" />
<field name="checkout" force_save="1" />
<field
name="board_service_room_id"
attrs="{'column_invisible': [('parent.reservation_type','!=','normal')]}"
options="{'no_create': True, 'no_edit': True, 'no_open': True}"
/>
<field
name="service_ids"
widget="many2many_tags"
attrs="{'column_invisible': [('parent.reservation_type','!=','normal')]}"
readonly="1"
/>
<field
name="price_total"
readonly="1"
force_save="1"
attrs="{'column_invisible': [('parent.reservation_type','!=','normal')]}"
/>
</tree>
</field>
</group>
</div>
</div>
<div class="row float-right border mr-2 mb-5">
<div class="col-3 ">
<div class="col-3 px-0">
<group>
<field
name="total_price_folio"
widget="monetary"
attrs="{'invisible': [('reservation_type','!=','normal')]}"
/>
</group>
</div>
</div>
</div>
<div>
<field name="created_folio_ids" readonly="1" />
</div>
<footer>
<button
name="create_and_new"
string="Create and Continue"
type="object"
class="btn-secondary"
/>
<button
name="create_and_close"
string="Create and Close"
type="object"
class="btn-primary"
/>
<span>
or
</span>
<button
string="Cancel"
class="btn-default border"
special="cancel"
/>
<button
name="view_folios"
string="View Folios"
type="object"
class="btn-primary"
attrs="{'invisible': [('created_folio_ids','=', False)]}"
/>
</footer>
</form>
</field>
</record>
<record id="action_booking_duplicate" model="ir.actions.act_window">
<field name="name">Folio creation</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pms.booking.duplicate</field>
<field name="view_id" ref="booking_duplicate" />
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@@ -1,4 +1,6 @@
from odoo import _, api, fields, models
from datetime import timedelta
from odoo import api, fields, models
class WizardFolioChanges(models.TransientModel):
@@ -11,6 +13,40 @@ class WizardFolioChanges(models.TransientModel):
default=lambda self: self._default_folio_id(),
comodel_name="pms.folio",
)
modification_type = fields.Selection(
string="Modification Type",
selection=[
("reservations", "Reservations"),
("dates", "Dates"),
("services", "Services Prices"),
],
default="reservations",
)
room_type_filter_ids = fields.Many2many(
string="Room types",
default=lambda self: self._default_room_type_filter_ids(),
comodel_name="pms.room.type",
relation="folio_changes_room_type_rel",
column1="folio_changes_id",
column2="room_type_ids",
domain="[('id', 'in', allowed_room_type_ids)]",
)
allowed_room_type_ids = fields.Many2many(
string="Allowed Room Types",
comodel_name="pms.room.type",
relation="folio_changes_allowed_room_type_rel",
column1="folio_changes_id",
column2="allowed_room_type_ids",
compute="_compute_allowed_room_type_ids",
)
change_from_date = fields.Date(
string="Apply From",
default=lambda self: self.default_change_from_date(),
)
change_to_date = fields.Date(
string="Apply To",
default=lambda self: self.default_change_to_date(),
)
reservation_ids = fields.Many2many(
string="Reservations",
default=lambda self: self._default_reservation_ids(),
@@ -26,18 +62,114 @@ class WizardFolioChanges(models.TransientModel):
relation="folio_changes_allowed_reservation_rel",
column1="folio_changes_id",
column2="allowed_reservation_ids",
compute="_compute_allowed_reservations",
compute="_compute_allowed_reservation_ids",
)
service_ids = fields.Many2many(
string="Services",
default=lambda self: self._default_service_ids(),
comodel_name="pms.service",
relation="folio_changes_service_rel",
column1="folio_changes_id",
column2="service_ids",
domain="[('id', 'in', allowed_service_ids)]",
)
allowed_service_ids = fields.Many2many(
string="Allowed Services",
comodel_name="pms.service",
relation="folio_changes_allowed_service_rel",
column1="folio_changes_id",
column2="allowed_service_ids",
compute="_compute_allowed_service_ids",
)
apply_new_checkin = fields.Boolean(
string="Apply Checkin Update",
default=False,
)
new_checkin = fields.Date(
string="New Checkin",
default=lambda self: self.default_change_new_checkin(),
)
apply_new_checkout = fields.Boolean(
string="Apply Checkout Update",
default=False,
)
new_checkout = fields.Date(
string="New Checkout",
default=lambda self: self.default_change_new_checkout(),
)
nights = fields.Integer(
string="Nights",
compute="_compute_nights",
)
dates_incongruence = fields.Boolean(
string="Dates incrongruence",
help="Indicates that there are reservations with different checkin and/or checkout",
compute="_compute_dates_incongruence",
store=True,
)
apply_price = fields.Boolean(
string="Apply Price update",
default=False,
)
new_price = fields.Float(
string="New Price",
)
apply_discount = fields.Boolean(
string="Apply Discount update",
default=False,
)
new_discount = fields.Float(
string="New Discount %",
)
apply_partner_id = fields.Boolean(
string="Apply Customer",
default=False,
)
new_partner_id = fields.Many2one(
string="Customer",
comodel_name="res.partner",
)
apply_pricelist_id = fields.Boolean(
string="Apply Pricelist",
default=False,
)
new_pricelist_id = fields.Many2one(
string="Pricelist",
comodel_name="product.pricelist",
)
apply_board_service = fields.Boolean(
string="Add Board Service to reservations",
default=False,
)
new_board_service_id = fields.Many2one(
string="New Board Service",
comodel_name="pms.board.service",
)
apply_service = fields.Boolean(
string="Add Service to reservations",
default=False,
)
new_service_id = fields.Many2one(
string="New Service",
comodel_name="product.product",
domain="[('sale_ok','=',True)]",
)
apply_day_qty = fields.Boolean(
string="Change cuantity service per day",
help="If not set, it will use the default product day qty",
default=False,
)
day_qty = fields.Integer(
string="Quantity per day",
)
apply_on_monday = fields.Boolean(
string="Apply Availability Rule on mondays",
default=False,
@@ -81,13 +213,86 @@ class WizardFolioChanges(models.TransientModel):
folio = self.env["pms.folio"].browse(folio_id)
return folio.reservation_ids
def _default_room_type_filter_ids(self):
folio_id = self._context.get("active_id")
folio = self.env["pms.folio"].browse(folio_id)
return self.env["pms.room.type"].browse(
folio.mapped("reservation_ids.room_type_id.id")
)
def default_change_new_checkin(self):
folio_id = self._context.get("active_id")
folio = self.env["pms.folio"].browse(folio_id)
return min(folio.reservation_ids.mapped("checkin"), default=False)
def default_change_new_checkout(self):
folio_id = self._context.get("active_id")
folio = self.env["pms.folio"].browse(folio_id)
return max(folio.reservation_ids.mapped("checkout"), default=False)
def _default_service_ids(self):
folio_id = self._context.get("active_id")
folio = self.env["pms.folio"].browse(folio_id)
return folio.service_ids
def default_change_from_date(self):
folio_id = self._context.get("active_id")
folio = self.env["pms.folio"].browse(folio_id)
return min(folio.reservation_ids.mapped("checkin"), default=False)
def default_change_to_date(self):
folio_id = self._context.get("active_id")
folio = self.env["pms.folio"].browse(folio_id)
return max(folio.reservation_ids.mapped("checkout"), default=False)
@api.depends("new_checkin", "new_checkout")
def _compute_nights(self):
for record in self:
record.nights = (record.new_checkout - record.new_checkin).days
@api.depends("reservation_ids")
def _compute_dates_incongruence(self):
self.dates_incongruence = False
for record in self:
if (
len(set(record.reservation_ids.mapped("checkin"))) > 1
or len(set(record.reservation_ids.mapped("checkout"))) > 1
):
record.dates_incongruence = True
@api.depends("folio_id")
def _compute_allowed_reservations(self):
def _compute_allowed_reservation_ids(self):
self.ensure_one()
self.allowed_reservation_ids = self.folio_id.reservation_ids
@api.depends("folio_id")
def _compute_allowed_service_ids(self):
self.ensure_one()
self.allowed_service_ids = self.folio_id.service_ids
@api.depends("folio_id")
def _compute_allowed_room_type_ids(self):
self.ensure_one()
self.allowed_room_type_ids = self.env["pms.room.type"].browse(
self.folio_id.mapped("reservation_ids.room_type_id.id")
)
@api.onchange("room_type_filter_ids")
def _onchange_room_type_filter_ids(self):
self.service_ids = self.folio_id.service_ids.filtered(
lambda s: s.reservation_id
and s.reservation_id.room_type_id.id in self.room_type_filter_ids.ids
)
self.reservation_ids = self.folio_id.reservation_ids.filtered(
lambda r: r.room_type_id.id in self.room_type_filter_ids.ids
)
@api.onchange("reservation_ids")
def _onchange_reservations_ids(self):
self.new_checkin = min(self.reservation_ids.mapped("checkin"), default=False)
self.new_checkout = max(self.reservation_ids.mapped("checkout"), default=False)
def button_change(self):
vals = {}
week_days_to_apply = (
self.apply_on_monday,
self.apply_on_tuesday,
@@ -97,52 +302,124 @@ class WizardFolioChanges(models.TransientModel):
self.apply_on_saturday,
self.apply_on_sunday,
)
reservation_lines = self.reservation_ids.reservation_line_ids
if not self.apply_on_all_week:
reservation_lines = reservation_lines.filtered(
lambda x: week_days_to_apply[x.date.timetuple()[6]]
if self.modification_type == "dates":
self._update_dates(
reservations=self.reservation_ids,
new_checkin=self.new_checkin,
new_checkout=self.new_checkout,
)
if self.new_price or self.new_discount:
if self.new_price:
vals["price"] = self.new_price
if self.new_discount:
vals["discount"] = self.new_discount
reservation_lines.write(vals)
self.folio_id.message_post(
body=_(
"Prices/Discounts have been changed from folio",
)
)
reservations = self.env["pms.reservation"].browse(
reservation_lines.mapped("reservation_id.id")
)
for reservation in reservations:
reservation.message_post(
body=_(
"Prices/Discounts have been changed from folio",
else:
dates = [
self.change_from_date + timedelta(days=d)
for d in range((self.change_to_date - self.change_from_date).days + 1)
]
if self.modification_type == "reservations":
reservation_lines = self.reservation_ids.reservation_line_ids
if not self.apply_on_all_week:
reservation_lines = reservation_lines.filtered(
lambda x: week_days_to_apply[x.date.timetuple()[6]]
and x.date in dates
)
if self.apply_discount or self.apply_price:
self._update_reservations(
reservation_lines=reservation_lines,
new_price=self.apply_price and self.new_price,
new_discount=self.apply_discount and self.new_discount,
)
if self.apply_board_service and self.new_board_service_id:
self._add_board_service(
reservations=self.reservation_ids,
new_board_service_id=self.new_board_service_id.id,
)
if self.apply_service and self.new_service_id:
self._add_service(
reservations=self.reservation_ids,
new_service_id=self.new_service_id.id,
day_qty=self.day_qty if self.apply_day_qty else -1,
)
if self.apply_pricelist_id and self.new_pricelist_id:
self.reservation_ids.pricelist_id = self.new_pricelist_id
self.folio_id.pricelist_id = self.new_pricelist_id
if self.apply_partner_id and self.new_partner_id:
self.reservation_ids.partner_id = self.new_partner_id
if not self.folio_id.partner_id:
self.folio_id.partner_id = self.new_partner_id
elif self.modification_type == "services":
service_lines = self.service_ids.service_line_ids
if not self.apply_on_all_week:
reservation_lines = service_lines.filtered(
lambda x: week_days_to_apply[x.date.timetuple()[6]]
and x.date in dates
)
self._update_services(
service_lines=service_lines,
new_price=self.apply_price and self.new_price,
new_discount=self.apply_discount and self.new_discount,
)
if self.new_board_service_id:
for reservation in self.reservation_ids:
if (
self.new_board_service_id.id
in reservation.room_type_id.board_service_room_type_ids.ids
):
reservation.board_service_room_id = (
reservation.room_type_id.board_service_room_type_ids.filtered(
lambda x: x.pms_board_service_id.id
== self.new_board_service_id.id
and (
self.folio_id.pms_property_id.id
in x.pms_property_ids.ids
or not x.pms_property_ids
)
def _update_dates(self, reservations, new_checkin, new_checkout):
for res in reservations:
if new_checkin:
res.checkin = new_checkin
if new_checkout:
res.checkout = new_checkout
def _update_reservations(
self, reservation_lines, new_price=False, new_discount=False
):
line_vals = {}
if new_price:
line_vals["price"] = new_price
if new_discount:
line_vals["discount"] = new_discount
if line_vals:
reservation_lines.write(line_vals)
def _add_board_service(self, reservations, new_board_service_id):
for reservation in reservations:
if new_board_service_id in reservation.room_type_id.mapped(
"board_service_room_type_ids.pms_board_service_id.id"
):
reservation.board_service_room_id = (
reservation.room_type_id.board_service_room_type_ids.filtered(
lambda x: x.pms_board_service_id.id == new_board_service_id
and (
reservation.folio_id.pms_property_id.id
in x.pms_property_ids.ids
or not x.pms_property_ids
)
)
reservation.message_post(
body=_(
"Board service has been changed from folio",
)
)
def _add_service(self, reservations, new_service_id, day_qty):
old_services = reservations.service_ids
reservations.write(
{
"service_ids": [
(
0,
0,
{
"product_id": new_service_id,
},
)
]
}
)
new_services = reservations.service_ids - old_services
# Use -1 to set default qty qty per day
if day_qty > -1:
new_services.day_qty = day_qty
def _update_services(
self, service_lines, new_price=False, new_discount=False, new_day_qty=False
):
line_vals = {}
if new_price:
line_vals["price_unit"] = new_price
if new_discount:
line_vals["discount"] = new_discount
if new_day_qty:
line_vals["day_qty"] = new_day_qty
if line_vals:
service_lines.write(line_vals)

View File

@@ -4,105 +4,355 @@
<field name="name">wizard.folio.changes.view.form</field>
<field name="model">wizard.folio.changes</field>
<field name="arch" type="xml">
<form string="Folio Changes">
<div class="row">
<div class="col-12">
<table class="table table-bordered text-center">
<thead>
<tr>
<th>All days</th>
<th>Sunday</th>
<th>Monday</th>
<th>Tuesday</th>
<th>Wednesday</th>
<th>Thursday</th>
<th>Friday</th>
<th>Saturday</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<field
name="apply_on_all_week"
widget="boolean_toggle"
/>
</td>
<td>
<field
name="apply_on_sunday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_monday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_tuesday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_wednesday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_thursday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_friday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_saturday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<form string="Folio Changes" class="pt-1">
<group>
<field name="modification_type" />
</group>
<group attrs="{'invisible':[('modification_type', '=', 'dates')]}">
<field name="change_from_date" />
<field name="change_to_date" />
</group>
</tr>
</tbody>
</table>
<div
class="col-12"
attrs="{'invisible':[('modification_type', '=', 'dates')]}"
>
<table class="table table-bordered text-center">
<thead>
<tr>
<th>All days</th>
<th>Sunday</th>
<th>Monday</th>
<th>Tuesday</th>
<th>Wednesday</th>
<th>Thursday</th>
<th>Friday</th>
<th>Saturday</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<field
name="apply_on_all_week"
widget="boolean_toggle"
/>
</td>
<td>
<field
name="apply_on_sunday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_monday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_tuesday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_wednesday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_thursday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_friday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
<td>
<field
name="apply_on_saturday"
widget="boolean_toggle"
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
/>
</td>
</tr>
</tbody>
</table>
</div>
<group>
<field name="folio_id" invisible="1" />
<field name="allowed_reservation_ids" invisible="1" />
<field name="allowed_service_ids" invisible="1" />
<field name="allowed_room_type_ids" invisible="1" />
<field name="room_type_filter_ids" widget="many2many_tags" />
<field
name="reservation_ids"
attrs="{'invisible':[('modification_type','=','services')]}"
widget="many2many_tags"
/>
<field
name="service_ids"
attrs="{'invisible':[('modification_type','!=','services')]}"
widget="many2many_tags"
/>
</group>
<field name="dates_incongruence" invisible="1" force_save="1" />
<div
class="alert alert-warning"
role="alert"
attrs="{'invisible': [('dates_incongruence','=',False)]}"
>
Selected reservations with different dates
</div>
<div
class="row"
attrs="{'invisible':[('modification_type', '!=', 'dates')]}"
>
<div class="col-4 pr-0">
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-2">
<field
name="apply_new_checkin"
widget="boolean_toggle"
/>
</div>
<div class="col-6">
<label for="new_checkin" />
</div>
<div class="col-4">
<field
name="new_checkin"
nolabel="1"
attrs="{'invisible':[('apply_new_checkin','=',False)], 'required':[('apply_new_checkin','=',True)]}"
/>
</div>
</div>
</div>
</div>
<div class="col-4 pr-0">
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-2">
<field
name="apply_new_checkout"
widget="boolean_toggle"
/>
</div>
<div class="col-6">
<label for="new_checkout" />
</div>
<div class="col-4">
<field
name="new_checkout"
nolabel="1"
attrs="{'invisible':[('apply_new_checkout','=',False)], 'required':[('apply_new_checkout','=',True)]}"
/>
</div>
</div>
</div>
</div>
<div class="col-4 pr-0">
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-6">
<label for="nights" />
</div>
<div class="col-4">
<field name="nights" nolabel="1" />
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-5 ">
<group>
<field name="folio_id" invisible="1" />
<field name="allowed_reservation_ids" invisible="1" />
<field
name="reservation_ids"
widget="many2many_tags"
nolabel="1"
/>
</group>
<div
class="row"
attrs="{'invisible':[('modification_type','=','dates')]}"
>
<div class="col-4 pr-0">
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-2">
<field name="apply_price" widget="boolean_toggle" />
</div>
<div class="col-6">
<label for="new_price" />
</div>
<div class="col-4">
<field
name="new_price"
nolabel="1"
attrs="{'invisible':[('apply_price','=',False)], 'required':[('apply_price','=',True)]}"
/>
</div>
</div>
</div>
</div>
<div class="col-4">
<group>
<field name="new_price" />
<field name="new_discount" />
<field name="new_board_service_id" />
</group>
<div class="col-4 pr-0">
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-2">
<field
name="apply_discount"
widget="boolean_toggle"
/>
</div>
<div class="col-6">
<label for="new_discount" />
</div>
<div class="col-4">
<field
name="new_discount"
nolabel="1"
attrs="{'invisible':[('apply_discount','=',False)], 'required':[('apply_discount','=',True)]}"
/>
</div>
</div>
</div>
</div>
<div
class="col-4 pr-0"
attrs="{'invisible':[('modification_type','!=','reservations')]}"
>
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-2">
<field
name="apply_board_service"
widget="boolean_toggle"
/>
</div>
<div class="col-6">
<label for="new_board_service_id" />
</div>
<div class="col-4">
<field
name="new_board_service_id"
nolabel="1"
attrs="{'invisible':[('apply_board_service','=',False)], 'required':[('apply_board_service','=',True)]}"
/>
</div>
</div>
</div>
</div>
<div
class="col-4 pr-0"
attrs="{'invisible':[('modification_type','!=','reservations')]}"
>
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-2">
<field
name="apply_partner_id"
widget="boolean_toggle"
/>
</div>
<div class="col-6">
<label for="new_partner_id" />
</div>
<div class="col-4">
<field
name="new_partner_id"
nolabel="1"
attrs="{'invisible':[('apply_partner_id','=',False)], 'required':[('apply_partner_id','=',True)]}"
/>
</div>
</div>
</div>
</div>
<div
class="col-4 pr-0"
attrs="{'invisible':[('modification_type','!=','reservations')]}"
>
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-2">
<field
name="apply_pricelist_id"
widget="boolean_toggle"
/>
</div>
<div class="col-6">
<label for="new_pricelist_id" />
</div>
<div class="col-4">
<field
name="new_pricelist_id"
nolabel="1"
attrs="{'invisible':[('apply_pricelist_id','=',False)], 'required':[('apply_pricelist_id','=',True)]}"
/>
</div>
</div>
</div>
</div>
<div class="col-4 pr-0">
<div
class="border h-100 pt-2 px-2"
attrs="{'invisible':[('modification_type','!=','reservations')]}"
>
<div class="row">
<div class="col-2">
<field
name="apply_service"
widget="boolean_toggle"
/>
</div>
<div class="col-6">
<label for="new_service_id" />
</div>
<div class="col-4">
<field
name="new_service_id"
nolabel="1"
attrs="{'invisible':[('apply_service','=',False)], 'required':[('apply_service','=',True)]}"
/>
</div>
</div>
</div>
</div>
<div
class="col-4 pr-0"
attrs="{'invisible':[('modification_type','!=','services')]}"
>
<div class="border h-100 pt-2 px-2">
<div class="row">
<div class="col-2">
<field
name="apply_day_qty"
widget="boolean_toggle"
/>
</div>
<div class="col-6">
<label for="day_qty" />
</div>
<div class="col-4">
<field
name="day_qty"
nolabel="1"
attrs="{'invisible':[('apply_day_qty','=',False)], 'required':[('apply_day_qty','=',True)]}"
/>
</div>
</div>
</div>
</div>
</div>
<footer>
<button
type="object"

View File

@@ -37,6 +37,9 @@
"views/pms_room_views.xml",
"views/pms_log_institution_traveller_report_views.xml",
"views/pms_ine_tourism_type_category.xml",
"views/res_partner_id_number_view.xml",
"views/pms_checkin_partner_views.xml",
"views/precheckin_portal_templates.xml",
"wizards/traveller_report.xml",
"wizards/wizard_ine.xml",
],

View File

@@ -4,6 +4,7 @@
<record id="document_type_dni" model="res.partner.id_category">
<field name="name">DNI</field>
<field name="code">D</field>
<field name="is_vat_equivalent">True</field>
<field name="validation_code">
letters = {
0: "T",

View File

@@ -7,3 +7,4 @@ from . import pms_ine_tourism_type_category
from . import pms_room
from . import res_partner
from . import pms_checkin_partner
from . import res_partner_id_number

View File

@@ -1,6 +1,6 @@
import logging
from odoo import api, models
from odoo import api, fields, models
CODE_SPAIN = "ES"
@@ -10,6 +10,29 @@ _logger = logging.getLogger(__name__)
class PmsCheckinParnert(models.Model):
_inherit = "pms.checkin.partner"
support_number = fields.Char(
string="Support number",
help="ID support number",
readonly=False,
store=True,
compute="_compute_support_number",
)
@api.depends("partner_id")
def _compute_support_number(self):
for record in self:
if not record.support_number:
if record.partner_id.id_numbers:
dni_numbers = record.partner_id.id_numbers.filtered(
lambda x: x.category_id.name == "DNI"
)
if len(dni_numbers) == 1 and dni_numbers.support_number:
record.support_number = dni_numbers.support_number
else:
record.support_number = False
else:
record.support_number = False
@api.model
def _checkin_mandatory_fields(self, country=False, depends=False):
mandatory_fields = super(PmsCheckinParnert, self)._checkin_mandatory_fields(
@@ -26,5 +49,5 @@ class PmsCheckinParnert(models.Model):
]
)
if depends or (country and country.code == CODE_SPAIN):
mandatory_fields.append("state_id")
mandatory_fields.append("residence_state_id")
return mandatory_fields

View File

@@ -16,7 +16,7 @@ class ResPartner(models.Model):
store=True,
)
@api.depends("nationality_id", "state_id")
@api.depends("nationality_id", "residence_state_id")
def _compute_ine_code(self):
for record in self:
if not record.nationality_id:
@@ -24,6 +24,16 @@ class ResPartner(models.Model):
elif record.nationality_id.code != CODE_SPAIN:
record.ine_code = record.nationality_id.code_alpha3
else:
if not record.state_id:
if not record.residence_state_id:
record.ine_code = False
record.ine_code = record.state_id.ine_code
record.ine_code = record.residence_state_id.ine_code
def _check_enought_invoice_data(self):
self.ensure_one()
res = super(ResPartner, self)._check_enought_invoice_data()
if not res:
return res
if self.country_id.code == "ES":
if not self.state_id and not self.zip:
return False
return True

View File

@@ -0,0 +1,8 @@
from odoo import fields, models
class ResPartnerIdNumber(models.Model):
_inherit = "res.partner.id_number"
_description = "Partner ID Number"
support_number = fields.Char(string="Support number", help="DNI support number")

View File

@@ -46,9 +46,9 @@ class TestResPartner(TestPms):
self.partner_1 = self.env["res.partner"].create(
{
"name": "partner1",
"country_id": country_spain.id,
"residence_country_id": country_spain.id,
"nationality_id": country_spain.id,
"state_id": state_madrid.id,
"residence_state_id": state_madrid.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -56,7 +56,7 @@ class TestResPartner(TestPms):
# ASSERT
self.assertEqual(
self.partner_1.ine_code,
self.partner_1.state_id.ine_code,
self.partner_1.residence_state_id.ine_code,
"The ine code for Spanish partners must match the ine"
" code of the state to which they belong",
)

View File

@@ -97,6 +97,7 @@ class TestWizardINE(TestPms):
"name": "partner1",
"country_id": self.country_italy.id,
"nationality_id": self.country_italy.id,
"residence_country_id": self.country_italy.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -116,6 +117,7 @@ class TestWizardINE(TestPms):
"name": "partner2",
"country_id": self.country_russia.id,
"nationality_id": self.country_russia.id,
"residence_country_id": self.country_russia.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -134,6 +136,7 @@ class TestWizardINE(TestPms):
"name": "partner3",
"country_id": self.country_italy.id,
"nationality_id": self.country_italy.id,
"residence_country_id": self.country_italy.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -152,6 +155,7 @@ class TestWizardINE(TestPms):
"name": "partner4",
"country_id": self.country_italy.id,
"nationality_id": self.country_italy.id,
"residence_country_id": self.country_italy.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -170,6 +174,7 @@ class TestWizardINE(TestPms):
"name": "partner5",
"country_id": self.country_afghanistan.id,
"nationality_id": self.country_afghanistan.id,
"residence_country_id": self.country_afghanistan.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -188,6 +193,7 @@ class TestWizardINE(TestPms):
"name": "partner6",
"country_id": self.country_afghanistan.id,
"nationality_id": self.country_afghanistan.id,
"residence_country_id": self.country_afghanistan.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -206,6 +212,7 @@ class TestWizardINE(TestPms):
"name": "partner7",
"country_id": self.country_afghanistan.id,
"nationality_id": self.country_afghanistan.id,
"residence_country_id": self.country_afghanistan.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -364,6 +371,7 @@ class TestWizardINE(TestPms):
"name": "partner1",
"country_id": self.country_russia.id,
"nationality_id": self.country_russia.id,
"residence_country_id": self.country_russia.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -373,6 +381,7 @@ class TestWizardINE(TestPms):
"name": "partner2",
"country_id": self.country_russia.id,
"nationality_id": self.country_russia.id,
"residence_country_id": self.country_russia.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -382,6 +391,7 @@ class TestWizardINE(TestPms):
"name": "partner3",
"country_id": self.country_russia.id,
"nationality_id": self.country_russia.id,
"residence_country_id": self.country_russia.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -646,38 +656,38 @@ class TestWizardINE(TestPms):
self.checkin1.nationality_id = country_spain
self.partner_1.nationality_id = country_spain
self.checkin1.state_id = state_ourense
self.partner_1.state_id = state_ourense
self.checkin1.residence_state_id = state_ourense
self.partner_1.residence_state_id = state_ourense
self.checkin2.nationality_id = country_spain
self.partner_2.nationality_id = country_spain
self.checkin2.state_id = state_pontevedra
self.partner_2.state_id = state_pontevedra
self.checkin2.residence_state_id = state_pontevedra
self.partner_2.residence_state_id = state_pontevedra
self.checkin3.nationality_id = country_spain
self.partner_3.nationality_id = country_spain
self.checkin3.state_id = state_ourense
self.partner_3.state_id = state_ourense
self.checkin3.residence_state_id = state_ourense
self.partner_3.residence_state_id = state_ourense
self.checkin4.nationality_id = country_spain
self.partner_4.nationality_id = country_spain
self.checkin4.state_id = state_ourense
self.partner_4.state_id = state_ourense
self.checkin4.residence_state_id = state_ourense
self.partner_4.residence_state_id = state_ourense
self.checkin5.nationality_id = country_spain
self.partner_5.nationality_id = country_spain
self.checkin5.state_id = state_madrid
self.partner_5.state_id = state_madrid
self.checkin5.residence_state_id = state_madrid
self.partner_5.residence_state_id = state_madrid
self.checkin6.nationality_id = country_spain
self.partner_6.nationality_id = country_spain
self.checkin6.state_id = state_madrid
self.partner_6.state_id = state_madrid
self.checkin6.residence_state_id = state_madrid
self.partner_6.residence_state_id = state_madrid
self.checkin7.nationality_id = country_spain
self.partner_7.nationality_id = country_spain
self.checkin7.state_id = state_madrid
self.partner_7.state_id = state_madrid
self.checkin7.residence_state_id = state_madrid
self.partner_7.residence_state_id = state_madrid
expected_result = {
country_spain.code: {
@@ -945,38 +955,38 @@ class TestWizardINE(TestPms):
self.checkin1.nationality_id = country_spain
self.partner_1.nationality_id = country_spain
self.checkin1.state_id = state_ourense
self.partner_1.state_id = state_ourense
self.checkin1.residence_state_id = state_ourense
self.partner_1.residence_state_id = state_ourense
self.checkin2.nationality_id = country_spain
self.partner_2.nationality_id = country_spain
self.checkin2.state_id = False
self.partner_2.state_id = False
self.checkin2.residence_state_id = False
self.partner_2.residence_state_id = False
self.checkin3.nationality_id = country_spain
self.partner_3.nationality_id = country_spain
self.checkin3.state_id = state_ourense
self.partner_3.state_id = state_ourense
self.checkin3.residence_state_id = state_ourense
self.partner_3.residence_state_id = state_ourense
self.checkin4.nationality_id = country_spain
self.partner_4.nationality_id = country_spain
self.checkin4.state_id = state_ourense
self.partner_4.state_id = state_ourense
self.checkin4.residence_state_id = state_ourense
self.partner_4.residence_state_id = state_ourense
self.checkin5.nationality_id = country_spain
self.partner_5.nationality_id = country_spain
self.checkin5.state_id = state_madrid
self.partner_5.state_id = state_madrid
self.checkin5.residence_state_id = state_madrid
self.partner_5.residence_state_id = state_madrid
self.checkin6.nationality_id = country_spain
self.partner_6.nationality_id = country_spain
self.checkin6.state_id = state_madrid
self.partner_6.state_id = state_madrid
self.checkin6.residence_state_id = state_madrid
self.partner_6.residence_state_id = state_madrid
self.checkin7.nationality_id = country_spain
self.partner_7.nationality_id = country_spain
self.checkin7.state_id = state_madrid
self.partner_7.state_id = state_madrid
self.checkin7.residence_state_id = state_madrid
self.partner_7.residence_state_id = state_madrid
# ACT & ASSERT
with self.assertRaises(

View File

@@ -56,6 +56,7 @@ class TestWizardTravellerReport(TestPms):
"name": "partner1",
"country_id": self.country_italy.id,
"nationality_id": self.country_italy.id,
"residence_country_id": self.country_italy.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -75,6 +76,7 @@ class TestWizardTravellerReport(TestPms):
"name": "partner2",
"country_id": self.country_italy.id,
"nationality_id": self.country_italy.id,
"residence_country_id": self.country_italy.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
@@ -175,6 +177,7 @@ class TestWizardTravellerReport(TestPms):
"reservation_id": self.reservation_1.id,
"firstname": "John",
"lastname": "Doe",
"nationality_id": self.country_italy.id,
}
)
# Create reservation 2

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="inherit_pms_checkin_partner_form" model="ir.ui.view">
<field name="name">inherit.pms.checkin_partner.form</field>
<field name="model">pms.checkin.partner</field>
<field name="inherit_id" ref="pms.pms_checkin_partner_view_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='document_expedition_date']" position="after">
<field name="support_number" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,79 @@
<odoo>
<template
id="inherit_pms_l10n_es_portal_my_precheckin_detail"
inherit_id="pms.portal_my_precheckin_detail"
>
<xpath expr="//div[@name='document_type_div']" position="replace">
<div t-attf-class="form-group col-md-4">
<label class="col-form-label" for="document_type">Doc. Type</label>
<div class="d-none"><p id="docTypeId"><t
t-esc="checkin_partner_id.document_type"
/></p></div>
<select
class="form-control #{error.get('document_type') and 'is-invalid' or ''}"
name='document_type'
id="doc_type"
>
<option value="">Select an option</option>
<t t-foreach="doc_type_ids" t-as='doc_type'>
<option
t-att-value="doc_type.name"
t-att-selected="doc_type.id == checkin_partner_id.document_type.id"
>
<t t-esc='doc_type.name' />
</option>
</t>
</select>
<t t-if="error_message">
<span
class="text-danger"
t-esc="error_message.get('document_type')"
/>
</t>
</div>
</xpath>
<xpath expr="//div[@name='document_number_div']" position="replace">
<div t-attf-class="col-12 col-md-4">
<label class="col-form-label" for="document_number">Doc. Number</label>
<input
type="text"
name="document_number"
t-attf-class="form-control #{error.get('document_number') and 'is-invalid' or ''}"
t-att-value="doc_number or checkin_partner_id.document_number"
/>
<t t-if="error_message">
<span
class="text-danger"
t-esc="error_message.get('document_number')"
/>
</t>
</div>
<div t-attf-class="form-group col-md-4">
<label
class="col-form-label"
for="support_number"
>Doc. Support Number</label>
<input
type="text"
name="support_number"
t-attf-class="form-control"
t-att-value="support_number or checkin_partner_id.support_number"
/>
</div>
</xpath>
</template>
<template
id="inherit_pms_l10n_es_portal_my_precheckin_end"
inherit_id="pms.portal_my_precheckin_end"
>
<xpath expr="//div[@name='birthdate_div']" position="before">
<div t-attf-class="form-group col-12 col-md-6 font-weight-bold">
Doc. Support Number:
<br />
<span class="font-weight-normal ml-3">
<t t-esc="checkin_partner.support_number" />
</span>
</div>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="inherit_partner_id_numbers_form" model="ir.ui.view">
<field name="name">inherit.res.partner.id_number.form</field>
<field name="model">res.partner.id_number</field>
<field
name="inherit_id"
ref="partner_identification.view_partner_id_numbers_form"
/>
<field name="arch" type="xml">
<xpath expr="//field[@name='valid_until']" position="after">
<field name="support_number" />
</xpath>
</field>
</record>
<record id="inherit_partner_id_numbers_tree" model="ir.ui.view">
<field name="name">inherit.res.partner.id_number.tree</field>
<field name="model">res.partner.id_number</field>
<field
name="inherit_id"
ref="partner_identification.view_partner_id_numbers_tree"
/>
<field name="arch" type="xml">
<xpath expr="//field[@name='valid_until']" position="after">
<field name="support_number" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -208,7 +208,7 @@ class WizardIne(models.TransientModel):
)
raise ValidationError(
_(
"The following guests have no nationality set :%s.",
"The following guests have no residence nationality set :%s.",
guests_with_no_nationality,
)
)
@@ -234,13 +234,13 @@ class WizardIne(models.TransientModel):
# arrivals grouped by state_id (Spain "provincias")
read_by_arrivals_spain = self.env["res.partner"].read_group(
entry["__domain"],
["state_id"],
["state_id"],
["residence_state_id"],
["residence_state_id"],
lazy=False,
)
# iterate read_group results from Spain
for entry_from_spain in read_by_arrivals_spain:
if not entry_from_spain["state_id"]:
if not entry_from_spain["residence_state_id"]:
spanish_guests_with_no_state = self.env[
"res.partner"
].search(entry_from_spain["__domain"])
@@ -255,10 +255,10 @@ class WizardIne(models.TransientModel):
spanish_guests_with_no_state,
)
)
state_id = self.env["res.country.state"].browse(
entry_from_spain["state_id"][0]
residence_state_id = self.env["res.country.state"].browse(
entry_from_spain["residence_state_id"][0]
) # .ine_code
ine_code = state_id.ine_code
ine_code = residence_state_id.ine_code
# get count of each result
num_spain = entry_from_spain["__count"]
@@ -317,18 +317,18 @@ class WizardIne(models.TransientModel):
# if there are some checkin partners in the same reservation
if chk_part_same_reserv_with_checkin:
# create partner with same country & state
country_other = (
chk_part_same_reserv_with_checkin.partner_id.country_id.id
)
state_other = (
chk_part_same_reserv_with_checkin.partner_id.state_id.id
)
country_other = chk_part_same_reserv_with_checkin[
0
].partner_id.nationality_id.id
state_other = chk_part_same_reserv_with_checkin[
0
].partner_id.residence_state_id.id
dummy_partner = self.env["res.partner"].create(
{
"name": "partner1",
"country_id": country_other,
"nationality_id": country_other,
"state_id": state_other,
"residence_state_id": state_other,
}
)
@@ -339,7 +339,7 @@ class WizardIne(models.TransientModel):
"name": "partner1",
"country_id": country_spain.id,
"nationality_id": country_spain.id,
"state_id": state_madrid.id,
"residence_state_id": state_madrid.id,
}
)
fake_partners_ids.append(dummy_partner.id)