From 61e1b18e76aefefe0255d8aa40d5f83c5fa86b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 17 Dec 2023 17:07:41 +0100 Subject: [PATCH] Compute remaining quantity of sale and purchase order following their method Ignored quantities must be deduced from the remaining quantity. Closes #12698 --- modules/product_kit/sale.py | 17 ++++ modules/purchase/purchase.py | 6 ++ ...io_purchase_line_cancelled_on_shipment.rst | 96 +++++++++++++++++++ modules/sale/sale.py | 15 +++ ...cenario_sale_line_cancelled_on_invoice.rst | 96 +++++++++++++++++++ ...enario_sale_line_cancelled_on_shipment.rst | 96 +++++++++++++++++++ 6 files changed, 326 insertions(+) create mode 100644 modules/purchase/tests/scenario_purchase_line_cancelled_on_shipment.rst create mode 100644 modules/sale/tests/scenario_sale_line_cancelled_on_invoice.rst create mode 100644 modules/sale/tests/scenario_sale_line_cancelled_on_shipment.rst diff --git a/modules/product_kit/sale.py b/modules/product_kit/sale.py index bb00f60f45..da79138ded 100644 --- a/modules/product_kit/sale.py +++ b/modules/product_kit/sale.py @@ -132,6 +132,23 @@ class LineComponent( invoice_line.quantity, self.line.unit) return self.quantity * quantity / self.line.quantity + @property + def _move_remaining_quantity(self): + pool = Pool() + UoM = pool.get('product.uom') + quantity = super()._move_remaining_quantity + if self.line.sale.shipment_method == 'invoice': + invoices_ignored = set(self.line.sale.invoices_ignored) + ratio = self.get_moved_ratio() + if ratio: + for invoice_line in self.line.invoice_lines: + if invoice_line.invoice in invoices_ignored: + quantity -= UoM.compute_qty( + invoice_line.unit * self.line.unit, + invoice_line.quantity / ratio, + self.line.unit) + return quantity + def check_move_quantity(self): from trytond.modules.sale.exceptions import SaleMoveQuantity pool = Pool() diff --git a/modules/purchase/purchase.py b/modules/purchase/purchase.py index ec56f2e8fb..e1ea687c97 100644 --- a/modules/purchase/purchase.py +++ b/modules/purchase/purchase.py @@ -1330,6 +1330,12 @@ class Line(sequence_ordered(), ModelSQL, ModelView): return skips = set(self.purchase.invoices_ignored) quantity = self.quantity + if self.purchase.invoice_method == 'shipment': + moves_ignored = set(self.moves_ignored) + for move in self.moves: + if move in moves_ignored: + quantity -= UoM.compute_qty( + move.unit, move.quantity, self.unit) for invoice_line in self.invoice_lines: if invoice_line.type != 'line': continue diff --git a/modules/purchase/tests/scenario_purchase_line_cancelled_on_shipment.rst b/modules/purchase/tests/scenario_purchase_line_cancelled_on_shipment.rst new file mode 100644 index 0000000000..b69fe24b79 --- /dev/null +++ b/modules/purchase/tests/scenario_purchase_line_cancelled_on_shipment.rst @@ -0,0 +1,96 @@ +============================================ +Purchase Line Cancelled On Shipment 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 + >>> from trytond.modules.account.tests.tools import ( + ... create_chart, get_accounts) + +Activate modules:: + + >>> config = activate_modules('purchase') + + >>> Party = Model.get('party.party') + >>> ProductCategory = Model.get('product.category') + >>> ProductTemplate = Model.get('product.template') + >>> ProductUom = Model.get('product.uom') + >>> Purchase = Model.get('purchase.purchase') + +Create company:: + + >>> _ = create_company() + +Create chart of accounts:: + + >>> _ = create_chart() + >>> accounts = get_accounts() + +Create party:: + + >>> supplier = Party(name="Supplier") + >>> supplier.save() + +Create product:: + + >>> account_category = ProductCategory(name="Account Category") + >>> account_category.accounting = True + >>> account_category.account_expense = accounts['expense'] + >>> account_category.save() + + >>> template = ProductTemplate(name="Product") + >>> template.default_uom, = ProductUom.find([('name', '=', "Unit")]) + >>> template.type = 'goods' + >>> template.purchasable = True + >>> template.account_category = account_category + >>> template.save() + >>> product, = template.products + +Purchase product:: + + >>> purchase = Purchase(party=supplier) + >>> purchase.invoice_method = 'shipment' + >>> line = purchase.lines.new() + >>> line.product = product + >>> line.quantity = 1 + >>> line.unit_price = Decimal('10.0000') + >>> purchase.click('quote') + >>> purchase.click('confirm') + >>> purchase.state + 'processing' + >>> purchase.shipment_state + 'waiting' + >>> purchase.invoice_state + 'none' + +Cancel stock move:: + + >>> move, = purchase.moves + >>> move.click('cancel') + >>> move.state + 'cancelled' + + >>> purchase.reload() + >>> purchase.state + 'processing' + >>> purchase.shipment_state + 'exception' + >>> purchase.invoice_state + 'none' + +Ignore exception:: + + >>> shipment_handle_exception = purchase.click('handle_shipment_exception') + >>> shipment_handle_exception.form.recreate_moves.clear() + >>> shipment_handle_exception.execute('handle') + + >>> purchase.shipment_state + 'none' + + >>> purchase.state + 'done' diff --git a/modules/sale/sale.py b/modules/sale/sale.py index 2a6ce1a035..701727445a 100644 --- a/modules/sale/sale.py +++ b/modules/sale/sale.py @@ -1405,6 +1405,12 @@ class SaleLine(TaxableMixin, sequence_ordered(), ModelSQL, ModelView): return skips = set(self.sale.invoices_ignored) quantity = self.quantity + if self.sale.invoice_method == 'shipment': + moves_ignored = set(self.moves_ignored) + for move in self.moves: + if move in moves_ignored: + quantity -= UoM.compute_qty( + move.unit, move.quantity, self.unit) for invoice_line in self.invoice_lines: if invoice_line.type != 'line': continue @@ -1434,6 +1440,15 @@ class SaleLine(TaxableMixin, sequence_ordered(), ModelSQL, ModelView): return skips = set(self.moves_ignored) quantity = abs(self.quantity) + if self.sale.shipment_method == 'invoice': + invoices_ignored = set(self.sale.invoices_ignored) + for invoice_line in self.invoice_lines: + if invoice_line.type != 'line': + continue + if invoice_line.invoice in invoices_ignored: + quantity -= Uom.compute_qty( + invoice_line.unit or self.unit, invoice_line.quantity, + self.unit) for move in self.moves: if move.state == 'done' or move in skips: quantity -= Uom.compute_qty( diff --git a/modules/sale/tests/scenario_sale_line_cancelled_on_invoice.rst b/modules/sale/tests/scenario_sale_line_cancelled_on_invoice.rst new file mode 100644 index 0000000000..a37837e408 --- /dev/null +++ b/modules/sale/tests/scenario_sale_line_cancelled_on_invoice.rst @@ -0,0 +1,96 @@ +======================================= +Sale Line Cancelled On Invoice 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 + >>> from trytond.modules.account.tests.tools import ( + ... create_chart, get_accounts) + +Activate modules:: + + >>> config = activate_modules('sale') + + >>> 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') + +Create company:: + + >>> _ = create_company() + +Create chart of accounts:: + + >>> _ = create_chart() + >>> accounts = get_accounts() + +Create party:: + + >>> customer = Party(name="Customer") + >>> customer.save() + +Create product:: + + >>> account_category = ProductCategory(name="Account Category") + >>> account_category.accounting = True + >>> account_category.account_revenue = accounts['revenue'] + >>> account_category.save() + + >>> template = ProductTemplate(name="Product") + >>> template.default_uom, = ProductUom.find([('name', '=', "Unit")]) + >>> template.type = 'goods' + >>> template.salable = True + >>> template.list_price = Decimal('10.0000') + >>> template.account_category = account_category + >>> template.save() + >>> product, = template.products + +Sale product:: + + >>> sale = Sale(party=customer) + >>> sale.shipment_method = 'invoice' + >>> line = sale.lines.new() + >>> line.product = product + >>> line.quantity = 1 + >>> sale.click('quote') + >>> sale.click('confirm') + >>> sale.state + 'processing' + >>> sale.shipment_state + 'none' + >>> sale.invoice_state + 'pending' + +Cancel invoice:: + + >>> invoice, = sale.invoices + >>> invoice.click('cancel') + >>> invoice.state + 'cancelled' + + >>> sale.reload() + >>> sale.state + 'processing' + >>> sale.shipment_state + 'none' + >>> sale.invoice_state + 'exception' + +Ignore exception:: + + >>> invoice_handle_exception = sale.click('handle_invoice_exception') + >>> invoice_handle_exception.form.recreate_invoices.clear() + >>> invoice_handle_exception.execute('handle') + + >>> sale.shipment_state + 'none' + + >>> sale.state + 'done' diff --git a/modules/sale/tests/scenario_sale_line_cancelled_on_shipment.rst b/modules/sale/tests/scenario_sale_line_cancelled_on_shipment.rst new file mode 100644 index 0000000000..6502a09ba9 --- /dev/null +++ b/modules/sale/tests/scenario_sale_line_cancelled_on_shipment.rst @@ -0,0 +1,96 @@ +======================================== +Sale Line Cancelled On Shipment 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 + >>> from trytond.modules.account.tests.tools import ( + ... create_chart, get_accounts) + +Activate modules:: + + >>> config = activate_modules('sale') + + >>> 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') + +Create company:: + + >>> _ = create_company() + +Create chart of accounts:: + + >>> _ = create_chart() + >>> accounts = get_accounts() + +Create party:: + + >>> customer = Party(name="Customer") + >>> customer.save() + +Create product:: + + >>> account_category = ProductCategory(name="Account Category") + >>> account_category.accounting = True + >>> account_category.account_revenue = accounts['revenue'] + >>> account_category.save() + + >>> template = ProductTemplate(name="Product") + >>> template.default_uom, = ProductUom.find([('name', '=', "Unit")]) + >>> template.type = 'goods' + >>> template.salable = True + >>> template.list_price = Decimal('10.0000') + >>> template.account_category = account_category + >>> template.save() + >>> product, = template.products + +Sale product:: + + >>> sale = Sale(party=customer) + >>> sale.invoice_method = 'shipment' + >>> line = sale.lines.new() + >>> line.product = product + >>> line.quantity = 1 + >>> sale.click('quote') + >>> sale.click('confirm') + >>> sale.state + 'processing' + >>> sale.shipment_state + 'waiting' + >>> sale.invoice_state + 'none' + +Cancel shipment:: + + >>> shipment, = sale.shipments + >>> shipment.click('cancel') + >>> shipment.state + 'cancelled' + + >>> sale.reload() + >>> sale.state + 'processing' + >>> sale.shipment_state + 'exception' + >>> sale.invoice_state + 'none' + +Ignore exception:: + + >>> shipment_handle_exception = sale.click('handle_shipment_exception') + >>> shipment_handle_exception.form.recreate_moves.clear() + >>> shipment_handle_exception.execute('handle') + + >>> sale.shipment_state + 'none' + + >>> sale.state + 'done'