@@ -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:
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
* Add lot to POS line
|
||||
|
||||
Version 8.0.0 - 2026-04-20
|
||||
--------------------------
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user