From 89367c74dbdb6b0bd673df9f93f56a30790580fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 26 Mar 2022 12:15:13 +0100 Subject: [PATCH] Recompute cost price of drop shipment Since issue9962 the unit price of the supplier move can be updated later so we must recompute the cost price of the customer moves. issue11295 review364951003 --- CHANGELOG | 1 + __init__.py | 3 +- product.py | 29 ++++++++++++ stock.py | 50 +++++++++++++++----- tests/scenario_sale_supply_drop_shipment.rst | 38 ++++++++++++++- 5 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 product.py diff --git a/CHANGELOG b/CHANGELOG index fc5dfd1bed..c400439cc0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Recompute cost price of drop shipment * Add support for Python 3.10 * Remove support for Python 3.6 diff --git a/__init__.py b/__init__.py index 6cab8e7a1d..6294905539 100644 --- a/__init__.py +++ b/__init__.py @@ -3,7 +3,7 @@ from trytond.pool import Pool -from . import party, purchase, sale, stock +from . import party, product, purchase, sale, stock def register(): @@ -22,6 +22,7 @@ def register(): purchase.Purchase, purchase.Line, purchase.ProductSupplier, + product.Product, module='sale_supply_drop_shipment', type_='model') Pool.register( purchase.RequestCreatePurchase, diff --git a/product.py b/product.py new file mode 100644 index 0000000000..fbd774b8fa --- /dev/null +++ b/product.py @@ -0,0 +1,29 @@ +# 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.pool import Pool, PoolMeta +from trytond.tools import grouped_slice + + +class Product(metaclass=PoolMeta): + __name__ = 'product.product' + + @classmethod + def recompute_cost_price(cls, products, start=None): + pool = Pool() + Move = pool.get('stock.move') + Shipment = pool.get('stock.shipment.drop') + shipments = set() + for sub_products in grouped_slice(products): + domain = [ + ('unit_price_updated', '=', True), + cls._domain_moves_cost(), + ('product', 'in', [p.id for p in sub_products]), + ('shipment', 'like', 'stock.shipment.drop,%'), + ] + if start: + domain.append(('effective_date', '>=', start)) + for move in Move.search(domain, order=[]): + shipments.add(move.shipment) + shipments = Shipment.browse(list(shipments)) + Shipment.set_cost(shipments) + super().recompute_cost_price(products, start=start) diff --git a/stock.py b/stock.py index 2c695afe9d..9f0fa4f81d 100644 --- a/stock.py +++ b/stock.py @@ -443,7 +443,6 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView): to_save = [] for shipment in shipments: product_qty = defaultdict(int) - product_cost = defaultdict(int) for c_move in shipment.customer_moves: if c_move.state == 'cancelled': @@ -456,10 +455,6 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView): for s_move in shipment.supplier_moves: if s_move.state == 'cancelled': continue - internal_quantity = Decimal(str(s_move.internal_quantity)) - product_cost[s_move.product] += ( - s_move.get_cost_price() * internal_quantity) - quantity = UoM.compute_qty( s_move.uom, s_move.quantity, s_move.product.default_uom, round=False) @@ -488,15 +483,8 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView): new_customer_move.unit_price = unit_price to_save.append(new_customer_move) - for product, cost in product_cost.items(): - qty = Decimal(str(s_product_qty[product])) - if qty: - product_cost[product] = round_price(cost / qty) for c_move in list(shipment.customer_moves) + to_save: - if c_move.id is not None and c_move.state == 'cancelled': - continue - c_move.cost_price = product_cost[c_move.product] - if c_move.id is None: + if c_move.id is None or c_move.state == 'cancelled': continue if product_qty[c_move.product] > 0: exc_qty = UoM.compute_qty( @@ -527,6 +515,41 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView): company=move.company, ) + @classmethod + def set_cost(cls, shipments): + pool = Pool() + Move = pool.get('stock.move') + UoM = pool.get('product.uom') + + to_save = [] + for shipment in shipments: + product_cost = defaultdict(int) + s_product_qty = defaultdict(int) + for s_move in shipment.supplier_moves: + if s_move.state == 'cancelled': + continue + internal_quantity = Decimal(str(s_move.internal_quantity)) + product_cost[s_move.product] += ( + s_move.get_cost_price() * internal_quantity) + + quantity = UoM.compute_qty( + s_move.uom, s_move.quantity, s_move.product.default_uom, + round=False) + s_product_qty[s_move.product] += quantity + + for product, cost in product_cost.items(): + qty = Decimal(str(s_product_qty[product])) + if qty: + product_cost[product] = round_price(cost / qty) + + for c_move in shipment.customer_moves: + cost_price = product_cost[c_move.product] + if cost_price != c_move.cost_price: + c_move.cost_price = cost_price + to_save.append(c_move) + if to_save: + Move.save(to_save) + @classmethod @ModelView.button @Workflow.transition('waiting') @@ -590,6 +613,7 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView): pool = Pool() Move = pool.get('stock.move') Date = pool.get('ir.date') + cls.set_cost(shipments) Move.do([m for s in shipments for m in s.customer_moves]) for company, shipments in groupby(shipments, key=lambda s: s.company): with Transaction().set_context(company=company.id): diff --git a/tests/scenario_sale_supply_drop_shipment.rst b/tests/scenario_sale_supply_drop_shipment.rst index 143707624c..c7035e8a54 100644 --- a/tests/scenario_sale_supply_drop_shipment.rst +++ b/tests/scenario_sale_supply_drop_shipment.rst @@ -62,10 +62,23 @@ Create stock user:: >>> stock_force_group, = Group.find([ ... ('name', '=', 'Stock Force Assignment'), ... ]) + >>> product_admin_group, = Group.find([ + ... ('name', '=', "Product Administration"), + ... ]) >>> stock_user.groups.append(stock_group) >>> stock_user.groups.append(stock_force_group) + >>> stock_user.groups.append(product_admin_group) >>> stock_user.save() +Create account user:: + + >>> account_user = User() + >>> account_user.name = "Account" + >>> account_user.login = 'account' + >>> account_group, = Group.find([('name', '=', "Account")]) + >>> account_user.groups.append(account_group) + >>> account_user.save() + Create fiscal year:: >>> fiscalyear = set_fiscalyear_invoice_sequences( @@ -190,7 +203,6 @@ Receiving only 100 products:: >>> move.unit_price Decimal('10.0000') >>> move.cost_price - Decimal('3.0000') >>> set_user(sale_user) >>> sale.reload() >>> sale.shipments @@ -204,6 +216,9 @@ Receiving only 100 products:: >>> shipment.click('done') >>> shipment.state 'done' + >>> move, = shipment.customer_moves + >>> move.cost_price + Decimal('3.0000') >>> set_user(sale_user) >>> sale.reload() >>> sale.shipments @@ -251,8 +266,29 @@ As a consequence the sale order is now in exception:: >>> sale.shipment_state 'exception' +Receive purchase invoice at different price:: + + >>> set_user(account_user) + >>> invoice, = purchase.invoices + >>> invoice_line, = invoice.lines + >>> invoice_line.unit_price = Decimal('4.0000') + >>> invoice.invoice_date = today + >>> invoice.click('post') + + >>> set_user(stock_user) + + >>> recompute = Wizard('product.recompute_cost_price', [product]) + >>> recompute.execute('recompute') + + >>> shipment, = [s for s in purchase.drop_shipments + ... if s.state == 'done'] + >>> move, = shipment.customer_moves + >>> move.cost_price + Decimal('4.0000') + Cancelling the workflow on the purchase step:: + >>> set_user(sale_user) >>> sale = Sale() >>> sale.party = customer >>> sale.payment_term = payment_term