# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.tools import config class AccountMove(models.Model): _inherit = "account.move" pricelist_id = fields.Many2one( comodel_name="product.pricelist", string="Pricelist", compute="_compute_pricelist_id", tracking=True, store=True, precompute=True, readonly=False, ) @api.constrains("pricelist_id", "currency_id") def _check_currency(self): if ( not config["test_enable"] or ( config["test_enable"] and self._context.get("force_check_currecy", False) ) ) and self.filtered( lambda a: a.pricelist_id and a.is_sale_document() and a.pricelist_id.currency_id != a.currency_id ): raise UserError(_("Pricelist and Invoice need to use the same currency.")) @api.depends("partner_id", "company_id") def _compute_pricelist_id(self): for invoice in self: if ( invoice.partner_id and invoice.is_sale_document() and invoice.partner_id.property_product_pricelist ): invoice.pricelist_id = invoice.partner_id.property_product_pricelist @api.depends("pricelist_id") def _compute_currency_id(self): res = super()._compute_currency_id() for invoice in self: if ( invoice.is_sale_document() and invoice.pricelist_id and invoice.currency_id != invoice.pricelist_id.currency_id ): invoice.currency_id = self.pricelist_id.currency_id return res def button_update_prices_from_pricelist(self): self.filtered( lambda r: r.state == "draft" ).invoice_line_ids._compute_price_unit() class AccountMoveLine(models.Model): _inherit = "account.move.line" pricelist_item_id = fields.Many2one( comodel_name="product.pricelist.item", compute="_compute_pricelist_item_id" ) @api.depends("product_id", "product_uom_id", "quantity") def _compute_pricelist_item_id(self): for line in self: if ( not line.product_id or line.display_type != "product" or not line.move_id.pricelist_id ): line.pricelist_item_id = False else: line.pricelist_item_id = line.move_id.pricelist_id._get_product_rule( line.product_id, quantity=line.quantity or 1.0, uom=line.product_uom_id, date=line._get_move_date(), ) def _get_move_date(self): self.ensure_one() return self.move_id.invoice_date def _calculate_discount(self): discount_enabled = self.env[ "product.pricelist.item" ]._is_discount_feature_enabled() for line in self: if not (line.move_id.pricelist_id and discount_enabled): continue line.discount = 0.0 if not line.pricelist_item_id._show_discount(): # No pricelist rule was found for the product # therefore, the pricelist didn't apply any discount/change # to the existing sales price. continue line = line.with_company(line.company_id) pricelist_price = line._get_pricelist_price() base_price = line._get_pricelist_price_before_discount() if base_price != 0: # Avoid division by zero discount = (base_price - pricelist_price) / base_price * 100 if (discount > 0 and base_price > 0) or ( discount < 0 and base_price < 0 ): # only show negative discounts if price is negative # otherwise it's a surcharge which shouldn't be shown # to the customer line.discount = discount @api.depends("quantity") def _compute_price_unit(self): res = super()._compute_price_unit() for line in self: line = line.with_company(line.company_id) if not line.move_id.pricelist_id: continue if not line.product_uom_id or not line.product_id: line.price_unit = 0.0 else: price = line._get_display_price() line.with_context( check_move_validity=False ).price_unit = line.product_id._get_tax_included_unit_price_from_price( price, product_taxes=line.product_id.taxes_id.filtered( lambda tax, line=line: tax.company_id == line.env.company ), fiscal_position=line.move_id.fiscal_position_id, ) return res def _get_display_price(self): """Compute the displayed unit price for a given line. Overridden in custom flows: * where the price is not specified by the pricelist * where the discount is not specified by the pricelist Note: self.ensure_one() """ self.ensure_one() if self.product_id.type == "combo": return 0 # The display price of a combo line should always be 0. return self._get_display_price_ignore_combo() def _get_display_price_ignore_combo(self): """This helper method allows to compute the display price of a SOL, while ignoring combo logic. I.e. this method returns the display price of a SOL as if it were neither a combo line nor a combo item line. """ self.ensure_one() pricelist_price = self._get_pricelist_price() if not self.pricelist_item_id._show_discount(): # No pricelist rule found => no discount from pricelist return pricelist_price base_price = self._get_pricelist_price_before_discount() self._calculate_discount() # negative discounts (= surcharge) are included in the display price return max(base_price, pricelist_price) def _get_pricelist_price(self): """Compute the price given by the pricelist for the given line information. :return: the product price in the move currency (without taxes) :rtype: float """ self.ensure_one() self.product_id.ensure_one() price = self.pricelist_item_id._compute_price( product=self.product_id, quantity=self.quantity or 1.0, uom=self.product_uom_id, date=self._get_move_date(), currency=self.currency_id, ) return price def _get_pricelist_price_before_discount(self): """Compute the price used as base for the pricelist price computation. :return: the product price in the move currency (without taxes) :rtype: float """ self.ensure_one() self.product_id.ensure_one() return self.pricelist_item_id._compute_price_before_discount( product=self.product_id, quantity=self.quantity or 1.0, uom=self.product_uom_id, date=self._get_move_date(), currency=self.currency_id, )