details: https://code.tryton.org/tryton/commit/d5ea3f916372
branch: default
user: Cédric Krier <[email protected]>
date: Mon Sep 21 15:28:29 2026 +0200
description:
Add lot to POS line
Closes #15109
diffstat:
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 +
modules/stock_lot/tests/scenario_stock_lot_sale_point.rst | 141 ++++++++++++++
modules/stock_lot/tests/test_module.py | 17 +-
modules/stock_lot/tryton.cfg | 6 +
modules/stock_lot/view/sale_point_sale_line_form.xml | 9 +
modules/stock_lot/view/sale_point_sale_line_list.xml | 8 +
10 files changed, 250 insertions(+), 2 deletions(-)
diffs (360 lines):
diff -r 1d8ace864c38 -r d5ea3f916372 modules/sale_point/sale.py
--- a/modules/sale_point/sale.py Tue Sep 22 11:13:55 2026 +0200
+++ b/modules/sale_point/sale.py Mon Sep 21 15:28:29 2026 +0200
@@ -663,14 +663,20 @@
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 -r 1d8ace864c38 -r d5ea3f916372 modules/sale_point/view/sale_line_list.xml
--- a/modules/sale_point/view/sale_line_list.xml Tue Sep 22 11:13:55
2026 +0200
+++ b/modules/sale_point/view/sale_line_list.xml Mon Sep 21 15:28:29
2026 +0200
@@ -3,7 +3,7 @@
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="sale"/>
- <field name="product" expand="1"/>
+ <field name="product" expand="2"/>
<field name="unit_price"/>
<field name="quantity" symbol="unit"/>
<field name="amount"/>
diff -r 1d8ace864c38 -r d5ea3f916372 modules/stock_lot/CHANGELOG
--- a/modules/stock_lot/CHANGELOG Tue Sep 22 11:13:55 2026 +0200
+++ b/modules/stock_lot/CHANGELOG Mon Sep 21 15:28:29 2026 +0200
@@ -1,3 +1,4 @@
+* Add lot to POS line
Version 8.0.0 - 2026-04-20
--------------------------
diff -r 1d8ace864c38 -r d5ea3f916372 modules/stock_lot/sale.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock_lot/sale.py Mon Sep 21 15:28:29 2026 +0200
@@ -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 -r 1d8ace864c38 -r d5ea3f916372 modules/stock_lot/sale.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock_lot/sale.xml Mon Sep 21 15:28:29 2026 +0200
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tryton>
+ <data depends="sale_point">
+ <record model="ir.ui.view" id="sale_point_sale_line_view_form">
+ <field name="model">sale.point.sale.line</field>
+ <field name="inherit" ref="sale_point.sale_line_view_form"/>
+ <field name="name">sale_point_sale_line_form</field>
+ </record>
+
+ <record model="ir.ui.view" id="sale_point_sale_line_view_list">
+ <field name="model">sale.point.sale.line</field>
+ <field name="inherit" ref="sale_point.sale_line_view_list"/>
+ <field name="name">sale_point_sale_line_list</field>
+ </record>
+ </data>
+</tryton>
diff -r 1d8ace864c38 -r d5ea3f916372
modules/stock_lot/tests/scenario_stock_lot_sale_point.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock_lot/tests/scenario_stock_lot_sale_point.rst Mon Sep 21
15:28:29 2026 +0200
@@ -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 -r 1d8ace864c38 -r d5ea3f916372 modules/stock_lot/tests/test_module.py
--- a/modules/stock_lot/tests/test_module.py Tue Sep 22 11:13:55 2026 +0200
+++ b/modules/stock_lot/tests/test_module.py Mon Sep 21 15:28:29 2026 +0200
@@ -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.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 @@
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 -r 1d8ace864c38 -r d5ea3f916372 modules/stock_lot/tryton.cfg
--- a/modules/stock_lot/tryton.cfg Tue Sep 22 11:13:55 2026 +0200
+++ b/modules/stock_lot/tryton.cfg Mon Sep 21 15:28:29 2026 +0200
@@ -5,6 +5,7 @@
product
stock
extras_depend:
+ sale_point
sale_supply_drop_shipment
stock_conversion
stock_split
@@ -12,6 +13,7 @@
ir.xml
stock.xml
product.xml
+ sale.xml
message.xml
[register]
@@ -42,6 +44,10 @@
stock.MoveAddLots
stock.InventoryCount
+[register sale_point]
+model:
+ sale.POSSaleLine
+
[register sale_supply_drop_shipment]
model:
stock.ShipmentDrop
diff -r 1d8ace864c38 -r d5ea3f916372
modules/stock_lot/view/sale_point_sale_line_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock_lot/view/sale_point_sale_line_form.xml Mon Sep 21
15:28:29 2026 +0200
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+ <xpath expr="//field[@name='product']" position="after">
+ <label name="lot"/>
+ <field name="lot"/>
+ </xpath>
+</data>
diff -r 1d8ace864c38 -r d5ea3f916372
modules/stock_lot/view/sale_point_sale_line_list.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock_lot/view/sale_point_sale_line_list.xml Mon Sep 21
15:28:29 2026 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+ <xpath expr="//field[@name='product']" position="after">
+ <field name="lot" expand="1"/>
+ </xpath>
+</data>