From 5ea8b1f784dadb436b0872cc8f85ebf1aaf30efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 21 Sep 2026 15:28:29 +0200 Subject: [PATCH] Add lot to POS line Closes #15109 --- modules/sale_point/sale.py | 6 + modules/sale_point/view/sale_line_list.xml | 2 +- modules/stock_lot/CHANGELOG | 1 + modules/stock_lot/sale.py | 44 ++++++ modules/stock_lot/sale.xml | 18 +++ .../tests/scenario_stock_lot_sale_point.rst | 141 ++++++++++++++++++ modules/stock_lot/tests/test_module.py | 17 ++- modules/stock_lot/tryton.cfg | 6 + .../view/sale_point_sale_line_form.xml | 9 ++ .../view/sale_point_sale_line_list.xml | 8 + 10 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 modules/stock_lot/sale.py create mode 100644 modules/stock_lot/sale.xml create mode 100644 modules/stock_lot/tests/scenario_stock_lot_sale_point.rst create mode 100644 modules/stock_lot/view/sale_point_sale_line_form.xml create mode 100644 modules/stock_lot/view/sale_point_sale_line_list.xml diff --git a/modules/sale_point/sale.py b/modules/sale_point/sale.py index 03b3ee0142..9d4ad471cf 100644 --- a/modules/sale_point/sale.py +++ b/modules/sale_point/sale.py @@ -663,14 +663,20 @@ class POSSaleLine(ModelSQL, ModelView, TaxableMixin): return move @property + @fields.depends('quantity', 'sale', '_parent_sale.point') def from_location(self): + if self.quantity is None: + return if self.quantity >= 0: return self.sale.point.storage_location else: return self.sale.point.customer_location @property + @fields.depends('quantity', 'sale', '_parent_sale.point') def to_location(self): + if self.quantity is None: + return if self.quantity >= 0: return self.sale.point.customer_location else: diff --git a/modules/sale_point/view/sale_line_list.xml b/modules/sale_point/view/sale_line_list.xml index 1f30e76351..269cd25215 100644 --- a/modules/sale_point/view/sale_line_list.xml +++ b/modules/sale_point/view/sale_line_list.xml @@ -3,7 +3,7 @@ this repository contains the full copyright notices and license terms. --> - + diff --git a/modules/stock_lot/CHANGELOG b/modules/stock_lot/CHANGELOG index b26b0a869d..ae2888fcc3 100644 --- a/modules/stock_lot/CHANGELOG +++ b/modules/stock_lot/CHANGELOG @@ -1,3 +1,4 @@ +* Add lot to POS line Version 8.0.0 - 2026-04-20 -------------------------- diff --git a/modules/stock_lot/sale.py b/modules/stock_lot/sale.py new file mode 100644 index 0000000000..524595ba19 --- /dev/null +++ b/modules/stock_lot/sale.py @@ -0,0 +1,44 @@ +# 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.model import fields +from trytond.pool import PoolMeta +from trytond.pyson import Eval + + +class POSSaleLine(metaclass=PoolMeta): + __name__ = 'sale.point.sale.line' + + lot = fields.Many2One( + 'stock.lot', "Lot", ondelete='RESTRICT', + domain=[ + ('product', '=', Eval('product', -1)), + ], + states={ + 'readonly': Eval('sale_state') != 'open', + 'required': Eval('lot_required', False), + }) + lot_required = fields.Function( + fields.Boolean("Lot Required"), + 'on_change_with_lot_required') + + @fields.depends('product', methods=['from_location', 'to_location']) + def on_change_with_lot_required(self, name=None): + if self.product and self.from_location and self.to_location: + return self.product.lot_is_required( + self.from_location, self.to_location) + + @fields.depends('product', 'lot') + def on_change_product(self): + try: + super().on_change_product() + except AttributeError: + pass + if self.lot and self.lot.product != self.product: + self.lot = None + + def get_stock_move(self): + move = super().get_stock_move() + if move: + move.lot = self.lot + return move diff --git a/modules/stock_lot/sale.xml b/modules/stock_lot/sale.xml new file mode 100644 index 0000000000..a2f39543f0 --- /dev/null +++ b/modules/stock_lot/sale.xml @@ -0,0 +1,18 @@ + + + + + + sale.point.sale.line + + sale_point_sale_line_form + + + + sale.point.sale.line + + sale_point_sale_line_list + + + diff --git a/modules/stock_lot/tests/scenario_stock_lot_sale_point.rst b/modules/stock_lot/tests/scenario_stock_lot_sale_point.rst new file mode 100644 index 0000000000..3ef334c04c --- /dev/null +++ b/modules/stock_lot/tests/scenario_stock_lot_sale_point.rst @@ -0,0 +1,141 @@ +====================== +Stock Lot POS Scenario +====================== + +Imports:: + + >>> from decimal import Decimal + + >>> from proteus import Model + >>> from trytond.modules.account.tests.tools import ( + ... create_chart, create_fiscalyear, get_accounts) + >>> from trytond.modules.account_invoice.tests.tools import ( + ... set_fiscalyear_invoice_sequences) + >>> from trytond.modules.company.tests.tools import create_company + >>> from trytond.tests.tools import activate_modules, assertEqual + +Activate modules:: + + >>> config = activate_modules( + ... ['stock_lot', 'sale_point'], create_company, create_chart) + + >>> Journal = Model.get('account.journal') + >>> Location = Model.get('stock.location') + >>> Lot = Model.get('stock.lot') + >>> POS = Model.get('sale.point') + >>> PaymentMethod = Model.get('sale.point.payment.method') + >>> ProductCategory = Model.get('product.category') + >>> ProductTemplate = Model.get('product.template') + >>> ProductUom = Model.get('product.uom') + >>> Sale = Model.get('sale.point.sale') + >>> SequenceStrict = Model.get('ir.sequence.strict') + >>> SequenceType = Model.get('ir.sequence.type') + >>> StockMove = Model.get('stock.move') + +Create fiscal year:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences(create_fiscalyear()) + >>> fiscalyear.click('create_period') + +Get accounts:: + + >>> accounts = get_accounts() + +Create account categories:: + + >>> 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.account_category = account_category + >>> template.gross_price = Decimal('10.0000') + >>> template.lot_required = ['storage', 'customer'] + >>> template.save() + >>> goods, = template.products + + >>> lot = Lot(number='0001', product=goods) + >>> lot.save() + + >>> template = ProductTemplate() + >>> template.name = 'service' + >>> template.default_uom = unit + >>> template.type = 'service' + >>> template.salable = True + >>> template.account_category = account_category + >>> template.gross_price = Decimal('10.0000') + >>> template.save() + >>> service, = template.products + +Get journal:: + + >>> journal_revenue, = Journal.find([('type', '=', 'revenue')], limit=1) + +Get stock locations:: + + >>> storage_loc, = Location.find([('code', '=', 'STO')]) + >>> customer_loc, = Location.find([('code', '=', 'CUS')]) + +Create POS:: + + >>> pos = POS(name="POS") + >>> pos.journal = journal_revenue + >>> pos.sequence = SequenceStrict(name="POS", company=pos.company) + >>> pos.sequence.sequence_type, = SequenceType.find( + ... [('name', '=', "POS")], limit=1) + >>> pos.sequence.save() + >>> pos.storage_location = storage_loc + >>> pos.customer_location = customer_loc + >>> pos.save() + +Setup a payment method:: + + >>> cash_method = PaymentMethod(name="Cash") + >>> cash_method.account = accounts['cash'] + >>> cash_method.cash = True + >>> cash_method.save() + +Make a sale with lot:: + + >>> sale = Sale(point=pos) + >>> line = sale.lines.new() + >>> line.product = goods + >>> line.lot = lot + >>> line.quantity = 1 + >>> line = sale.lines.new() + >>> line.product = service + >>> line.quantity = 1 + >>> sale.save() + >>> sale.state + 'open' + +Pay the sale:: + + >>> payment = sale.click('pay') + >>> payment.form.method = cash_method + >>> payment.execute('pay') + >>> sale.state + 'done' + +Post the sale:: + + >>> sale.click('post') + >>> sale.state + 'posted' + +Check stock move:: + + >>> move, = StockMove.find([ + ... ('origin.sale', '=', sale.id, 'sale.point.sale.line') + ... ]) + >>> assertEqual(move.product, goods) + >>> assertEqual(move.lot, lot) diff --git a/modules/stock_lot/tests/test_module.py b/modules/stock_lot/tests/test_module.py index 0a5063a1b4..41405c88a2 100644 --- a/modules/stock_lot/tests/test_module.py +++ b/modules/stock_lot/tests/test_module.py @@ -3,6 +3,8 @@ import datetime as dt from decimal import Decimal +from functools import wraps +from unittest.mock import patch from trytond.modules.company.tests import ( CompanyTestMixin, create_company, set_company) @@ -11,12 +13,24 @@ from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.transaction import Transaction +def disable_check_origin(func): + @wraps(func) + def wrapper(*args, **kwargs): + pool = Pool() + Move = pool.get('stock.move') + with patch.object(Move, 'check_origin') as check_origin: + check_origin.return_value = set() + return func(*args, **kwargs) + return wrapper + + class StockLotTestCase(CompanyTestMixin, ModuleTestCase): 'Test Stock Lot module' module = 'stock_lot' - extras = ['stock_split', 'stock_conversion'] + extras = ['sale_point', 'stock_split', 'stock_conversion'] @with_transaction() + @disable_check_origin def test_products_by_location(self): 'Test products_by_location' pool = Pool() @@ -215,6 +229,7 @@ class StockLotTestCase(CompanyTestMixin, ModuleTestCase): quantities[(lot_cache.location, lot_cache.lot)]) @with_transaction() + @disable_check_origin def test_assign_try_with_lot(self): "Test Move assign_try with lot" pool = Pool() diff --git a/modules/stock_lot/tryton.cfg b/modules/stock_lot/tryton.cfg index ad1aba0323..cb36aab43d 100644 --- a/modules/stock_lot/tryton.cfg +++ b/modules/stock_lot/tryton.cfg @@ -5,6 +5,7 @@ depends: product stock extras_depend: + sale_point sale_supply_drop_shipment stock_conversion stock_split @@ -12,6 +13,7 @@ xml: ir.xml stock.xml product.xml + sale.xml message.xml [register] @@ -42,6 +44,10 @@ wizard: stock.MoveAddLots stock.InventoryCount +[register sale_point] +model: + sale.POSSaleLine + [register sale_supply_drop_shipment] model: stock.ShipmentDrop diff --git a/modules/stock_lot/view/sale_point_sale_line_form.xml b/modules/stock_lot/view/sale_point_sale_line_form.xml new file mode 100644 index 0000000000..8dcaa3e3c1 --- /dev/null +++ b/modules/stock_lot/view/sale_point_sale_line_form.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/modules/stock_lot/view/sale_point_sale_line_list.xml b/modules/stock_lot/view/sale_point_sale_line_list.xml new file mode 100644 index 0000000000..a1ed3a6574 --- /dev/null +++ b/modules/stock_lot/view/sale_point_sale_line_list.xml @@ -0,0 +1,8 @@ + + + + + + +