From 1b1e5558f960e0c0dc7ebf764ea8a0160e7e316f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Tue, 13 Mar 2018 14:14:18 +0100 Subject: [PATCH] Handle remaining qty when synchronizing supplier and customer moves issue6833 review39781002 --- stock.py | 83 +++++++++-- ...ale_supply_drop_shipment_remaining_qty.rst | 129 ++++++++++++++++++ tests/test_sale_supply_drop_shipment.py | 5 + 3 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 tests/scenario_sale_supply_drop_shipment_remaining_qty.rst diff --git a/stock.py b/stock.py index 88fb3e6b1e..96b4cb0085 100644 --- a/stock.py +++ b/stock.py @@ -445,37 +445,90 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView): for shipment in shipments: product_qty = defaultdict(lambda: 0) product_cost = defaultdict(lambda: 0) + + for c_move in shipment.customer_moves: + if c_move.state == 'cancel': + continue + product_qty[c_move.product] += UoM.compute_qty( + c_move.uom, c_move.quantity, c_move.product.default_uom, + round=False) + for s_move in shipment.supplier_moves: if s_move.state == 'cancel': continue - product_qty[s_move.product] += UoM.compute_qty(s_move.uom, - s_move.quantity, s_move.product.default_uom, round=False) if s_move.cost_price: internal_quantity = Decimal(str(s_move.internal_quantity)) product_cost[s_move.product] += ( s_move.unit_price * internal_quantity) + + quantity = UoM.compute_qty( + s_move.uom, s_move.quantity, s_move.product.default_uom, + round=False) + if product_qty[s_move.product]: + if quantity <= product_qty[s_move.product]: + product_qty[s_move.product] -= quantity + continue + else: + out_quantity = ( + quantity - product_qty[s_move.product]) + out_quantity = UoM.compute_qty( + s_move.product.default_uom, out_quantity, + s_move.uom) + product_qty[s_move.product] = 0 + else: + out_quantity = s_move.quantity + + if not out_quantity: + continue + unit_price = UoM.compute_price( + s_move.product.default_uom, s_move.product.list_price, + s_move.uom) + new_customer_move = shipment._get_customer_move(s_move) + new_customer_move.quantity = out_quantity + new_customer_move.unit_price = unit_price + to_save.append(new_customer_move) + for product, cost in product_cost.iteritems(): qty = Decimal(str(product_qty[product])) if qty: product_cost[product] = (cost / qty).quantize(cost_exp) - for c_move in shipment.customer_moves: - if c_move.state == 'cancel': + for c_move in list(shipment.customer_moves) + to_save: + if c_move.id is not None and c_move.state == 'cancel': continue - if product_qty[c_move.product] <= 0: - c_move.shipment = None - else: - move_qty = UoM.compute_qty(c_move.uom, c_move.quantity, - c_move.product.default_uom, round=False) - qty = min(product_qty[c_move.product], move_qty) - c_move.quantity = UoM.compute_qty( + c_move.cost_price = product_cost[c_move.product] + if c_move.id is None: + continue + if product_qty[c_move.product] > 0: + exc_qty = UoM.compute_qty( c_move.product.default_uom, - qty, c_move.uom) - product_qty[c_move.product] -= qty - c_move.cost_price = product_cost[c_move.product] - to_save.append(c_move) + product_qty[c_move.product], c_move.uom) + removed_qty = UoM.compute_qty( + c_move.uom, min(exc_qty, c_move.quantity), + c_move.product.default_uom, round=False) + c_move.quantity = max( + 0, c_move.uom.round(c_move.quantity - exc_qty)) + product_qty[c_move.product] -= removed_qty + to_save.append(c_move) + if to_save: Move.save(to_save) + def _get_customer_move(self, move): + pool = Pool() + Move = pool.get('stock.move') + return Move( + from_location=move.to_location, + to_location=self.customer.customer_location, + product=move.product, + uom=move.uom, + quantity=move.quantity, + shipment=self, + planned_date=self.planned_date, + company=move.company, + currency=move.company.currency, + unit_price=move.unit_price, + ) + @classmethod @ModelView.button @Workflow.transition('waiting') diff --git a/tests/scenario_sale_supply_drop_shipment_remaining_qty.rst b/tests/scenario_sale_supply_drop_shipment_remaining_qty.rst new file mode 100644 index 0000000000..2fd24eb227 --- /dev/null +++ b/tests/scenario_sale_supply_drop_shipment_remaining_qty.rst @@ -0,0 +1,129 @@ +================================== +Sale Supply Drop Shipment Scenario +================================== + +Imports:: + + >>> import datetime + >>> from dateutil.relativedelta import relativedelta + >>> from decimal import Decimal + >>> from proteus import Model, Wizard + >>> from trytond.tests.tools import activate_modules + >>> from trytond.modules.company.tests.tools import create_company, \ + ... get_company + >>> from trytond.modules.account.tests.tools import create_fiscalyear, \ + ... create_chart, get_accounts + >>> from trytond.modules.account_invoice.tests.tools import \ + ... set_fiscalyear_invoice_sequences, create_payment_term + >>> today = datetime.date.today() + +Install sale_supply_drop_shipment, sale, purchase:: + + >>> config = activate_modules([ + ... 'sale_supply_drop_shipment', + ... 'sale', + ... 'purchase', + ... ]) + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create fiscal year:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences( + ... create_fiscalyear(company)) + >>> fiscalyear.click('create_period') + +Create chart of accounts:: + + >>> _ = create_chart(company) + >>> accounts = get_accounts(company) + >>> revenue = accounts['revenue'] + >>> expense = accounts['expense'] + +Create parties:: + + >>> Party = Model.get('party.party') + >>> supplier = Party(name='Supplier') + >>> supplier.save() + >>> customer = Party(name='Customer') + >>> customer.save() + +Create product:: + + >>> ProductUom = Model.get('product.uom') + >>> ProductSupplier = Model.get('purchase.product_supplier') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> ProductTemplate = Model.get('product.template') + + >>> template = ProductTemplate() + >>> template.name = 'product' + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.purchasable = True + >>> template.salable = True + >>> template.list_price = Decimal('10') + >>> template.account_expense = expense + >>> template.account_revenue = revenue + >>> template.supply_on_sale = True + >>> template.save() + >>> product, = template.products + >>> product_supplier = ProductSupplier() + >>> product_supplier.product = template + >>> product_supplier.party = supplier + >>> product_supplier.drop_shipment = True + >>> product_supplier.lead_time = datetime.timedelta(0) + >>> product_supplier.save() + +Create payment term:: + + >>> payment_term = create_payment_term() + >>> payment_term.save() + +Sale 250 products:: + + >>> Sale = Model.get('sale.sale') + >>> sale = Sale() + >>> sale.party = customer + >>> sale.payment_term = payment_term + >>> sale_line = sale.lines.new() + >>> sale_line.product = product + >>> sale_line.quantity = 250 + >>> sale.click('quote') + >>> sale.click('confirm') + >>> sale.click('process') + +Create Purchase from Request:: + + >>> Purchase = Model.get('purchase.purchase') + >>> PurchaseRequest = Model.get('purchase.request') + >>> purchase_request, = PurchaseRequest.find() + >>> purchase_request.quantity + 250.0 + >>> create_purchase = Wizard('purchase.request.create_purchase', + ... [purchase_request]) + >>> purchase, = Purchase.find() + >>> purchase.payment_term = payment_term + >>> purchase.click('quote') + >>> purchase.click('confirm') + >>> purchase.click('process') + >>> purchase.state + u'processing' + + >>> sale.reload() + >>> shipment, = sale.drop_shipments + +The supplier sends more than expected:: + + >>> move, = shipment.supplier_moves + >>> move.quantity = 300 + >>> shipment.click('ship') + +Another move has been created to synchronize supplier and customer quantities:: + + >>> len(shipment.customer_moves) + 2 + >>> sum(m.quantity for m in shipment.customer_moves) + 300.0 diff --git a/tests/test_sale_supply_drop_shipment.py b/tests/test_sale_supply_drop_shipment.py index c06517756b..679339cf43 100644 --- a/tests/test_sale_supply_drop_shipment.py +++ b/tests/test_sale_supply_drop_shipment.py @@ -22,4 +22,9 @@ def suite(): tearDown=doctest_teardown, encoding='utf-8', checker=doctest_checker, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(doctest.DocFileSuite( + 'scenario_sale_supply_drop_shipment_remaining_qty.rst', + tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite