Add lot to POS line

Closes #15109
This commit is contained in:
Cédric Krier
2026-09-21 15:28:29 +02:00
parent 8690e9aa07
commit 5ea8b1f784
10 changed files with 250 additions and 2 deletions
+6
View File
@@ -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:
+1 -1
View File
@@ -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"/>
+1
View File
@@ -1,3 +1,4 @@
* Add lot to POS line
Version 8.0.0 - 2026-04-20
--------------------------
+44
View File
@@ -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
+18
View File
@@ -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>
@@ -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)
+16 -1
View File
@@ -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()
+6
View File
@@ -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
@@ -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>
@@ -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>