From d03ff8f1de05f555388b1f0a497afd374795563e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 21 Aug 2023 15:34:26 +0200 Subject: [PATCH] Deduce sold shipment cost from shipment once sale is shipped When the shipment cost method is by order, the sold shipment cost is dispatched between shipments once the sale is shipped. The amount is stored on the shipment for each sale (in case the shipments have been grouped) and deduced from the shipment cost when it is set on the stock moves. Closes #12506 --- modules/carrier_carriage/__init__.py | 1 + modules/carrier_carriage/sale.py | 7 ++++ modules/carrier_carriage/stock.py | 9 +++++ modules/sale_shipment_cost/__init__.py | 1 + modules/sale_shipment_cost/sale.py | 55 ++++++++++++++++++++++++++ modules/sale_shipment_cost/stock.py | 48 ++++++++++++---------- 6 files changed, 100 insertions(+), 21 deletions(-) diff --git a/modules/carrier_carriage/__init__.py b/modules/carrier_carriage/__init__.py index 5c2c8eef28..46c0820717 100644 --- a/modules/carrier_carriage/__init__.py +++ b/modules/carrier_carriage/__init__.py @@ -18,6 +18,7 @@ def register(): sale.Sale, sale.Line, sale.Carriage, + stock.ShipmentCostSale, stock.Carriage_Sale, account.InvoiceLine, module='carrier_carriage', type_='model', diff --git a/modules/carrier_carriage/sale.py b/modules/carrier_carriage/sale.py index 139954856a..83aee852c4 100644 --- a/modules/carrier_carriage/sale.py +++ b/modules/carrier_carriage/sale.py @@ -98,6 +98,13 @@ class Sale(metaclass=PoolMeta): default.setdefault('after_carriages') return super().copy(sales, default=default) + @property + def _cost_shipments(self): + shipments = super()._cost_shipments + for shipment in self.shipments: + shipments.extend(shipment.carriages) + return shipments + class Line(metaclass=PoolMeta): __name__ = 'sale.line' diff --git a/modules/carrier_carriage/stock.py b/modules/carrier_carriage/stock.py index 27607a9f8b..27584de782 100644 --- a/modules/carrier_carriage/stock.py +++ b/modules/carrier_carriage/stock.py @@ -136,6 +136,15 @@ class _ShipmentMixin: return super().copy(shipments, default=default) +class ShipmentCostSale(metaclass=PoolMeta): + __name__ = 'stock.shipment.cost_sale' + + @classmethod + def __setup__(cls): + super().__setup__() + cls.shipment.selection.append(('stock.shipment.carriage', "Carriage")) + + class Carriage_Sale(ShipmentCostSaleMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.carriage' diff --git a/modules/sale_shipment_cost/__init__.py b/modules/sale_shipment_cost/__init__.py index 6c2f1cd6e1..184f092bc8 100644 --- a/modules/sale_shipment_cost/__init__.py +++ b/modules/sale_shipment_cost/__init__.py @@ -18,6 +18,7 @@ def register(): sale.ConfigurationSaleMethod, sale.Sale, sale.Line, + stock.ShipmentCostSale, stock.ShipmentOut, module='sale_shipment_cost', type_='model') Pool.register( diff --git a/modules/sale_shipment_cost/sale.py b/modules/sale_shipment_cost/sale.py index 4a78897a29..a5de846c56 100644 --- a/modules/sale_shipment_cost/sale.py +++ b/modules/sale_shipment_cost/sale.py @@ -85,6 +85,8 @@ class Sale(metaclass=PoolMeta): states={ 'readonly': Eval('state') != 'draft', }) + shipment_costs = fields.One2Many( + 'stock.shipment.cost_sale', 'sale', "Shipment Costs", readonly=True) @classmethod def __register__(cls, module): @@ -227,6 +229,59 @@ class Sale(metaclass=PoolMeta): with Transaction().set_context(_shipment_cost_invoiced=list()): super().process(sales) + @classmethod + def _process_invoice_shipment_states(cls, sales): + pool = Pool() + ShipmentOut = pool.get('stock.shipment.out') + ShipmentCostSale = pool.get('stock.shipment.cost_sale') + + sent, not_sent = [], [] + for sale in sales: + if sale.shipment_cost_method == 'order': + if sale.shipment_state == 'sent': + sent.append(sale) + else: + not_sent.append(sale) + + super()._process_invoice_shipment_states(sales) + + to_save, to_delete, shipments = [], [], set() + for sale in sent: + if sale.shipment_state != 'sent': + to_delete.extend(sale.shipment_costs) + shipments.update(sale.shipments) + for sale in not_sent: + if sale.shipment_state == 'sent': + to_save.extend(sale._get_shipment_costs()) + shipments.update(sale.shipments) + + ShipmentCostSale.delete(to_delete) + ShipmentCostSale.save(to_save) + ShipmentOut.set_shipment_cost(shipments) + + @property + def _cost_shipments(self): + "Return the shipments to apply cost sale" + return list(self.shipments) + + def _get_shipment_costs(self): + "Yield shipment costs" + pool = Pool() + ShipmentCostSale = pool.get('stock.shipment.cost_sale') + cost = self.shipment_cost_amount + shipments = self._cost_shipments + sum_ = sum(s.cost_used for s in shipments) + for shipment in shipments: + if sum_: + factor = shipment.cost_used / sum_ + else: + factor = Decimal(1) / len(shipments) + yield ShipmentCostSale( + shipment=shipment, + sale=self, + amount=round_price(cost * factor), + currency=self.currency) + def _get_carrier_context(self, carrier): return {} diff --git a/modules/sale_shipment_cost/stock.py b/modules/sale_shipment_cost/stock.py index f2f85467a2..d985e6ee95 100644 --- a/modules/sale_shipment_cost/stock.py +++ b/modules/sale_shipment_cost/stock.py @@ -1,7 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from trytond.i18n import gettext -from trytond.model import ModelView, Workflow, fields +from trytond.model import ModelSQL, ModelView, Workflow, fields from trytond.modules.product import price_digits, round_price from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval @@ -44,6 +44,9 @@ class ShipmentCostSaleMixin: 'invisible': Eval('cost_method') != 'shipment', }) + cost_sales = fields.One2Many( + 'stock.shipment.cost_sale', 'shipment', "Cost Sales", readonly=True) + cost_invoice_line = fields.Many2One( 'account.invoice.line', "Cost Invoice Line", readonly=True) cost_method = fields.Selection( @@ -135,15 +138,32 @@ class ShipmentCostSaleMixin: pool = Pool() Currency = pool.get('currency.currency') cost = super()._get_shipment_cost() - if self.cost_method == 'shipment': - if self.cost_sale: - cost_sale = Currency.compute( - self.cost_sale_currency, self.cost_sale, - self.company.currency, round=False) - cost -= cost_sale + if self.cost_sale: + cost_sale = Currency.compute( + self.cost_sale_currency, self.cost_sale, + self.company.currency, round=False) + cost -= cost_sale + for line in self.cost_sales: + cost_sale = Currency.compute( + line.currency, line.amount, + self.company.currency, round=False) + cost -= cost_sale return cost +class ShipmentCostSale(ModelSQL): + "Shipment Cost Sale" + __name__ = 'stock.shipment.cost_sale' + + shipment = fields.Reference( + "Shipment", [ + ('stock.shipment.out', "Customer"), + ], required=True) + sale = fields.Many2One('sale.sale', "Sale", required=True) + amount = fields.Numeric("Amount", digits=price_digits, required=True) + currency = fields.Many2One('currency.currency', "Currency", required=True) + + class ShipmentOut(ShipmentCostSaleMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.out' @@ -175,20 +195,6 @@ class ShipmentOut(ShipmentCostSaleMixin, metaclass=PoolMeta): def on_change_with_shipment_cost_sale_readonly(self, name=None): return self.state in {'done', 'cancelled'} - def _get_shipment_cost(self): - cost = super()._get_shipment_cost() - if self.cost_method == 'order': - sales = { - m.sale for m in self.outgoing_moves - if m.sale and m.sale.shipment_cost_method == 'order'} - for sale in sales: - shipment_cost = sum( - (s.cost_used or 0) for s in sale.shipments - if s.state == 'done' and s != self) - cost_sale = sale.shipment_cost_amount - cost -= max(cost_sale - shipment_cost, 0) - return cost - def get_cost_invoice_line(self, invoice): invoice_line = super().get_cost_invoice_line(invoice) if invoice_line: