From 35614d981f2b47d542d20a20a3feb405bdcdb636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 23 Mar 2021 15:33:02 +0100 Subject: [PATCH] Add unsold shipment cost to the outgoing moves Even there is a shipment cost method if the cost sold is lower than the actual shipment cost, the difference must be added to the outgoing moves. issue10149 review335731002 --- sale.py | 10 +- setup.py | 5 +- stock.py | 31 +++- tests/scenario_sale_shipment_cost_stock.rst | 190 ++++++++++++++++++++ tests/test_sale_shipment_cost.py | 5 + 5 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 tests/scenario_sale_shipment_cost_stock.rst diff --git a/sale.py b/sale.py index dc8825e488..eddbe23720 100644 --- a/sale.py +++ b/sale.py @@ -224,7 +224,7 @@ class Sale(metaclass=PoolMeta): unit_price = None lines = list(self.lines or []) for line in self.lines: - if line.type == 'line' and line.shipment_cost: + if line.type == 'line' and line.shipment_cost is not None: if line.shipment_cost == cost: unit_price = line.unit_price * Decimal(str(line.quantity)) lines.remove(line) @@ -265,6 +265,14 @@ class Sale(metaclass=PoolMeta): cost_line.amount = cost_line.on_change_with_amount() return cost_line + @property + def shipment_cost_amount(self): + cost = Decimal(0) + for line in self.lines: + if line.type == 'line' and line.shipment_cost is not None: + cost += line.amount + return cost + def create_shipment(self, shipment_type): pool = Pool() Shipment = pool.get('stock.shipment.out') diff --git a/setup.py b/setup.py index c855160a4a..ca5aeadedb 100755 --- a/setup.py +++ b/setup.py @@ -63,7 +63,10 @@ for dep in info.get('depends', []): requires.append(get_require_version('trytond_%s' % dep)) requires.append(get_require_version('trytond')) -tests_require = [get_require_version('proteus')] +tests_require = [ + get_require_version('proteus'), + get_require_version('trytond_stock_shipment_cost'), + ] for dep in ['account', 'party', 'stock', 'sale_promotion']: tests_require.append(get_require_version('trytond_%s' % dep)) dependency_links = [] diff --git a/stock.py b/stock.py index b409d28a41..498fe7900b 100644 --- a/stock.py +++ b/stock.py @@ -209,9 +209,36 @@ class ShipmentOut(metaclass=PoolMeta): return invoice_line def _get_shipment_cost(self): + pool = Pool() + Currency = pool.get('currency.currency') cost = super()._get_shipment_cost() - if all(not m.sale.shipment_cost_method for m in self.outgoing_moves): - cost += self.cost_used + methods = { + m.sale.shipment_cost_method for m in self.outgoing_moves if m.sale} + if methods == {None}: + cost += self.cost_used or 0 + if 'shipment' in methods: + if self.cost_sale: + cost_sale = Currency.compute( + self.cost_sale_currency, self.cost_sale, + self.company.currency, round=False) + else: + cost_sale = 0 + delta = cost_sale - (self.cost_used or 0) + if delta < 0: + cost -= delta + if 'order' in methods and self.cost_used: + 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 + if cost_sale < shipment_cost: + cost += self.cost_used or 0 + elif cost_sale < shipment_cost + (self.cost_used or 0): + cost += shipment_cost + self.cost_used - cost_sale return cost @classmethod diff --git a/tests/scenario_sale_shipment_cost_stock.rst b/tests/scenario_sale_shipment_cost_stock.rst new file mode 100644 index 0000000000..6c3a881c76 --- /dev/null +++ b/tests/scenario_sale_shipment_cost_stock.rst @@ -0,0 +1,190 @@ +================================= +Sale Shipment Cost Stock Scenario +================================= + +Imports:: + + >>> from decimal import Decimal + >>> from proteus import Model + >>> 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_chart, get_accounts) + +Activate modules:: + + >>> config = activate_modules([ + ... 'sale_shipment_cost', + ... 'sale', + ... 'stock_shipment_cost', + ... ]) + + >>> Carrier = Model.get('carrier') + >>> CarrierSelection = Model.get('carrier.selection') + >>> Party = Model.get('party.party') + >>> ProductCategory = Model.get('product.category') + >>> ProductTemplate = Model.get('product.template') + >>> ProductUom = Model.get('product.uom') + >>> Sale = Model.get('sale.sale') + >>> ShipmentOut = Model.get('stock.shipment.out') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create chart of accounts:: + + >>> _ = create_chart(company) + >>> accounts = get_accounts(company) + +Create customer:: + + >>> customer = Party(name='Customer') + >>> customer.save() + +Create account category:: + + >>> account_category = ProductCategory(name="Account Category") + >>> account_category.accounting = True + >>> account_category.account_revenue = accounts['revenue'] + >>> account_category.save() + +Create product:: + + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + + >>> template = ProductTemplate() + >>> template.name = 'Product' + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.salable = True + >>> template.list_price = Decimal('20') + >>> template.account_category = account_category + >>> template.save() + >>> product, = template.products + + >>> carrier_template = ProductTemplate() + >>> carrier_template.name = 'Carrier Product' + >>> carrier_template.default_uom = unit + >>> carrier_template.type = 'service' + >>> carrier_template.salable = True + >>> carrier_template.list_price = Decimal('2') + >>> carrier_template.account_category = account_category + >>> carrier_template.save() + >>> carrier_product, = carrier_template.products + >>> carrier_product.cost_price = Decimal('5') + >>> carrier_product.save() + +Create carrier:: + + >>> carrier = Carrier() + >>> party = Party(name='Carrier') + >>> party.save() + >>> carrier.party = party + >>> carrier.carrier_product = carrier_product + >>> carrier.save() + +Use it as the default carrier:: + + >>> csc = CarrierSelection(carrier=carrier) + >>> csc.save() + + +Sell product with no shipment cost:: + + >>> sale = Sale() + >>> sale.party = customer + >>> sale.carrier = carrier + >>> sale.shipment_cost_method = None + >>> line = sale.lines.new() + >>> line.product = product + >>> line.quantity = 1 + >>> sale.click('quote') + >>> len(sale.lines) + 1 + >>> sale.click('confirm') + >>> sale.click('process') + >>> sale.state + 'processing' + + >>> shipment, = sale.shipments + >>> shipment.cost_used + Decimal('5.0000') + >>> shipment.click('assign_force') + >>> shipment.click('pick') + >>> shipment.click('pack') + >>> shipment.click('done') + >>> shipment.state + 'done' + >>> move, = shipment.outgoing_moves + >>> move.shipment_out_cost_price + Decimal('5.0000') + +Sell product with cost on shipment:: + + >>> sale = Sale() + >>> sale.party = customer + >>> sale.carrier = carrier + >>> sale.shipment_cost_method = 'shipment' + >>> line = sale.lines.new() + >>> line.product = product + >>> line.quantity = 1 + >>> sale.click('quote') + >>> len(sale.lines) + 2 + >>> sale.click('confirm') + >>> sale.click('process') + >>> sale.state + 'processing' + + >>> shipment, = sale.shipments + >>> shipment.cost_used + Decimal('5.0000') + >>> shipment.cost_sale_used + Decimal('2.0000') + >>> shipment.click('assign_force') + >>> shipment.click('pick') + >>> shipment.click('pack') + >>> shipment.click('done') + >>> shipment.state + 'done' + >>> move, = shipment.outgoing_moves + >>> move.shipment_out_cost_price + Decimal('3.0000') + +Sell product with cost on order:: + + >>> sale = Sale() + >>> sale.party = customer + >>> sale.carrier = carrier + >>> sale.shipment_cost_method = 'order' + >>> line = sale.lines.new() + >>> line.product = product + >>> line.quantity = 1 + >>> sale.click('quote') + >>> len(sale.lines) + 2 + >>> sale.click('draft') + >>> sale.lines[-1].unit_price = Decimal('3.0000') + >>> sale.click('quote') + >>> sale.lines[-1].unit_price + Decimal('3.0000') + >>> sale.click('confirm') + >>> sale.click('process') + >>> sale.state + 'processing' + + >>> shipment, = sale.shipments + >>> shipment.cost_used + Decimal('5.0000') + >>> shipment.click('assign_force') + >>> shipment.click('pick') + >>> shipment.click('pack') + >>> shipment.click('done') + >>> shipment.state + 'done' + >>> move, = shipment.outgoing_moves + >>> move.shipment_out_cost_price + Decimal('2.0000') diff --git a/tests/test_sale_shipment_cost.py b/tests/test_sale_shipment_cost.py index 33bd31e783..e8f5886d3a 100644 --- a/tests/test_sale_shipment_cost.py +++ b/tests/test_sale_shipment_cost.py @@ -32,4 +32,9 @@ def suite(): tearDown=doctest_teardown, encoding='utf-8', checker=doctest_checker, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(doctest.DocFileSuite( + 'scenario_sale_shipment_cost_stock.rst', + tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite