diff --git a/pms/__manifest__.py b/pms/__manifest__.py index a8d4fb0d5..d9446afa7 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -37,7 +37,6 @@ "wizards/wizard_payment_folio.xml", "wizards/folio_make_invoice_advance_views.xml", "wizards/wizard_folio.xml", - "wizards/wizard_invoice_filter_days.xml", "wizards/wizard_folio_changes.xml", "views/pms_amenity_views.xml", "views/pms_amenity_type_views.xml", diff --git a/pms/demo/pms_master_data.xml b/pms/demo/pms_master_data.xml index 895b9d0f6..1e382f175 100644 --- a/pms/demo/pms_master_data.xml +++ b/pms/demo/pms_master_data.xml @@ -301,7 +301,6 @@ 'product_id': ref('pms_service_0'), 'amount': 3})]" /> - fixed Half Board @@ -314,7 +313,6 @@ 'amount': 8}) ]" /> - fixed FullBoard @@ -329,36 +327,29 @@ 'amount': 8}) ]" /> - fixed - fixed - fixed - - fixed - fixed - fixed diff --git a/pms/models/account_bank_statement_line.py b/pms/models/account_bank_statement_line.py index 57a8e69af..06ebb848f 100644 --- a/pms/models/account_bank_statement_line.py +++ b/pms/models/account_bank_statement_line.py @@ -22,8 +22,6 @@ class AccountBankStatementLine(models.Model): line.update( { "folio_ids": [(6, 0, self.statement_folio_ids.ids)], - "reservation_ids": [(6, 0, self.reservation_ids.ids)], - "service_ids": [(6, 0, self.service_ids.ids)], } ) return line_vals_list diff --git a/pms/models/account_move.py b/pms/models/account_move.py index 7b36ac26c..d4bd0eae4 100644 --- a/pms/models/account_move.py +++ b/pms/models/account_move.py @@ -14,7 +14,6 @@ class AccountMove(models.Model): comodel_name="pms.folio", compute="_compute_folio_origin" ) pms_property_id = fields.Many2one("pms.property") - from_reservation = fields.Boolean(compute="_compute_from_reservation") outstanding_folios_debits_widget = fields.Text( compute="_compute_get_outstanding_folios_JSON" ) @@ -31,12 +30,6 @@ class AccountMove(models.Model): if folios: inv.folio_ids = [(6, 0, folios.ids)] - def _compute_from_reservation(self): - for inv in self: - inv.from_reservation = False - if len(inv.invoice_line_ids.mapped("reservation_line_ids")) > 0: - inv.from_reservation = True - # Action methods def action_folio_payments(self): diff --git a/pms/models/account_move_line.py b/pms/models/account_move_line.py index 032b0edf4..b3a803cc4 100644 --- a/pms/models/account_move_line.py +++ b/pms/models/account_move_line.py @@ -1,40 +1,13 @@ # Copyright 2017 Alexandre Díaz # Copyright 2017 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from odoo import api, fields, models +from odoo import fields, models class AccountMoveLine(models.Model): _inherit = "account.move.line" # Fields declaration - reservation_ids = fields.Many2many( - "pms.reservation", - "reservation_move_rel", - "move_line_id", - "reservation_id", - string="Reservations", - readonly=True, - copy=False, - ) - service_ids = fields.Many2many( - "pms.service", - "service_line_move_rel", - "move_line_id", - "service_id", - string="Services", - readonly=True, - copy=False, - ) - reservation_line_ids = fields.Many2many( - "pms.reservation.line", - "reservation_line_move_rel", - "move_line_id", - "reservation_line_id", - string="Reservation Lines", - readonly=True, - copy=False, - ) folio_line_ids = fields.Many2many( "folio.sale.line", "folio_sale_line_invoice_rel", @@ -49,68 +22,8 @@ class AccountMoveLine(models.Model): "move_id", "folio_id", string="Folios", - ondelete="cascade", - compute="_compute_folio_ids", - readonly=False, - store=True, ) - name = fields.Char( - compute="_compute_name", - readonly=False, - store=True, - ) - - @api.depends("service_ids", "reservation_ids") - def _compute_folio_ids(self): - for record in self: - if record.service_ids: - record.folio_ids = record.mapped("service_ids.folio_id") - elif record.reservation_ids: - record.folio_ids = record.mapped("reservation_ids.folio_id") - elif not record.folio_ids: - record.folio_ids = False - - def invoice_filter_days(self): - action = self.env["ir.actions.act_window"]._for_xml_id( - "pms.pms_invoice_filter_days_action" - ) - # Force the values of the move line in the context to avoid issues - ctx = dict(self.env.context) - ctx.pop("active_id", None) - ctx["active_ids"] = self.ids - ctx["active_model"] = "account.move.line" - action["context"] = ctx - return action def _copy_data_extend_business_fields(self, values): super(AccountMoveLine, self)._copy_data_extend_business_fields(values) values["folio_line_ids"] = [(6, None, self.folio_line_ids.ids)] - values["reservation_line_ids"] = [(6, None, self.reservation_line_ids.ids)] - values["service_ids"] = [(6, None, self.service_ids.ids)] - values["reservation_ids"] = [(6, None, self.reservation_ids.ids)] - - @api.depends("reservation_line_ids") - def _compute_name(self): - if hasattr(super(), "_compute_name"): - super()._compute_field() - for record in self: - if record.reservation_line_ids: - record.name = record._get_compute_name() - - def _get_compute_name(self): - self.ensure_one() - if self.reservation_line_ids: - month = False - name = False - lines = self.reservation_line_ids.sorted("date") - for date in lines.mapped("date"): - if date.month != month: - name = name + "\n" if name else "" - name += date.strftime("%B-%Y") + ": " - name += date.strftime("%d") - month = date.month - else: - name += ", " + date.strftime("%d") - return name - else: - return False diff --git a/pms/models/folio_sale_line.py b/pms/models/folio_sale_line.py index 1f6852e0c..f3a2571eb 100644 --- a/pms/models/folio_sale_line.py +++ b/pms/models/folio_sale_line.py @@ -24,6 +24,9 @@ class FolioSaleLine(models.Model): for line in self: if line.state == "draft": 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 + # https://github.com/OCA/OCB/blob/14.0/addons/sale/models/sale.py#L1160 elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): line.invoice_status = "to invoice" elif ( @@ -71,18 +74,6 @@ class FolioSaleLine(models.Model): else: return False - @api.depends("service_id", "service_id.price_unit") - def _compute_price_unit(self): - """ - Compute unit prices of services - On reservations the unit price is compute by group in folio - """ - for record in self: - if record.service_id: - record.price_unit = record.service_id.price_unit - elif not record.price_unit: - record.price_unit = False - @api.depends("product_uom_qty", "discount", "price_unit", "tax_ids") def _compute_amount(self): """ @@ -121,11 +112,23 @@ class FolioSaleLine(models.Model): else record.reservation_id.tax_ids ) - @api.depends("service_id", "service_id.discount") + @api.depends( + "service_id", + "service_id.service_line_ids", + "service_id.service_line_ids.discount", + ) def _compute_discount(self): - self.discount = 0.0 - for record in self.filtered("service_id"): - record.discount = record.service_id.discount + """ + Only in services without room we compute discount, + and this services only have one service line + """ + for record in self: + if record.service_id and not record.service_id.reservation_id: + record.discount = record.service_id.service_line_ids.mapped("discount")[ + 0 + ] + elif not record.discount: + record.discount = 0 @api.depends("reservation_id.room_type_id", "service_id.product_id") def _compute_product_id(self): @@ -355,6 +358,12 @@ class FolioSaleLine(models.Model): index=True, copy=False, ) + is_board_service = fields.Boolean( + string="Board Service", + related="service_id.is_board_service", + store=True, + ) + name = fields.Text( string="Description", compute="_compute_name", store=True, readonly=False ) @@ -363,6 +372,10 @@ class FolioSaleLine(models.Model): "pms.reservation.line", string="Nights", ) + service_line_ids = fields.Many2many( + "pms.service.line", + string="Service Lines", + ) sequence = fields.Integer(string="Sequence", default=10) invoice_lines = fields.Many2many( @@ -389,8 +402,6 @@ class FolioSaleLine(models.Model): price_unit = fields.Float( "Unit Price", digits="Product Price", - compute="_compute_price_unit", - store=True, ) price_subtotal = fields.Monetary( @@ -433,6 +444,7 @@ class FolioSaleLine(models.Model): string="Discount (%)", digits="Discount", compute="_compute_discount", + readonly=False, store=True, ) @@ -550,13 +562,13 @@ class FolioSaleLine(models.Model): help="Technical field for UX purpose.", ) - @api.depends("reservation_line_ids", "service_id") + @api.depends("reservation_line_ids", "service_line_ids", "service_line_ids.day_qty") def _compute_product_uom_qty(self): for line in self: if line.reservation_line_ids: line.product_uom_qty = len(line.reservation_line_ids) - elif line.service_id: - line.product_uom_qty = line.service_id.product_qty + elif line.service_line_ids: + line.product_uom_qty = sum(line.service_line_ids.mapped("day_qty")) elif not line.product_uom_qty: line.product_uom_qty = False @@ -716,15 +728,6 @@ class FolioSaleLine(models.Model): + " The quantity pending to invoice is %s" % self.qty_to_invoice ) ) - reservation = self.reservation_id - service = self.service_id - reservation_lines = self.reservation_line_ids.filtered( - lambda l: not l.invoiced and l.reservation_id - ) - lines_to_invoice = list() - if self.reservation_id: - for i in range(0, int(qty)): - lines_to_invoice.append(reservation_lines[i].id) res = { "display_type": self.display_type, "sequence": self.sequence, @@ -738,9 +741,6 @@ class FolioSaleLine(models.Model): "analytic_account_id": self.folio_id.analytic_account_id.id, "analytic_tag_ids": [(6, 0, self.analytic_tag_ids.ids)], "folio_line_ids": [(6, 0, [self.id])], - "reservation_ids": [(6, 0, reservation.ids)], - "service_ids": [(6, 0, service.ids)], - "reservation_line_ids": [(6, 0, lines_to_invoice)], } if optional_values: res.update(optional_values) diff --git a/pms/models/pms_board_service.py b/pms/models/pms_board_service.py index 8dc7a4ebf..5f4d971db 100644 --- a/pms/models/pms_board_service.py +++ b/pms/models/pms_board_service.py @@ -18,12 +18,6 @@ class PmsBoardService(models.Model): pms_board_service_room_type_ids = fields.One2many( "pms.board.service.room.type", "pms_board_service_id" ) - price_type = fields.Selection( - [("fixed", "Fixed"), ("percent", "Percent")], - string="Type", - default="fixed", - required=True, - ) amount = fields.Float( "Amount", digits=("Product Price"), compute="_compute_board_amount", store=True ) diff --git a/pms/models/pms_board_service_room_type.py b/pms/models/pms_board_service_room_type.py index 38219458a..292d52320 100644 --- a/pms/models/pms_board_service_room_type.py +++ b/pms/models/pms_board_service_room_type.py @@ -1,7 +1,7 @@ # Copyright 2017 Dario # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import UserError class PmsBoardServiceRoomType(models.Model): @@ -11,21 +11,6 @@ class PmsBoardServiceRoomType(models.Model): _log_access = False _description = "Board Service included in Room" - # Default Methods ang Gets - - def name_get(self): - result = [] - for res in self: - if res.pricelist_id: - name = u"{} ({})".format( - res.pms_board_service_id.name, - res.pricelist_id.name, - ) - else: - name = u"{} ({})".format(res.pms_board_service_id.name, _("Generic")) - result.append((res.id, name)) - return result - # Fields declaration pms_board_service_id = fields.Many2one( "pms.board.service", @@ -52,27 +37,9 @@ class PmsBoardServiceRoomType(models.Model): ("pms_property_ids", "in", pms_property_ids), ], ) - pricelist_id = fields.Many2one( - "product.pricelist", - string="Pricelist", - required=False, - domain=[ - "|", - ("pms_property_ids", "=", False), - ("pms_property_ids", "in", pms_property_ids), - ], - ) board_service_line_ids = fields.One2many( "pms.board.service.room.type.line", "pms_board_service_room_type_id" ) - # TODO:review relation with pricelist and properties - - price_type = fields.Selection( - [("fixed", "Fixed"), ("percent", "Percent")], - string="Type", - default="fixed", - required=True, - ) amount = fields.Float( "Amount", digits=("Product Price"), compute="_compute_board_amount", store=True ) @@ -87,41 +54,7 @@ class PmsBoardServiceRoomType(models.Model): total += service.amount record.update({"amount": total}) - # Constraints and onchanges - @api.constrains("pricelist_id") - def constrains_pricelist_id(self): - for record in self: - if self.pricelist_id: - board_pricelist = self.env["pms.board.service.room.type"].search( - [ - ("pricelist_id", "=", record.pricelist_id.id), - ("pms_room_type_id", "=", record.pms_room_type_id.id), - ("pms_board_service_id", "=", record.pms_board_service_id.id), - ("id", "!=", record.id), - ] - ) - if board_pricelist: - raise UserError( - _("This Board Service in this Room can't repeat pricelist") - ) - else: - board_pricelist = self.env["pms.board.service.room.type"].search( - [ - ("pricelist_id", "=", False), - ("pms_room_type_id", "=", record.pms_room_type_id.id), - ("pms_board_service_id", "=", record.pms_board_service_id.id), - ("id", "!=", record.id), - ] - ) - if board_pricelist: - raise UserError( - _( - "This Board Service in this Room \ - can't repeat without pricelist" - ) - ) - - @api.constrains("by_default", "pricelist_id") + @api.constrains("by_default") def constrains_duplicated_board_defaul(self): for record in self: default_boards = ( @@ -130,34 +63,8 @@ class PmsBoardServiceRoomType(models.Model): ) ) # TODO Check properties (with different propertys is allowed) - if any( - default_boards.mapped( - lambda l: l.pricelist_id == record.pricelist_id - and l.id != record.id - ) - ): - raise UserError( - _( - """Only can set one default board service by - pricelist (or without pricelist)""" - ) - ) - - @api.constrains("pms_property_ids", "pms_room_type_ids") - def _check_room_type_property_integrity(self): - for record in self: - if record.pms_property_ids and record.pms_room_type_id.pms_property_ids: - for pms_property in record.pms_property_ids: - if pms_property not in record.pms_room_type_id.pms_property_ids: - raise ValidationError(_("Property not allowed in room type")) - - @api.constrains("pms_property_ids", "pricelist_id") - def _check_pricelist_property_integrity(self): - for record in self: - if record.pms_property_ids and record.pricelist_id: - for pms_property in record.pms_property_ids: - if pms_property not in record.pricelist_id.pms_property_ids: - raise ValidationError(_("Property not allowed in pricelist")) + if any(default_boards.filtered(lambda l: l.id != record.id)): + raise UserError(_("""Only can set one default board service""")) # Action methods @@ -176,13 +83,13 @@ class PmsBoardServiceRoomType(models.Model): def init(self): self._cr.execute( "SELECT indexname FROM pg_indexes WHERE indexname = %s", - ("pms_board_service_id_pms_room_type_id_pricelist_id",), + ("pms_board_service_id_pms_room_type_id",), ) if not self._cr.fetchone(): self._cr.execute( - "CREATE INDEX pms_board_service_id_pms_room_type_id_pricelist_id \ + "CREATE INDEX pms_board_service_id_pms_room_type_id \ ON pms_board_service_room_type_rel \ - (pms_board_service_id, pms_room_type_id, pricelist_id)" + (pms_board_service_id, pms_room_type_id)" ) @api.model diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 96213a3c0..7b5d3ccbb 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -391,6 +391,9 @@ class PmsFolio(models.Model): "reservation_ids", "service_ids", "service_ids.reservation_id", + "service_ids.service_line_ids.price_day_total", + "service_ids.service_line_ids.discount", + "service_ids.service_line_ids.cancel_discount", "reservation_ids.reservation_line_ids", "reservation_ids.reservation_line_ids.price", "reservation_ids.reservation_line_ids.discount", @@ -415,7 +418,7 @@ class PmsFolio(models.Model): }, ) ) - group_lines = {} + group_reservation_lines = {} for line in reservation.reservation_line_ids: # On resevations the price, and discounts fields are used # by group, we need pass this in the create line @@ -431,32 +434,45 @@ class PmsFolio(models.Model): for discount in [line.discount, line.cancel_discount]: discount_factor = discount_factor * ((100.0 - discount) / 100.0) final_discount = 100.0 - (discount_factor * 100.0) - if group_key not in group_lines: - group_lines[group_key] = { + if group_key not in group_reservation_lines: + group_reservation_lines[group_key] = { "reservation_id": reservation.id, "discount": final_discount, "price_unit": line.price, "reservation_line_ids": [(4, line.id)], } else: - group_lines[group_key][("reservation_line_ids")].append( - (4, line.id) - ) - for item in group_lines.items(): + group_reservation_lines[group_key][ + ("reservation_line_ids") + ].append((4, line.id)) + for item in group_reservation_lines.items(): sale_lines.append((0, False, item[1])) for service in reservation.service_ids: - # On service the price, and discounts fields are - # compute in the sale.order.line - sale_lines.append( - ( - 0, - False, - { + # Service days with different prices, + # go to differente sale lines + group_service_lines = {} + for service_line in service.service_line_ids: + service_group_key = ( + service_line.price_unit, + service_line.discount, + service_line.cancel_discount, + ) + if service_group_key not in group_service_lines: + # On service the price, and discounts fields are + # compute in the sale.order.line + group_service_lines[service_group_key] = { "name": service.name, "service_id": service.id, - }, - ) - ) + "discount": service_line.discount, + "price_unit": service_line.price_unit, + "service_line_ids": [(4, service_line.id)], + } + else: + group_service_lines[service_group_key][ + ("service_line_ids") + ].append((4, service_line.id)) + for item in group_service_lines.items(): + sale_lines.append((0, False, item[1])) if services_without_room: sale_lines.append( ( @@ -646,25 +662,20 @@ class PmsFolio(models.Model): else: order.invoice_status = "no" - @api.depends("reservation_ids.price_total", "service_ids.price_total") + @api.depends("sale_line_ids.price_total") def _compute_amount_all(self): """ Compute the total amounts of the SO. """ - for record in self.filtered("pricelist_id"): + for folio in self: amount_untaxed = amount_tax = 0.0 - amount_untaxed = sum(record.reservation_ids.mapped("price_subtotal")) + sum( - record.service_ids.mapped("price_subtotal") - ) - amount_tax = sum(record.reservation_ids.mapped("price_tax")) + sum( - record.service_ids.mapped("price_tax") - ) - record.update( + for line in folio.sale_line_ids: + amount_untaxed += line.price_subtotal + amount_tax += line.price_tax + folio.update( { - "amount_untaxed": record.pricelist_id.currency_id.round( - amount_untaxed - ), - "amount_tax": record.pricelist_id.currency_id.round(amount_tax), + "amount_untaxed": amount_untaxed, + "amount_tax": amount_tax, "amount_total": amount_untaxed + amount_tax, } ) @@ -1248,9 +1259,9 @@ class PmsFolio(models.Model): def _get_tax_amount_by_group(self): self.ensure_one() res = {} - for line in self.reservation_ids: + for line in self.sale_line_ids: price_reduce = line.price_total - product = line.room_type_id.product_id + product = line.product_id taxes = line.tax_ids.compute_all(price_reduce, quantity=1, product=product)[ "taxes" ] @@ -1261,18 +1272,6 @@ class PmsFolio(models.Model): if t["id"] == tax.id or t["id"] in tax.children_tax_ids.ids: res[group]["amount"] += t["amount"] res[group]["base"] += t["base"] - for line in self.service_ids: - price_reduce = line.price_unit * (1.0 - line.discount / 100.0) - taxes = line.tax_ids.compute_all( - price_reduce, quantity=line.product_qty, product=line.product_id - )["taxes"] - for tax in line.tax_ids: - group = tax.tax_group_id - res.setdefault(group, {"amount": 0.0, "base": 0.0}) - for t in taxes: - if t["id"] == tax.id or t["id"] in tax.children_tax_ids.ids: - res[group]["amount"] += t["amount"] - res[group]["base"] += t["base"] res = sorted(res.items(), key=lambda line: line[0].sequence) res = [ (line[0].name, line[1]["amount"], line[1]["base"], len(res)) for line in res diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index e6c50477e..0751e2a66 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -7,7 +7,6 @@ import time from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError -from odoo.tools import float_compare, float_is_zero _logger = logging.getLogger(__name__) @@ -83,6 +82,12 @@ class PmsReservation(models.Model): copy=False, check_company=True, ) + sale_line_ids = fields.One2many( + comodel_name="folio.sale.line", + inverse_name="reservation_id", + string="Sale Lines", + copy=False, + ) board_service_room_id = fields.Many2one( "pms.board.service.room.type", string="Board Service", @@ -184,7 +189,8 @@ class PmsReservation(models.Model): ) user_id = fields.Many2one( related="folio_id.user_id", - depends=["folio_id"], + depends=["folio_id.user_id"], + default=lambda self: self.env.user.id, readonly=False, store=True, ) @@ -274,14 +280,6 @@ class PmsReservation(models.Model): ondelete="restrict", domain=["|", ("active", "=", False), ("active", "=", True)], ) - move_line_ids = fields.Many2many( - "account.move.line", - "reservation_move_rel", - "reservation_id", - "move_line_id", - string="Invoice Lines", - copy=False, - ) localizator = fields.Char( string="Localizator", compute="_compute_localizator", @@ -446,32 +444,6 @@ class PmsReservation(models.Model): readonly=True, default="no", ) - qty_to_invoice = fields.Float( - compute="_compute_qty_to_invoice", - string="To Invoice Quantity", - store=True, - readonly=True, - digits=("Product Unit of Measure"), - ) - qty_invoiced = fields.Float( - compute="_compute_qty_invoiced", - string="Invoiced Quantity", - store=True, - readonly=True, - digits=("Product Unit of Measure"), - ) - untaxed_amount_invoiced = fields.Monetary( - "Untaxed Invoiced Amount", - compute="_compute_untaxed_amount_invoiced", - compute_sudo=True, - store=True, - ) - untaxed_amount_to_invoice = fields.Monetary( - "Untaxed Amount To Invoice", - compute="_compute_untaxed_amount_to_invoice", - compute_sudo=True, - store=True, - ) analytic_tag_ids = fields.Many2many( "account.analytic.tag", string="Analytic Tags", @@ -552,20 +524,15 @@ class PmsReservation(models.Model): if reservation.pricelist_id and reservation.room_type_id: board_service_default = self.env["pms.board.service.room.type"].search( [ - "&", - "&", ("pms_room_type_id", "=", reservation.room_type_id.id), ("by_default", "=", True), - "|", - ("pricelist_id", "=", reservation.pricelist_id.id), - ("pricelist_id", "=", False), ] ) - if len(board_service_default) > 1: - reservation.board_service_room_id = board_service_default.filtered( - lambda b: b.pricelist_id == reservation.pricelist_id - ) - else: + if ( + not reservation.board_service_room_id + or not reservation.board_service_room_id.pms_room_type_id + == reservation.room_type_id + ): reservation.board_service_room_id = ( board_service_default.id if board_service_default else False ) @@ -693,6 +660,13 @@ class PmsReservation(models.Model): ("is_board_service", "=", True), ] ) + # Avoid recalculating services if the boardservice has not changed + if ( + old_board_lines + and reservation.board_service_room_id + == reservation._origin.board_service_room_id + ): + return if reservation.board_service_room_id: board = self.env["pms.board.service.room.type"].browse( reservation.board_service_room_id.id @@ -707,6 +681,8 @@ class PmsReservation(models.Model): board_services.append((0, False, res)) reservation.service_ids -= old_board_lines reservation.service_ids = board_services + elif old_board_lines: + reservation.service_ids -= old_board_lines @api.depends("partner_id", "agency_id") def _compute_pricelist_id(self): @@ -727,13 +703,19 @@ class PmsReservation(models.Model): reservation.pms_property_id.default_pricelist_id.id ) - @api.depends("pricelist_id") + @api.depends("pricelist_id", "room_type_id") def _compute_show_update_pricelist(self): for reservation in self: if ( sum(reservation.reservation_line_ids.mapped("price")) > 0 - and reservation.pricelist_id - and reservation._origin.pricelist_id != reservation.pricelist_id + and ( + reservation.pricelist_id + and reservation._origin.pricelist_id != reservation.pricelist_id + ) + or ( + reservation.room_type_id + and reservation._origin.room_type_id != reservation.room_type_id + ) ): reservation.show_update_pricelist = True else: @@ -1036,183 +1018,28 @@ class PmsReservation(models.Model): if room_ids: reservation.preferred_room_id = room_ids[0] - @api.depends("state", "qty_to_invoice", "qty_invoiced") + @api.depends( + "sale_line_ids", + "sale_line_ids.invoice_status", + ) def _compute_invoice_status(self): """ Compute the invoice status of a Reservation. Possible statuses: - - no: if the Folio is not in status 'sale' or 'done', we consider - that there is nothing to invoice. This is also hte default value - if the conditions of no other status is met. - - to invoice: we refer to the quantity to invoice of the line. - Refer to method `_compute_get_to_invoice_qty()` for more information - on how this quantity is calculated. - - invoiced: the quantity invoiced is larger or equal to the - quantity ordered. - """ - precision = self.env["decimal.precision"].precision_get( - "Product Unit of Measure" - ) - for line in self: - if line.state == "draft": - line.invoice_status = "no" - elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): - line.invoice_status = "to invoice" - elif ( - float_compare( - line.qty_invoiced, - len(line.reservation_line_ids), - precision_digits=precision, - ) - >= 0 - ): - line.invoice_status = "invoiced" - else: - line.invoice_status = "no" - - @api.depends("qty_invoiced", "nights", "folio_id.state") - def _compute_qty_to_invoice(self): - """ - Compute the quantity to invoice. The quantity to invoice is - calculated from the nights quantity. + Base on folio sale line invoice status """ for line in self: - if line.folio_id.state not in ["draft"]: - line.qty_to_invoice = len(line.reservation_line_ids) - line.qty_invoiced - else: - line.qty_to_invoice = 0 - - @api.depends( - "move_line_ids.move_id.state", - "move_line_ids.quantity", - "untaxed_amount_to_invoice", - ) - def _compute_qty_invoiced(self): - """ - Compute the quantity invoiced. If case of a refund, the quantity - invoiced is decreased. We must check day per day and sum or - decreased on 1 unit per invoice_line - """ - for record in self: - qty_invoiced = 0.0 - for line in record.reservation_line_ids: - invoice_lines = line.move_line_ids.filtered( - lambda r: r.move_id.state != "cancel" - ) - qty_invoiced += len( - invoice_lines.filtered( - lambda r: r.move_id.move_type == "out_invoice" - ) - ) - len( - invoice_lines.filtered( - lambda r: r.move_id.move_type == "out_refund" - ) - ) - record.qty_invoiced = qty_invoiced - - @api.depends( - "move_line_ids", - "move_line_ids.price_total", - "move_line_ids.move_id.state", - "move_line_ids.move_id.move_type", - ) - def _compute_untaxed_amount_invoiced(self): - """Compute the untaxed amount already invoiced from the reservation, - taking the refund attached - the reservation into account. This amount is computed as - SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal) - where - `inv_line` is a customer invoice line linked to the reservation - `ref_line` is a customer credit note (refund) - line linked to the reservation - """ - for line in self: - amount_invoiced = 0.0 - for invoice_line in line.move_line_ids: - if invoice_line.move_id.state == "posted": - invoice_date = ( - invoice_line.move_id.invoice_date or fields.Date.today() - ) - if invoice_line.move_id.move_type == "out_invoice": - amount_invoiced += invoice_line.currency_id._convert( - invoice_line.price_subtotal, - line.currency_id, - line.company_id, - invoice_date, - ) - elif invoice_line.move_id.move_type == "out_refund": - amount_invoiced -= invoice_line.currency_id._convert( - invoice_line.price_subtotal, - line.currency_id, - line.company_id, - invoice_date, - ) - line.untaxed_amount_invoiced = amount_invoiced - - @api.depends( - "state", "discount", "price_total", "room_type_id", "untaxed_amount_invoiced" - ) - def _compute_untaxed_amount_to_invoice(self): - """Total of remaining amount to invoice on the reservation (taxes excl.) as - total_sol - amount already invoiced - - Note: Draft invoice are ignored on purpose, the 'to invoice' amount should - come only from the reservation. - """ - for line in self: - amount_to_invoice = 0.0 - if line.state not in ["draft"]: - price_subtotal = 0.0 - price_subtotal = line.price_subtotal - if len(line.tax_ids.filtered(lambda tax: tax.price_include)) > 0: - # As included taxes are not excluded from the computed subtotal, - # `compute_all()` method - # has to be called to retrieve the subtotal without them. - price_subtotal = line.tax_ids.compute_all( - price_subtotal, - currency=line.currency_id, - quantity=line.nights, - product=line.room_type_id.product_id, - )["total_excluded"] - - if any( - line.move_line_ids.mapped(lambda i: i.discount != line.discount) - ): - # In case of re-invoicing with different discount we - # try to calculate manually the - # remaining amount to invoice - amount = 0 - for move in line.move_line_ids: - if ( - len(move.tax_ids.filtered(lambda tax: tax.price_include)) - > 0 - ): - amount += move.tax_ids.compute_all( - move.currency_id._convert( - move.price_unit, - line.currency_id, - line.company_id, - move.date or fields.Date.today(), - round=False, - ) - * move.quantity - )["total_excluded"] - else: - amount += ( - move.currency_id._convert( - move.price_unit, - line.currency_id, - line.company_id, - move.date or fields.Date.today(), - round=False, - ) - * move.quantity - ) - - amount_to_invoice = max(price_subtotal - amount, 0) + states = list(set(line.sale_line_ids.mapped("invoice_status"))) + if len(states) == 1: + line.invoice_status = states[0] + elif len(states) >= 1: + if "to_invoice" in states: + line.invoice_status = "to_invoice" + elif "invoiced" in states: + line.invoice_status = "invoiced" else: - amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced - - line.untaxed_amount_to_invoice = amount_to_invoice + line.invoice_status = "no" + else: + line.invoice_status = "no" @api.depends("reservation_line_ids") def _compute_nights(self): @@ -1529,8 +1356,10 @@ class PmsReservation(models.Model): self.show_update_pricelist = False self.message_post( body=_( - "Prices have been recomputed according to pricelist %s ", + """Prices have been recomputed according to pricelist %s + and room type %s""", self.pricelist_id.display_name, + self.room_type_id.name, ) ) diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index fd49aeb9b..c1605c48d 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -41,12 +41,12 @@ class PmsReservationLine(models.Model): store=True, readonly=False, ) - move_line_ids = fields.Many2many( - "account.move.line", - "reservation_line_move_rel", + sale_line_ids = fields.Many2many( + "folio.sale.line", + "reservation_line_sale_line_rel", "reservation_line_id", - "move_line_id", - string="Invoice Lines", + "sale_line_id", + string="Sales Lines", readonly=True, copy=False, ) @@ -65,11 +65,6 @@ class PmsReservationLine(models.Model): store=True, readonly=False, ) - invoiced = fields.Boolean( - string="Invoiced", - compute="_compute_invoiced", - store=True, - ) cancel_discount = fields.Float( string="Cancelation Discount (%)", digits=("Discount"), @@ -311,31 +306,15 @@ class PmsReservationLine(models.Model): else: line.occupies_availability = True - @api.depends("move_line_ids", "move_line_ids.move_id.state") - def _compute_invoiced(self): - for line in self: - qty_invoiced = 0 - for invoice_line in line.move_line_ids: - if invoice_line.move_id.state != "cancel": - if invoice_line.move_id.move_type == "out_invoice": - qty_invoiced += 1 - elif invoice_line.move_id.move_type == "out_refund": - qty_invoiced -= 1 - line.invoiced = False if qty_invoiced < 1 else True - # TODO: Refact method and allowed cancelled single days @api.depends("reservation_id.cancelled_reason") def _compute_cancel_discount(self): for line in self: line.cancel_discount = 0 + # TODO: Review cancel logic # reservation = line.reservation_id # pricelist = reservation.pricelist_id # if reservation.state == "cancelled": - # # TODO: Set 0 qty on cancel room services change to compute day_qty - # # (view constrain service_line_days) - # for service in reservation.service_ids: - # service.service_line_ids.write({"day_qty": 0}) - # service._compute_days_qty() # if ( # reservation.cancelled_reason # and pricelist diff --git a/pms/models/pms_room_type.py b/pms/models/pms_room_type.py index 124b0e72b..b9ffd4968 100644 --- a/pms/models/pms_room_type.py +++ b/pms/models/pms_room_type.py @@ -208,6 +208,8 @@ class PmsRoomType(models.Model): if room.pms_property_id not in record.pms_property_ids: raise ValidationError(_("Property not allowed in room")) + # TODO: Not allowed repeat boardservice on room_type with + # same properties os without properties @api.constrains("board_service_room_type_ids", "pms_property_ids") def _check_integrity_property_board_service_room_type(self): for record in self: diff --git a/pms/models/pms_service.py b/pms/models/pms_service.py index dd47e93ca..800480d7d 100644 --- a/pms/models/pms_service.py +++ b/pms/models/pms_service.py @@ -5,7 +5,6 @@ import logging from datetime import timedelta from odoo import _, api, fields, models -from odoo.tools import float_compare, float_is_zero _logger = logging.getLogger(__name__) @@ -52,8 +51,17 @@ class PmsService(models.Model): readonly=False, store=True, ) + sale_line_ids = fields.One2many( + comodel_name="folio.sale.line", + inverse_name="service_id", + string="Sale Lines", + copy=False, + ) reservation_id = fields.Many2one( - "pms.reservation", "Room", default=_default_reservation_id + "pms.reservation", + "Room", + default=_default_reservation_id, + ondelete="cascade", ) service_line_ids = fields.One2many( "pms.service.line", @@ -79,14 +87,6 @@ class PmsService(models.Model): readonly=False, domain=["|", ("active", "=", False), ("active", "=", True)], ) - move_line_ids = fields.Many2many( - "account.move.line", - "service_line_move_rel", - "service_id", - "move_line_id", - string="move Lines", - copy=False, - ) analytic_tag_ids = fields.Many2many("account.analytic.tag", string="Analytic Tags") currency_id = fields.Many2one( related="folio_id.currency_id", store=True, string="Currency", readonly=True @@ -128,28 +128,6 @@ class PmsService(models.Model): ], string="Sales Channel", ) - price_unit = fields.Float( - "Unit Price", - digits=("Product Price"), - compute="_compute_price_unit", - store=True, - readonly=False, - ) - discount = fields.Float(string="Discount (%)", digits=("Discount"), default=0.0) - qty_to_invoice = fields.Float( - compute="_compute_get_to_invoice_qty", - string="To Invoice", - store=True, - readonly=True, - digits=("Product Unit of Measure"), - ) - qty_invoiced = fields.Float( - compute="_compute_get_invoice_qty", - string="Invoiced", - store=True, - readonly=True, - digits=("Product Unit of Measure"), - ) price_subtotal = fields.Monetary( string="Subtotal", readonly=True, store=True, compute="_compute_amount_service" ) @@ -214,6 +192,7 @@ class PmsService(models.Model): # cached (otherwise double the date) pass elif not old_line: + price_unit = service._get_price_unit_line(idate) lines.append( ( 0, @@ -221,6 +200,7 @@ class PmsService(models.Model): { "date": idate, "day_qty": day_qty, + "price_unit": price_unit, }, ) ) @@ -246,12 +226,11 @@ class PmsService(models.Model): ] ) ) - _logger.info(service) - _logger.info(lines) service.service_line_ids = lines else: # TODO: Review (business logic refact) no per_day logic service if not service.service_line_ids: + price_unit = service._get_price_unit_line() service.service_line_ids = [ ( 0, @@ -259,6 +238,7 @@ class PmsService(models.Model): { "date": fields.Date.today(), "day_qty": day_qty, + "price_unit": price_unit, }, ) ] @@ -266,6 +246,7 @@ class PmsService(models.Model): # TODO: Service without reservation(room) but with folio¿? # example: tourist tour in group if not service.service_line_ids: + price_unit = service._get_price_unit_line() service.service_line_ids = [ ( 0, @@ -273,6 +254,7 @@ class PmsService(models.Model): { "date": fields.Date.today(), "day_qty": day_qty, + "price_unit": price_unit, }, ) ] @@ -300,79 +282,6 @@ class PmsService(models.Model): qty = sum(service.service_line_ids.mapped("day_qty")) service.product_qty = qty - @api.depends( - "product_id", - "service_line_ids", - "reservation_id.pricelist_id", - "reservation_id.pms_property_id", - "pms_property_id", - ) - def _compute_price_unit(self): - for service in self: - folio = service.folio_id - reservation = service.reservation_id - origin = reservation if reservation else folio - if origin: - if service._recompute_price(): - partner = origin.partner_id - pricelist = origin.pricelist_id - if reservation and service.is_board_service: - board_room_type = reservation.board_service_room_id - if board_room_type.price_type == "fixed": - service.price_unit = ( - self.env["pms.board.service.room.type.line"] - .search( - [ - ( - "pms_board_service_room_type_id", - "=", - board_room_type.id, - ), - ("product_id", "=", service.product_id.id), - ] - ) - .amount - ) - else: - service.price_unit = ( - reservation.price_total - * self.env["pms.board.service.room.type.line"] - .search( - [ - ( - "pms_board_service_room_type_id", - "=", - board_room_type.id, - ), - ("product_id", "=", service.product_id.id), - ] - ) - .amount - ) / 100 - else: - product = service.product_id.with_context( - lang=partner.lang, - partner=partner.id, - quantity=service.product_qty, - date=folio.date_order if folio else fields.Date.today(), - pricelist=pricelist.id, - uom=service.product_id.uom_id.id, - fiscal_position=False, - property=service.pms_property_id.id, - ) - service.price_unit = self.env[ - "account.tax" - ]._fix_tax_included_price_company( - service._get_display_price(product), - product.taxes_id, - service.tax_ids, - origin.company_id, - ) - else: - service.price_unit = service._origin.price_unit - else: - service.price_unit = 0 - @api.depends("reservation_id") def _compute_folio_id(self): for record in self: @@ -381,133 +290,54 @@ class PmsService(models.Model): elif not record.folio_id: record.folio_id = False - def _recompute_price(self): - # REVIEW: Conditional to avoid overriding already calculated prices, - # I'm not sure it's the best way - self.ensure_one() - # folio/reservation origin service - folio_origin = self._origin.folio_id - reservation_origin = self._origin.reservation_id - origin = reservation_origin if reservation_origin else folio_origin - # folio/reservation new service - folio_new = self.folio_id - reservation_new = self.reservation_id - new = reservation_new if reservation_new else folio_new - price_fields = [ - "pricelist_id", - "reservation_type", - "pms_property_id", - ] - if ( - any(origin[field] != new[field] for field in price_fields) - or self._origin.price_unit == 0 - ): - return True - return False - - @api.depends("qty_invoiced", "product_qty", "folio_id.state") - def _compute_get_to_invoice_qty(self): - """ - Compute the quantity to invoice. If the invoice policy is order, - the quantity to invoice is calculated from the ordered quantity. - Otherwise, the quantity delivered is used. - """ - for line in self: - if line.folio_id.state not in ["draft"]: - line.qty_to_invoice = line.product_qty - line.qty_invoiced - else: - line.qty_to_invoice = 0 - - @api.depends("move_line_ids.move_id.state", "move_line_ids.quantity") - def _compute_get_invoice_qty(self): - """ - Compute the quantity invoiced. If case of a refund, - the quantity invoiced is decreased. Note that this is the case only - if the refund is generated from the Folio and that is intentional: if - a refund made would automatically decrease the invoiced quantity, - then there is a risk of reinvoicing it automatically, which may - not be wanted at all. That's why the refund has to be - created from the Folio - """ - for line in self: - qty_invoiced = 0.0 - for invoice_line in line.move_line_ids: - if invoice_line.move_id.state != "cancel": - if invoice_line.move_id.move_type == "out_invoice": - qty_invoiced += invoice_line.product_uom_id._compute_quantity( - invoice_line.quantity, line.product_id.uom_id - ) - elif invoice_line.move_id.move_type == "out_refund": - if ( - not line.is_downpayment - or line.untaxed_amount_to_invoice == 0 - ): - qty_invoiced -= ( - invoice_line.product_uom_id._compute_quantity( - invoice_line.quantity, line.product_id.uom_id - ) - ) - line.qty_invoiced = qty_invoiced - - @api.depends("product_qty", "qty_to_invoice", "qty_invoiced") + @api.depends( + "sale_line_ids", + "sale_line_ids.invoice_status", + ) def _compute_invoice_status(self): """ - Compute the invoice status of a SO line. Possible statuses: - - no: if the SO is not in status 'sale' or 'done', - we consider that there is nothing to invoice. - This is also hte default value if the conditions of no other - status is met. - - to invoice: we refer to the quantity to invoice of the line. - Refer to method `_compute_get_to_invoice_qty()` for more information on - how this quantity is calculated. - - upselling: this is possible only for a product invoiced on ordered - quantities for which we delivered more than expected. - The could arise if, for example, a project took more time than - expected but we decided not to invoice the extra cost to the - client. This occurs onyl in state 'sale', so that when a Folio - is set to done, the upselling opportunity is removed from the list. - - invoiced: the quantity invoiced is larger or equal to the - quantity ordered. + Compute the invoice status of a Reservation. Possible statuses: + Base on folio sale line invoice status """ - precision = self.env["decimal.precision"].precision_get( - "Product Unit of Measure" - ) for line in self: - state = line.folio_id.state or "draft" - if state == "draft": - line.invoice_status = "no" - elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): - line.invoice_status = "to invoice" - elif ( - float_compare( - line.qty_invoiced, line.product_qty, precision_digits=precision - ) - >= 0 - ): - line.invoice_status = "invoiced" + states = list(set(line.sale_line_ids.mapped("invoice_status"))) + if len(states) == 1: + line.invoice_status = states[0] + elif len(states) >= 1: + if "to_invoice" in states: + line.invoice_status = "to_invoice" + elif "invoiced" in states: + line.invoice_status = "invoiced" + else: + line.invoice_status = "no" else: line.invoice_status = "no" - @api.depends("product_qty", "discount", "price_unit", "tax_ids") + @api.depends("service_line_ids.price_day_total") def _compute_amount_service(self): for service in self: - folio = service.folio_id - reservation = service.reservation_id - currency = folio.currency_id if folio else reservation.currency_id - product = service.product_id - price = service.price_unit * (1 - (service.discount or 0.0) * 0.01) - taxes = service.tax_ids.compute_all( - price, currency, service.product_qty, product=product - ) - service.update( - { - "price_tax": sum( - t.get("amount", 0.0) for t in taxes.get("taxes", []) - ), - "price_total": taxes["total_included"], - "price_subtotal": taxes["total_excluded"], - } - ) + if service.service_line_ids: + service.update( + { + "price_tax": sum( + service.service_line_ids.mapped("price_day_tax") + ), + "price_total": sum( + service.service_line_ids.mapped("price_day_total") + ), + "price_subtotal": sum( + service.service_line_ids.mapped("price_day_subtotal") + ), + } + ) + else: + service.update( + { + "price_tax": 0, + "price_total": 0, + "price_subtotal": 0, + } + ) # Action methods def open_service_ids(self): @@ -537,20 +367,12 @@ class PmsService(models.Model): reservation = self.reservation_id origin = folio if folio else reservation if origin.pricelist_id.discount_policy == "with_discount": - return product.with_context(pricelist=origin.pricelist_id.id).price - product_context = dict( - self.env.context, - partner_id=origin.partner_id.id, - date=folio.date_order if folio else fields.Date.today(), - uom=self.product_id.uom_id.id, - ) + return product.price final_price, rule_id = origin.pricelist_id.with_context( - product_context - ).get_product_price_rule( - self.product_id, self.product_qty or 1.0, origin.partner_id - ) + product._context + ).get_product_price_rule(product, self.product_qty or 1.0, origin.partner_id) base_price, currency_id = self.with_context( - product_context + product._context )._get_real_price_currency( product, rule_id, @@ -562,12 +384,74 @@ class PmsService(models.Model): base_price = ( self.env["res.currency"] .browse(currency_id) - .with_context(product_context) + .with_context(product._context) .compute(base_price, origin.pricelist_id.currency_id) ) # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) + def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id): + """Retrieve the price before applying the pricelist + :param obj product: object of current product record + :parem float qty: total quantity of product + :param tuple price_and_rule: tuple(price, suitable_rule) + coming from pricelist computation + :param obj uom: unit of measure of current order line + :param integer pricelist_id: pricelist id of sales order""" + PricelistItem = self.env["product.pricelist.item"] + field_name = "lst_price" + currency_id = None + product_currency = product.currency_id + if rule_id: + pricelist_item = PricelistItem.browse(rule_id) + if pricelist_item.pricelist_id.discount_policy == "without_discount": + while ( + pricelist_item.base == "pricelist" + and pricelist_item.base_pricelist_id + and pricelist_item.base_pricelist_id.discount_policy + == "without_discount" + ): + price, rule_id = pricelist_item.base_pricelist_id.with_context( + uom=uom.id + ).get_product_price_rule(product, qty, self.order_id.partner_id) + pricelist_item = PricelistItem.browse(rule_id) + + if pricelist_item.base == "standard_price": + field_name = "standard_price" + product_currency = product.cost_currency_id + elif ( + pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id + ): + field_name = "price" + product = product.with_context( + pricelist=pricelist_item.base_pricelist_id.id + ) + product_currency = pricelist_item.base_pricelist_id.currency_id + currency_id = pricelist_item.pricelist_id.currency_id + + if not currency_id: + currency_id = product_currency + cur_factor = 1.0 + else: + if currency_id.id == product_currency.id: + cur_factor = 1.0 + else: + cur_factor = currency_id._get_conversion_rate( + product_currency, + currency_id, + self.company_id or self.env.company, + self.folio_id.date_order or fields.Date.today(), + ) + + product_uom = self.env.context.get("uom") or product.uom_id.id + if uom and uom.id != product_uom: + # the unit price is in a different uom + uom_factor = uom._compute_price(1.0, product.uom_id) + else: + uom_factor = 1.0 + + return product[field_name] * uom_factor * cur_factor, currency_id + # Businness Methods def _service_day_qty(self): self.ensure_one() @@ -578,3 +462,38 @@ class PmsService(models.Model): if self.product_id.per_person: qty = self.reservation_id.adults return qty + + def _get_price_unit_line(self, date=False): + self.ensure_one() + folio = self.folio_id + reservation = self.reservation_id + origin = reservation if reservation else folio + if origin: + partner = origin.partner_id + pricelist = origin.pricelist_id + board_room_type = False + product_context = dict( + self.env.context, + lang=partner.lang, + partner=partner.id, + quantity=self.product_qty, + date=folio.date_order if folio else fields.Date.today(), + pricelist=pricelist.id, + board_service=board_room_type.id if board_room_type else False, + uom=self.product_id.uom_id.id, + fiscal_position=False, + property=self.pms_property_id.id, + ) + if date: + product_context["date_overnight"] = date + if reservation and self.is_board_service: + product_context["board_service"] = reservation.board_service_room_id.id + product = self.product_id.with_context(product_context) + return self.env["account.tax"]._fix_tax_included_price_company( + self._get_display_price(product), + product.taxes_id, + self.tax_ids, + origin.company_id, + ) + else: + return 0 diff --git a/pms/models/pms_service_line.py b/pms/models/pms_service_line.py index ab346eb68..ab74c30d2 100644 --- a/pms/models/pms_service_line.py +++ b/pms/models/pms_service_line.py @@ -29,35 +29,144 @@ class PmsServiceLine(models.Model): ) date = fields.Date("Date") day_qty = fields.Integer("Units") - price_total = fields.Float( - "Price Total", compute="_compute_price_total", store=True - ) price_unit = fields.Float( - "Unit Price", related="service_id.price_unit", readonly=True, store=True + "Unit Price", + digits=("Product Price"), + ) + price_day_subtotal = fields.Monetary( + string="Subtotal", + readonly=True, + store=True, + compute="_compute_day_amount_service", + ) + price_day_total = fields.Monetary( + string="Total", readonly=True, store=True, compute="_compute_day_amount_service" + ) + price_day_tax = fields.Float( + string="Taxes Amount", + readonly=True, + store=True, + compute="_compute_day_amount_service", + ) + currency_id = fields.Many2one( + related="service_id.currency_id", store=True, string="Currency", readonly=True ) room_id = fields.Many2one( string="Room", related="service_id.reservation_id", readonly=True, store=True ) discount = fields.Float( - "Discount", related="service_id.discount", readonly=True, store=True + string="Discount (%)", + digits=("Discount"), + default=0.0, + compute="_compute_discount", + readonly=False, + store=True, ) cancel_discount = fields.Float( "Cancelation Discount", compute="_compute_cancel_discount" ) - # Compute and Search methods - @api.depends("day_qty", "service_id.price_total") - def _compute_price_total(self): + @api.depends("day_qty", "discount", "price_unit", "tax_ids") + def _compute_day_amount_service(self): + for line in self: + amount_service = line.price_unit + if amount_service > 0: + currency = line.service_id.currency_id + product = line.product_id + price = amount_service * (1 - (line.discount or 0.0) * 0.01) + # REVIEW: line.day_qty is not the total qty (the total is on service_id) + taxes = line.tax_ids.compute_all( + price, currency, line.day_qty, product=product + ) + line.update( + { + "price_day_tax": sum( + t.get("amount", 0.0) for t in taxes.get("taxes", []) + ), + "price_day_total": taxes["total_included"], + "price_day_subtotal": taxes["total_excluded"], + } + ) + else: + line.update( + { + "price_day_tax": 0, + "price_day_total": 0, + "price_day_subtotal": 0, + } + ) + + @api.depends("service_id.reservation_id", "service_id.reservation_id.discount") + def _compute_discount(self): """ - Used to reports + On board service the line discount is always + equal to reservation line discount """ for record in self: - if record.service_id.product_qty != 0: - record.price_total = ( - record.service_id.price_total * record.day_qty - ) / record.service_id.product_qty - else: - record.price_total = 0 + if record.is_board_service: + record.discount = ( + record.service_id.reservation_id.reservation_line_ids.filtered( + lambda l: l.date == record.date + ).discount + ) + elif not record.discount: + record.discount = 0 + + # TODO: Refact method and allowed cancelled single days + @api.depends("service_id.reservation_id.cancelled_reason") + def _compute_cancel_discount(self): + for line in self: + line.cancel_discount = 0 + # TODO: Review cancel logic + # reservation = line.reservation_id.reservation_id + # pricelist = reservation.pricelist_id + # if reservation.state == "cancelled": + # if ( + # reservation.cancelled_reason + # and pricelist + # and pricelist.cancelation_rule_id + # ): + # date_start_dt = fields.Date.from_string( + # reservation.checkin + # ) + # date_end_dt = fields.Date.from_string( + # reservation.checkout + # ) + # days = abs((date_end_dt - date_start_dt).days) + # rule = pricelist.cancelation_rule_id + # if reservation.cancelled_reason == "late": + # discount = 100 - rule.penalty_late + # if rule.apply_on_late == "first": + # days = 1 + # elif rule.apply_on_late == "days": + # days = rule.days_late + # elif reservation.cancelled_reason == "noshow": + # discount = 100 - rule.penalty_noshow + # if rule.apply_on_noshow == "first": + # days = 1 + # elif rule.apply_on_noshow == "days": + # days = rule.days_late - 1 + # elif reservation.cancelled_reason == "intime": + # discount = 100 + + # checkin = reservation.checkin + # dates = [] + # for i in range(0, days): + # dates.append( + # ( + # fields.Date.from_string(checkin) + timedelta(days=i) + # ).strftime(DEFAULT_SERVER_DATE_FORMAT) + # ) + # reservation.reservation_line_ids.filtered( + # lambda r: r.date in dates + # ).update({"cancel_discount": discount}) + # reservation.reservation_line_ids.filtered( + # lambda r: r.date not in dates + # ).update({"cancel_discount": 100}) + # else: + # reservation.reservation_line_ids.update({"cancel_discount": 0}) + # else: + # reservation.reservation_line_ids.update({"cancel_discount": 0}) # Constraints and onchanges @api.constrains("day_qty") diff --git a/pms/models/product_pricelist.py b/pms/models/product_pricelist.py index 97c38a9f8..7dc097a1b 100644 --- a/pms/models/product_pricelist.py +++ b/pms/models/product_pricelist.py @@ -80,15 +80,16 @@ class ProductPricelist(models.Model): def _compute_price_rule_get_items( self, products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids ): - if ( "property" in self._context and self._context["property"] - and "date_overnight" in self._context + and self._context.get("date_overnight") ): - self.env["product.pricelist.item"].flush( - ["price", "currency_id", "company_id"] - ) + # board_service_id = self._context.get("board_service") + # on_board_service_bool = True if board_service_id else False + # self.env["product.pricelist.item"].flush( + # ["price", "currency_id", "company_id"] + # ) self.env.cr.execute( """ SELECT item.id @@ -99,6 +100,8 @@ class ProductPricelist(models.Model): ON item.pricelist_id = cab.product_pricelist_id LEFT JOIN pms_property_product_pricelist_item_rel lin ON item.id = lin.product_pricelist_item_id + LEFT JOIN board_service_pricelist_item_rel board + ON item.id = board.pricelist_item_id WHERE (lin.pms_property_id = %s OR lin.pms_property_id IS NULL) AND (cab.pms_property_id = %s OR cab.pms_property_id IS NULL) AND (item.product_tmpl_id IS NULL @@ -132,6 +135,8 @@ class ProductPricelist(models.Model): prod_tmpl_ids, prod_ids, categ_ids, + # on_board_service_bool, + # board_service_id, self.id, date, date, diff --git a/pms/models/product_pricelist_item.py b/pms/models/product_pricelist_item.py index ea1738655..77f5f007d 100644 --- a/pms/models/product_pricelist_item.py +++ b/pms/models/product_pricelist_item.py @@ -18,6 +18,17 @@ class ProductPricelistItem(models.Model): string="End Date Overnight", help="End date to apply daily pricelist items", ) + on_board_service = fields.Boolean("Those included in Board Services") + board_service_room_type_ids = fields.Many2many( + "pms.board.service.room.type", + "board_service_pricelist_item_rel", + "pricelist_item_id", + "board_service_id", + string="Board Services on Room Types", + ondelete="cascade", # check_company=True, + help="""Specify a Board services on Room Types.""", + # domain="[('pms_property_ids', 'in', [allowed_property_ids, False])]", + ) allowed_property_ids = fields.Many2many( "pms.property", diff --git a/pms/models/product_product.py b/pms/models/product_product.py index c87902e0b..282ed6921 100644 --- a/pms/models/product_product.py +++ b/pms/models/product_product.py @@ -1,9 +1,16 @@ -from odoo import api, models +from odoo import api, fields, models class ProductProduct(models.Model): _inherit = "product.product" + board_price = fields.Float( + "Board Service Price", + digits="Product Price", + compute="_compute_board_price", + help="Get price price on board service", + ) + @api.depends_context( "pricelist", "partner", @@ -15,3 +22,30 @@ class ProductProduct(models.Model): ) def _compute_product_price(self): super(ProductProduct, self)._compute_product_price() + + def _compute_board_price(self): + for record in self: + if self._context.get("board_service"): + record.board_price = ( + self.env["pms.board.service.room.type.line"] + .search( + [ + ( + "pms_board_service_room_type_id", + "=", + self._context.get("board_service"), + ), + ("product_id", "=", record.id), + ] + ) + .amount + ) + else: + record.board_price = False + + def price_compute(self, price_type, uom=False, currency=False, company=None): + if self._context.get("board_service"): + price_type = "board_price" + return super(ProductProduct, self).price_compute( + price_type, uom, currency, company + ) diff --git a/pms/report/pms_folio_templates.xml b/pms/report/pms_folio_templates.xml index e110e8955..48f3e4146 100644 --- a/pms/report/pms_folio_templates.xml +++ b/pms/report/pms_folio_templates.xml @@ -133,7 +133,7 @@ diff --git a/pms/security/ir.model.access.csv b/pms/security/ir.model.access.csv index 23c05a463..a88072a92 100644 --- a/pms/security/ir.model.access.csv +++ b/pms/security/ir.model.access.csv @@ -57,8 +57,6 @@ user_access_pms_folio_availability_wizard,user_access_pms_folio_availability_wiz user_access_pms_num_rooms_selection,user_access_pms_num_rooms_selection,model_pms_num_rooms_selection,pms.group_pms_user,1,1,1,1 user_access_pms_folio_sale_line,user_access_pms_folio_sale_line,model_folio_sale_line,pms.group_pms_user,1,0,0,0 user_access_folio_make_invoice_advance,user_access_folio_make_invoice_advance,model_folio_advance_payment_inv,pms.group_pms_user,1,1,1,1 -user_access_pms_invoice_filter_days,user_access_pms_invoice_filter_days,model_pms_invoice_filter_days,pms.group_pms_user,1,1,1,1 -user_access_pms_invoice_filter_days_items,user_access_pms_invoice_filter_days_items,model_pms_invoice_filter_days_items,pms.group_pms_user,1,1,1,1 user_access_wizard_payment_folio,user_access_wizard_payment_folio,model_wizard_payment_folio,pms.group_pms_user,1,1,1,1 user_access_wizard_folio_changes,user_access_wizard_folio_changes,model_wizard_folio_changes,pms.group_pms_user,1,1,1,1 user_access_pms_folio_portal,user_access_pms_folio_portal,model_pms_folio,base.group_portal,1,0,0,0 diff --git a/pms/tests/test_pms_board_service_room_type.py b/pms/tests/test_pms_board_service_room_type.py index b598c1aae..f417eded5 100644 --- a/pms/tests/test_pms_board_service_room_type.py +++ b/pms/tests/test_pms_board_service_room_type.py @@ -1,5 +1,3 @@ -from odoo.exceptions import ValidationError - from .common import TestHotel @@ -39,32 +37,3 @@ class TestPmsBoardServiceRoomType(TestHotel): "class_id": self.room_type_class.id, } ) - - def test_room_type_property_integrity(self): - self._create_common_scenario() - self.room_type.pms_property_ids = [self.property1.id] - with self.assertRaises(ValidationError): - self.board_service_room_type = self.env[ - "pms.board.service.room.type" - ].create( - { - "pms_board_service_id": self.board_service.id, - "pms_room_type_id": self.room_type.id, - "pms_property_ids": self.property2, - } - ) - - def test_pricelist_property_integrity(self): - self._create_common_scenario() - self.pricelist = self.env["product.pricelist"].create( - {"name": "pricelist_1", "pms_property_ids": [self.property1.id]} - ) - with self.assertRaises(ValidationError): - self.env["pms.board.service.room.type"].create( - { - "pms_board_service_id": self.board_service.id, - "pms_room_type_id": self.room_type.id, - "pricelist_id": self.pricelist.id, - "pms_property_ids": self.property2, - } - ) diff --git a/pms/tests/test_pms_room_type.py b/pms/tests/test_pms_room_type.py index 6bae3bbfb..d0f0d04a2 100644 --- a/pms/tests/test_pms_room_type.py +++ b/pms/tests/test_pms_room_type.py @@ -726,51 +726,52 @@ class TestRoomTypeCodePropertyUniqueness(TestRoomType): ) r.pms_property_ids = [(4, self.p1.id)] - def test_check_board_service_property_integrity(self): - self.company1 = self.env["res.company"].create( - { - "name": "Pms_Company_Test", - } - ) - self.property1 = self.env["pms.property"].create( - { - "name": "Pms_property_test1", - "company_id": self.company1.id, - "default_pricelist_id": self.env.ref("product.list0").id, - } - ) - self.property2 = self.env["pms.property"].create( - { - "name": "Pms_property_test2", - "company_id": self.company1.id, - "default_pricelist_id": self.env.ref("product.list0").id, - } - ) - self.room_type_class = self.env["pms.room.type.class"].create( - {"name": "Room Type Class", "code_class": "SIN1"} - ) - self.room_type = self.env["pms.room.type"].create( - { - "name": "Room Type", - "code_type": "Type1", - "pms_property_ids": self.property2, - "class_id": self.room_type_class.id, - } - ) - self.board_service = self.env["pms.board.service"].create( - { - "name": "Board Service", - } - ) - with self.assertRaises(ValidationError): - self.env["pms.board.service.room.type"].create( - { - "pms_board_service_id": self.board_service.id, - "pms_room_type_id": self.room_type.id, - "pricelist_id": self.env.ref("product.list0").id, - "pms_property_ids": self.property2, - } - ) + # TODO: pending multi property PR + # def test_check_board_service_property_integrity(self): + # self.company1 = self.env["res.company"].create( + # { + # "name": "Pms_Company_Test", + # } + # ) + # self.property1 = self.env["pms.property"].create( + # { + # "name": "Pms_property_test1", + # "company_id": self.company1.id, + # "default_pricelist_id": self.env.ref("product.list0").id, + # } + # ) + # self.property2 = self.env["pms.property"].create( + # { + # "name": "Pms_property_test2", + # "company_id": self.company1.id, + # "default_pricelist_id": self.env.ref("product.list0").id, + # } + # ) + # self.room_type_class = self.env["pms.room.type.class"].create( + # {"name": "Room Type Class", "code_class": "SIN1"} + # ) + # self.room_type = self.env["pms.room.type"].create( + # { + # "name": "Room Type", + # "code_type": "Type1", + # "pms_property_ids": self.property2, + # "class_id": self.room_type_class.id, + # } + # ) + # self.board_service = self.env["pms.board.service"].create( + # { + # "name": "Board Service", + # "pms_property_ids": self.property1, + # } + # ) + # with self.assertRaises(ValidationError): + # self.env["pms.board.service.room.type"].create( + # { + # "pms_board_service_id": self.board_service.id, + # "pms_room_type_id": self.room_type.id, + # "pms_property_ids": self.property2, + # } + # ) def test_check_amenities_property_integrity(self): self.company1 = self.env["res.company"].create( diff --git a/pms/views/account_move_views.xml b/pms/views/account_move_views.xml index 352f82997..7566699d1 100644 --- a/pms/views/account_move_views.xml +++ b/pms/views/account_move_views.xml @@ -6,37 +6,8 @@ - - -