diff --git a/pms/__manifest__.py b/pms/__manifest__.py index 4722890c1..ce1b011e1 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -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", ], diff --git a/pms/controllers/pms_portal.py b/pms/controllers/pms_portal.py index 507c12858..bff115e58 100644 --- a/pms/controllers/pms_portal.py +++ b/pms/controllers/pms_portal.py @@ -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 diff --git a/pms/data/cron_jobs.xml b/pms/data/cron_jobs.xml index 87ace086f..305fbe6d6 100644 --- a/pms/data/cron_jobs.xml +++ b/pms/data/cron_jobs.xml @@ -101,5 +101,21 @@ model.send_cancelation_mail() + + Auto Invoicing Folios + 5 + + + days + -1 + + code + + + model.autoinvoicing() + diff --git a/pms/data/pms_data.xml b/pms/data/pms_data.xml index 66c9b3053..5345432e5 100644 --- a/pms/data/pms_data.xml +++ b/pms/data/pms_data.xml @@ -20,6 +20,13 @@ + + + Various Clients + Contact used for simplified invoices where no customer is available + diff --git a/pms/i18n/pms.pot b/pms/i18n/pms.pot index 71188ab0b..64e050871 100644 --- a/pms/i18n/pms.pot +++ b/pms/i18n/pms.pot @@ -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 diff --git a/pms/models/__init__.py b/pms/models/__init__.py index 8ee809acd..f70ee90e0 100644 --- a/pms/models/__init__.py +++ b/pms/models/__init__.py @@ -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 diff --git a/pms/models/account_bank_statement.py b/pms/models/account_bank_statement.py index 18c1a8398..96a61bf59 100644 --- a/pms/models/account_bank_statement.py +++ b/pms/models/account_bank_statement.py @@ -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() diff --git a/pms/models/account_bank_statement_line.py b/pms/models/account_bank_statement_line.py index 03ec91a54..064a0471c 100644 --- a/pms/models/account_bank_statement_line.py +++ b/pms/models/account_bank_statement_line.py @@ -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 diff --git a/pms/models/account_journal.py b/pms/models/account_journal.py index 9edb82d3c..deea22ab4 100644 --- a/pms/models/account_journal.py +++ b/pms/models/account_journal.py @@ -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")), + ) + ) + ) diff --git a/pms/models/account_move.py b/pms/models/account_move.py index 5857b620b..849e6c1c2 100644 --- a/pms/models/account_move.py +++ b/pms/models/account_move.py @@ -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 diff --git a/pms/models/account_move_line.py b/pms/models/account_move_line.py index aed425ca5..9fee23ccf 100644 --- a/pms/models/account_move_line.py +++ b/pms/models/account_move_line.py @@ -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] diff --git a/pms/models/account_payment.py b/pms/models/account_payment.py index 1d72cb2e1..664111aad 100644 --- a/pms/models/account_payment.py +++ b/pms/models/account_payment.py @@ -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): diff --git a/pms/models/folio_sale_line.py b/pms/models/folio_sale_line.py index fa0e333e7..becea0ffc 100644 --- a/pms/models/folio_sale_line.py +++ b/pms/models/folio_sale_line.py @@ -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 ) diff --git a/pms/models/pms_availability_plan.py b/pms/models/pms_availability_plan.py index 5052ecc4b..20b3718a4 100644 --- a/pms/models/pms_availability_plan.py +++ b/pms/models/pms_availability_plan.py @@ -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 diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index e5b52a40d..448b815b5 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -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") diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index b12d59b8d..9a81d6afb 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -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, } ) diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 317f12e02..90c0b9749 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -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 diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 835f01072..1c6347a5b 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -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: diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index 4da16632f..9d9b5656b 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -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, ) diff --git a/pms/models/res_company.py b/pms/models/res_company.py index 341d20f80..419d4f973 100644 --- a/pms/models/res_company.py +++ b/pms/models/res_company.py @@ -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", diff --git a/pms/models/res_partner.py b/pms/models/res_partner.py index 22ede5464..9866bf033 100644 --- a/pms/models/res_partner.py +++ b/pms/models/res_partner.py @@ -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() diff --git a/pms/models/res_partner_id_category.py b/pms/models/res_partner_id_category.py new file mode 100644 index 000000000..c54ea9488 --- /dev/null +++ b/pms/models/res_partner_id_category.py @@ -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, + ) diff --git a/pms/models/res_partner_id_number.py b/pms/models/res_partner_id_number.py index 9f43b045e..89e2e07de 100644 --- a/pms/models/res_partner_id_number.py +++ b/pms/models/res_partner_id_number.py @@ -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 diff --git a/pms/report/invoice.xml b/pms/report/invoice.xml new file mode 100644 index 000000000..a7e97acc4 --- /dev/null +++ b/pms/report/invoice.xml @@ -0,0 +1,31 @@ + + + + diff --git a/pms/report/pms_folio_templates.xml b/pms/report/pms_folio_templates.xml index a9f4ea018..56c62db6e 100644 --- a/pms/report/pms_folio_templates.xml +++ b/pms/report/pms_folio_templates.xml @@ -43,9 +43,9 @@
-
+
Your Reference: -

+

account.bank.statement + - - + + diff --git a/pms/views/account_move_views.xml b/pms/views/account_move_views.xml index 1b344e23a..201f78f36 100644 --- a/pms/views/account_move_views.xml +++ b/pms/views/account_move_views.xml @@ -5,17 +5,41 @@ - - + + - - + + + + + + + + @@ -25,6 +49,7 @@ + @@ -42,6 +67,11 @@ string="Property" context="{'group_by':'pms_property_id'}" /> + @@ -59,6 +89,11 @@ string="Property" context="{'group_by':'pms_property_id'}" /> + diff --git a/pms/views/account_payment_views.xml b/pms/views/account_payment_views.xml index 161f2b19f..73e55378a 100644 --- a/pms/views/account_payment_views.xml +++ b/pms/views/account_payment_views.xml @@ -5,7 +5,56 @@ - + + + + + + + + + account.payment + + + + + + + + + + + + + account.payment + + + + + + + + + + diff --git a/pms/views/pms_checkin_partner_views.xml b/pms/views/pms_checkin_partner_views.xml index eee4a1467..702edbc9c 100644 --- a/pms/views/pms_checkin_partner_views.xml +++ b/pms/views/pms_checkin_partner_views.xml @@ -67,11 +67,46 @@ - + +