|
| 1 | +# Copyright 2021 Comunitea - Omar Castiñeira |
| 2 | +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). |
| 3 | +from odoo import _, api, exceptions, fields, models |
| 4 | + |
| 5 | + |
| 6 | +class PurchaseOrder(models.Model): |
| 7 | + _inherit = "purchase.order" |
| 8 | + |
| 9 | + global_discount_ids = fields.Many2many( |
| 10 | + comodel_name="global.discount", |
| 11 | + string="Purchase Global Discounts", |
| 12 | + domain="[('discount_scope', '=', 'purchase'), " |
| 13 | + "('account_id', '!=', False), '|', " |
| 14 | + "('company_id', '=', company_id), ('company_id', '=', False)]", |
| 15 | + ) |
| 16 | + # HACK: Looks like UI doesn't behave well with Many2many fields and |
| 17 | + # negative groups when the same field is shown. In this case, we want to |
| 18 | + # show the readonly version to any not in the global discount group. |
| 19 | + # TODO: Check if it's fixed in future versions |
| 20 | + global_discount_ids_readonly = fields.Many2many( |
| 21 | + related="global_discount_ids", |
| 22 | + string="Purchase Global Discounts (readonly)", |
| 23 | + readonly=True, |
| 24 | + ) |
| 25 | + amount_global_discount = fields.Monetary( |
| 26 | + string="Total Global Discounts", |
| 27 | + compute="_amount_all", # pylint: disable=C8108 |
| 28 | + currency_field="currency_id", |
| 29 | + compute_sudo=True, # Odoo core fields are storable so compute_sudo is True |
| 30 | + readonly=True, |
| 31 | + store=True, |
| 32 | + ) |
| 33 | + amount_untaxed_before_global_discounts = fields.Monetary( |
| 34 | + string="Amount Untaxed Before Discounts", |
| 35 | + compute="_amount_all", # pylint: disable=C8108 |
| 36 | + currency_field="currency_id", |
| 37 | + compute_sudo=True, # Odoo core fields are storable so compute_sudo is True |
| 38 | + readonly=True, |
| 39 | + store=True, |
| 40 | + ) |
| 41 | + amount_total_before_global_discounts = fields.Monetary( |
| 42 | + string="Amount Total Before Discounts", |
| 43 | + compute="_amount_all", # pylint: disable=C8108 |
| 44 | + currency_field="currency_id", |
| 45 | + compute_sudo=True, # Odoo core fields are storable so compute_sudo is True |
| 46 | + readonly=True, |
| 47 | + store=True, |
| 48 | + ) |
| 49 | + |
| 50 | + @api.model |
| 51 | + def get_discounted_global(self, price=0, discounts=None): |
| 52 | + """Compute discounts successively""" |
| 53 | + discounts = discounts or [] |
| 54 | + if not discounts: |
| 55 | + return price |
| 56 | + discount = discounts.pop(0) |
| 57 | + price *= 1 - (discount / 100) |
| 58 | + return self.get_discounted_global(price, discounts) |
| 59 | + |
| 60 | + def _check_global_discounts_sanity(self): |
| 61 | + """Perform a sanity check for discarding cases that will lead to |
| 62 | + incorrect data in discounts. |
| 63 | + """ |
| 64 | + self.ensure_one() |
| 65 | + if not self.global_discount_ids: |
| 66 | + return True |
| 67 | + taxes_keys = {} |
| 68 | + for line in self.order_line.filtered(lambda l: not l.display_type): |
| 69 | + if not line.taxes_id: |
| 70 | + raise exceptions.UserError( |
| 71 | + _("With global discounts, taxes in lines are required.") |
| 72 | + ) |
| 73 | + for key in taxes_keys: |
| 74 | + if key == line.taxes_id: |
| 75 | + break |
| 76 | + elif key & line.taxes_id: |
| 77 | + raise exceptions.UserError( |
| 78 | + _("Incompatible taxes found for global discounts.") |
| 79 | + ) |
| 80 | + else: |
| 81 | + taxes_keys[line.taxes_id] = True |
| 82 | + |
| 83 | + @api.depends("order_line.price_total", "global_discount_ids") |
| 84 | + def _amount_all(self): |
| 85 | + res = super()._amount_all() |
| 86 | + for order in self: |
| 87 | + order._check_global_discounts_sanity() |
| 88 | + amount_untaxed_before_global_discounts = order.amount_untaxed |
| 89 | + amount_total_before_global_discounts = order.amount_total |
| 90 | + discounts = order.global_discount_ids.mapped("discount") |
| 91 | + amount_discounted_untaxed = amount_discounted_tax = 0 |
| 92 | + for line in order.order_line: |
| 93 | + discounted_subtotal = self.get_discounted_global( |
| 94 | + line.price_subtotal, discounts.copy() |
| 95 | + ) |
| 96 | + amount_discounted_untaxed += discounted_subtotal |
| 97 | + discounted_tax = line.taxes_id.compute_all( |
| 98 | + discounted_subtotal, |
| 99 | + line.order_id.currency_id, |
| 100 | + 1.0, |
| 101 | + product=line.product_id, |
| 102 | + partner=line.order_id.partner_id, |
| 103 | + ) |
| 104 | + amount_discounted_tax += sum( |
| 105 | + t.get("amount", 0.0) for t in discounted_tax.get("taxes", []) |
| 106 | + ) |
| 107 | + order.update( |
| 108 | + { |
| 109 | + "amount_untaxed_before_global_discounts": ( |
| 110 | + amount_untaxed_before_global_discounts |
| 111 | + ), |
| 112 | + "amount_total_before_global_discounts": ( |
| 113 | + amount_total_before_global_discounts |
| 114 | + ), |
| 115 | + "amount_global_discount": ( |
| 116 | + amount_untaxed_before_global_discounts |
| 117 | + - amount_discounted_untaxed |
| 118 | + ), |
| 119 | + "amount_untaxed": amount_discounted_untaxed, |
| 120 | + "amount_tax": amount_discounted_tax, |
| 121 | + "amount_total": (amount_discounted_untaxed + amount_discounted_tax), |
| 122 | + } |
| 123 | + ) |
| 124 | + return res |
| 125 | + |
| 126 | + @api.onchange("partner_id") |
| 127 | + def onchange_partner_id(self): |
| 128 | + res = super().onchange_partner_id() |
| 129 | + self.global_discount_ids = self.partner_id.supplier_global_discount_ids.filtered( |
| 130 | + lambda d: d.company_id == self.company_id |
| 131 | + ) or self.partner_id.commercial_partner_id.supplier_global_discount_ids.filtered( |
| 132 | + lambda d: d.company_id == self.company_id |
| 133 | + ) |
| 134 | + return res |
| 135 | + |
| 136 | + def _prepare_invoice(self): |
| 137 | + invoice_vals = super()._prepare_invoice() |
| 138 | + if self.global_discount_ids: |
| 139 | + invoice_vals.update( |
| 140 | + {"global_discount_ids": [(6, 0, self.global_discount_ids.ids)]} |
| 141 | + ) |
| 142 | + return invoice_vals |
0 commit comments