Add wizard to add lots and sequence for lot number

issue9800
review326511002
This commit is contained in:
Cédric Krier committed 2021-02-03 21:19:06 +01:00
1 parent 437c60c871
commit 1b46c46534
18 files changed
+578 -7

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
* Add wizard to add lots
* Allow lot sequence number to be configured on product
* Add lots_by_locations Model
Version 5.8.0 - 2020-11-02
+9
View File
@@ -5,6 +5,10 @@ from trytond.pool import Pool
from . import stock
from . import product
from .stock import LotMixin
__all__ = ['LotMixin', 'register']
def register():
Pool.register(
@@ -14,6 +18,8 @@ def register():
stock.LotByWarehouseContext,
stock.Location,
stock.Move,
stock.MoveAddLotsStart,
stock.MoveAddLotsStartLot,
stock.ShipmentIn,
stock.ShipmentOut,
stock.ShipmentOutReturn,
@@ -23,9 +29,12 @@ def register():
stock.Inventory,
stock.InventoryLine,
stock.InventoryCountSearch,
product.Configuration,
product.ConfigurationDefaultLotSequence,
product.Template,
product.Product,
module='stock_lot', type_='model')
Pool.register(
stock.MoveAddLots,
stock.InventoryCount,
module='stock_lot', type_='wizard')
+19
View File
@@ -0,0 +1,19 @@
<?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>
<record model="ir.sequence.type" id="sequence_type_stock_lot">
<field name="name">Stock Lot</field>
<field name="code">stock.lot</field>
</record>
<record model="ir.sequence.type-res.group" id="sequence_type_stock_lot_group_admin">
<field name="sequence_type" ref="sequence_type_stock_lot"/>
<field name="group" ref="res.group_admin"/>
</record>
<record model="ir.sequence.type-res.group" id="sequence_type_stock_lot_group_product_admin">
<field name="sequence_type" ref="sequence_type_stock_lot"/>
<field name="group" ref="product.group_product_admin"/>
</record>
</data>
</tryton>
+3
View File
@@ -12,5 +12,8 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_only_lot">
<field name="text">You must select a stock lot instead of a product for "%(product)s".</field>
</record>
<record model="ir.message" id="msg_move_add_lot_quantity">
<field name="text">The total quantity "%(lot_quantity)s" of the lots must be less than or equal to the quantity "%(move_quantity)s" of the move.</field>
</record>
</data>
</tryton>
+41 -2
View File
@@ -3,12 +3,33 @@
from sql import Table
from trytond.config import config
from trytond.model import fields
from trytond.model import ModelSQL, ValueMixin, fields
from trytond.pyson import Eval
from trytond.pool import PoolMeta
from trytond.pool import PoolMeta, Pool
from trytond.transaction import Transaction
class Configuration(metaclass=PoolMeta):
__name__ = 'product.configuration'
default_lot_sequence = fields.MultiValue(
fields.Many2One(
'ir.sequence', "Default Lot Sequence",
domain=[
('code', '=', 'stock.lot'),
]))
class ConfigurationDefaultLotSequence(ModelSQL, ValueMixin):
"Product Configuration Default Lot Sequence"
__name__ = 'product.configuration.default_lot_sequence'
default_lot_sequence = fields.Many2One(
'ir.sequence', "Default Lot Sequence",
domain=[
('code', '=', 'stock.lot'),
])
class Template(metaclass=PoolMeta):
__name__ = 'product.template'
@@ -24,6 +45,16 @@ class Template(metaclass=PoolMeta):
'invisible': ~Eval('type').in_(['goods', 'assets']),
},
depends=['type'])
lot_sequence = fields.Many2One(
'ir.sequence', "Lot Sequence",
domain=[
('code', '=', 'stock.lot'),
],
states={
'invisible': ~Eval('type').in_(['goods', 'assets']),
},
depends=['type'],
help="The sequence used to automatically number lots.")
@classmethod
def __register__(cls, module):
@@ -66,6 +97,14 @@ class Template(metaclass=PoolMeta):
template_lot_type_table_name)
table_h.drop_table('stock.lot.type', lot_type_table_name)
@classmethod
def default_lot_sequence(cls, **pattern):
pool = Pool()
Configuration = pool.get('product.configuration')
sequence = Configuration(1).get_multivalue(
'default_lot_sequence', **pattern)
return sequence.id if sequence else None
class Product(metaclass=PoolMeta):
__name__ = 'product.product'
+6
View File
@@ -3,6 +3,12 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="product_configuration_view_form">
<field name="model">product.configuration</field>
<field name="inherit" ref="product.product_configuration_view_form"/>
<field name="name">product_configuration_form</field>
</record>
<record model="ir.ui.view" id="template_view_form">
<field name="model">product.template</field>
<field name="inherit" ref="product.template_view_form"/>
+254 -5
View File
@@ -1,18 +1,21 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from copy import copy
from functools import wraps
from sql import Column
from trytond.i18n import gettext
from trytond.model import Model, ModelView, ModelSQL, fields
from trytond.model.exceptions import AccessError, RequiredValidationError
from trytond.pyson import Eval
from trytond.model.exceptions import (
AccessError, RequiredValidationError, ValidationError)
from trytond.pyson import Eval, Bool, Len
from trytond.pool import Pool, PoolMeta
from trytond.tools import grouped_slice
from trytond.transaction import Transaction
from trytond.modules.stock import StockMixin
from trytond.wizard import Wizard, StateView, StateTransition, Button
def check_no_move(func):
@@ -47,12 +50,29 @@ def check_no_move(func):
return decorator
class Lot(ModelSQL, ModelView, StockMixin):
class LotMixin:
number = fields.Char(
"Number", required=True, select=True,
states={
'required': ~Eval('has_sequence') | (Eval('id', -1) >= 0),
},
depends=['has_sequence'])
product = fields.Many2One('product.product', 'Product', required=True)
has_sequence = fields.Function(
fields.Boolean("Has Sequence"), 'on_change_with_has_sequence')
@fields.depends('product')
def on_change_with_has_sequence(self, name=None):
if self.product:
return bool(self.product.lot_sequence)
class Lot(ModelSQL, ModelView, LotMixin, StockMixin):
"Stock Lot"
__name__ = 'stock.lot'
_rec_name = 'number'
number = fields.Char('Number', required=True, select=True)
product = fields.Many2One('product.product', 'Product', required=True)
quantity = fields.Function(fields.Float('Quantity'), 'get_quantity',
searcher='search_quantity')
forecast_quantity = fields.Function(fields.Float('Forecast Quantity'),
@@ -93,6 +113,24 @@ class Lot(ModelSQL, ModelView, StockMixin):
if self.product:
return self.product.default_uom.digits
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for values in vlist:
if not values.get('number'):
values['number'] = cls._new_number(values)
return super().create(vlist)
@classmethod
def _new_number(cls, values):
pool = Pool()
Product = pool.get('product.product')
Sequence = pool.get('ir.sequence')
if values.get('product'):
product = Product(values['product'])
if product.lot_sequence:
return Sequence.get_id(product.lot_sequence.id)
@classmethod
@check_no_move
def write(cls, *args):
@@ -235,6 +273,22 @@ class Move(metaclass=PoolMeta):
},
depends=['state', 'product'])
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update({
'add_lots_wizard': {
'invisible': ~Eval('state').in_(['draft', 'assigned']),
'readonly': Bool(Eval('lot')),
'depends': ['lot', 'state'],
},
})
@classmethod
@ModelView.button_action('stock_lot.wizard_move_add_lots')
def add_lots_wizard(cls, moves):
pass
def check_lot(self):
"Check if lot is required"
if (self.state == 'done'
@@ -253,6 +307,201 @@ class Move(metaclass=PoolMeta):
move.check_lot()
class MoveAddLots(Wizard):
"Add Lots"
__name__ = 'stock.move.add.lots'
start = StateView('stock.move.add.lots.start',
'stock_lot.move_add_lots_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Add", 'add', 'tryton-ok', default=True),
])
add = StateTransition()
def default_start(self, fields):
default = {}
if 'product' in fields:
default['product'] = self.record.product.id
if 'quantity' in fields:
default['quantity'] = self.record.quantity
if 'unit' in fields:
default['unit'] = self.record.uom.id
return default
def transition_add(self):
pool = Pool()
Lang = pool.get('ir.lang')
Lot = pool.get('stock.lot')
lang = Lang.get()
quantity_remaining = self.start.on_change_with_quantity_remaining()
if quantity_remaining < 0:
digits = self.record.uom.digits
move_quantity = self.record.quantity
lot_quantity = self.record.quantity - quantity_remaining
raise ValidationError(gettext(
'stock_lot.msg_move_add_lot_quantity',
lot_quantity=lang.format('%.*f', (digits, lot_quantity)),
move_quantity=lang.format(
'%.*f', (digits, move_quantity))))
lots = []
for line in self.start.lots:
lot = line.get_lot(self.record)
lots.append(lot)
Lot.save(lots)
if quantity_remaining:
self.record.quantity = quantity_remaining
self.record.save()
for i, (line, lot) in enumerate(zip(self.start.lots, lots)):
if not i and not quantity_remaining:
self.record.quantity = line.quantity
self.record.lot = lot
self.record.save()
else:
with Transaction().set_context(_stock_move_split=True):
self.model.copy([self.record], {
'quantity': line.quantity,
'lot': lot.id,
})
return 'end'
class MoveAddLotsStart(ModelView):
"Add Lots"
__name__ = 'stock.move.add.lots.start'
product = fields.Many2One('product.product', "Product", readonly=True)
quantity = fields.Float(
"Quantity", digits=(16, Eval('unit_digits', 2)), readonly=True,
depends=['unit_digits'])
unit = fields.Many2One('product.uom', "Unit", readonly=True)
unit_digits = fields.Function(
fields.Integer("Unit Digits"), 'on_change_with_unit_digits')
quantity_remaining = fields.Function(
fields.Float(
"Quantity Remaining", digits=(16, Eval('unit_digits', 2)),
depends=['unit_digits']),
'on_change_with_quantity_remaining')
lots = fields.One2Many(
'stock.move.add.lots.start.lot', 'parent', "Lots",
domain=[
('product', '=', Eval('product', -1)),
],
states={
'readonly': ~Eval('quantity_remaining', 0),
},
depends=['product', 'quantity_remaining'])
duplicate_lot_number = fields.Integer(
"Duplicate Lot Number",
states={
'invisible': Len(Eval('lots')) != 1,
},
depends=['lots'],
help="The number of times the lot must be duplicated.")
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update(
duplicate_lot={
'invisible': Len(Eval('lots')) != 1,
'readonly': (~Eval('duplicate_lot_number')
| (Eval('duplicate_lot_number', 0) <= 0)),
'depends': ['lots', 'duplicate_lot_number'],
},
)
@fields.depends('unit')
def on_change_with_unit_digits(self, name=None):
if self.unit:
return self.unit.digits
@fields.depends('quantity', 'lots', 'unit')
def on_change_with_quantity_remaining(self, name=None):
if self.quantity is not None:
quantity = self.quantity
if self.lots:
for lot in self.lots:
quantity -= getattr(lot, 'quantity', 0) or 0
if self.unit:
quantity = self.unit.round(quantity)
return quantity
@ModelView.button_change(
'lots', 'duplicate_lot_number',
methods=['on_change_with_quantity_remaining'])
def duplicate_lot(self):
lots = list(self.lots)
template, = self.lots
for i in range(self.duplicate_lot_number):
lot = copy(template)
lot._id = None
lots.append(lot)
self.lots = lots
self.quantity_remaining = self.on_change_with_quantity_remaining()
class MoveAddLotsStartLot(ModelView, LotMixin):
"Add Lots"
__name__ = 'stock.move.add.lots.start.lot'
parent = fields.Many2One('stock.move.add.lots.start', "Parent")
quantity = fields.Float(
"Quantity", digits=(16, Eval('unit_digits', 2)), required=True,
depends=['unit_digits'])
unit_digits = fields.Function(
fields.Integer("Unit Digits"), 'on_change_with_unit_digits')
@fields.depends(
'parent', '_parent_parent.quantity_remaining')
def on_change_parent(self):
if (self.parent
and self.parent.quantity_remaining is not None):
self.quantity = self.parent.quantity_remaining
@fields.depends('parent', '_parent_parent.unit_digits')
def on_change_with_unit_digits(self, name=None):
if self.parent:
return self.parent.unit_digits
@fields.depends('number', 'product', methods=['_set_lot_values'])
def on_change_number(self):
pool = Pool()
Lot = pool.get('stock.lot')
if self.number and self.product:
lots = Lot.search([
('number', '=', self.number),
('product', '=', self.product.id),
])
if len(lots) == 1:
lot, = lots
self._set_lot_values(lot)
def _set_lot_values(self, lot):
pass
def get_lot(self, move):
pool = Pool()
Lot = pool.get('stock.lot')
values = self._get_lot_values(move)
lots = Lot.search(
[(k, '=', v) for k, v in values.items()],
limit=1)
if lots:
lot, = lots
else:
lot = Lot()
for k, v in values.items():
setattr(lot, k, v)
return lot
def _get_lot_values(self, move):
return {
'number': self.number,
'product': move.product,
}
class ShipmentIn(metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
+30
View File
@@ -201,6 +201,36 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">move_tree</field>
</record>
<record model="ir.model.button" id="move_add_lots_wizard_button">
<field name="name">add_lots_wizard</field>
<field name="string">Add Lots</field>
<field name="model" search="[('model', '=', 'stock.move')]"/>
</record>
<record model="ir.action.wizard" id="wizard_move_add_lots">
<field name="name">Add Lots</field>
<field name="wiz_name">stock.move.add.lots</field>
<field name="model">stock.move</field>
</record>
<record model="ir.ui.view" id="move_add_lots_start_view_form">
<field name="model">stock.move.add.lots.start</field>
<field name="type">form</field>
<field name="name">stock_move_add_lots_start_form</field>
</record>
<record model="ir.ui.view" id="move_add_lots_start_lot_view_list">
<field name="model">stock.move.add.lots.start.lot</field>
<field name="type">tree</field>
<field name="name">stock_move_add_lots_start_lot_list</field>
</record>
<record model="ir.model.button" id="move_add_lots_start_duplicate_lot_button">
<field name="name">duplicate_lot</field>
<field name="string">Duplicate Lot</field>
<field name="model" search="[('model', '=', 'stock.move.add.lots.start')]"/>
</record>
<record model="ir.ui.view" id="period_cache_lot_view_form">
<field name="model">stock.period.cache.lot</field>
<field name="type">form</field>
+108
View File
@@ -0,0 +1,108 @@
===========================
Stock Lot Move Add Scenario
===========================
Imports::
>>> from decimal import Decimal
>>> from proteus import Model, Wizard
>>> from trytond.tests.tools import activate_modules
>>> from trytond.modules.company.tests.tools import create_company
Activate modules::
>>> config = activate_modules('stock_lot')
Create company::
>>> _ = create_company()
Create product::
>>> ProductUom = Model.get('product.uom')
>>> ProductTemplate = Model.get('product.template')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> template = ProductTemplate()
>>> template.name = 'Product'
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.list_price = Decimal('20')
>>> template.save()
>>> product, = template.products
Get stock locations::
>>> Location = Model.get('stock.location')
>>> storage_loc, = Location.find([('code', '=', 'STO')])
>>> customer_loc, = Location.find([('code', '=', 'CUS')])
Create a move::
>>> Move = Model.get('stock.move')
>>> move = Move()
>>> move.from_location = storage_loc
>>> move.to_location = customer_loc
>>> move.product = product
>>> move.quantity = 10
>>> move.unit_price = Decimal('20')
>>> move.save()
Create a lot::
>>> Lot = Model.get('stock.lot')
>>> lot2 = Lot(number='02', product=product)
>>> lot2.save()
Add few lots::
>>> add_lots = Wizard('stock.move.add.lots', [move])
>>> add_lots.form.product== product
True
>>> add_lots.form.quantity
10.0
>>> add_lots.form.unit == unit
True
>>> add_lots.form.quantity_remaining
10.0
>>> lot = add_lots.form.lots.new()
>>> lot.quantity
10.0
>>> lot.number = '01'
>>> lot.quantity = 2
>>> lot = add_lots.form.lots.new()
>>> lot.quantity
8.0
>>> lot.number = '02'
>>> lot.quantity = 1
>>> add_lots.execute('add')
Check moves::
>>> move, = Move.find([('lot.number', '=', '01')])
>>> move.quantity
2.0
>>> move, = Move.find([('lot.number', '=', '02')])
>>> move.quantity
1.0
>>> move.lot == lot2
True
>>> move, = Move.find([('lot', '=', None)])
>>> move.quantity
7.0
Add lot to remaining::
>>> add_lots = Wizard('stock.move.add.lots', [move])
>>> lot = add_lots.form.lots.new()
>>> lot.number = '03'
>>> add_lots.execute('add')
>>> len(Move.find([]))
3
>>> move.lot.number
'03'
>>> move.quantity
7.0
+51
View File
@@ -0,0 +1,51 @@
=========================
Stock Lot Number Scenario
=========================
Imports::
>>> from decimal import Decimal
>>> from proteus import Model
>>> from trytond.tests.tools import activate_modules
Activate modules::
>>> config = activate_modules('stock_lot')
Create lot sequence::
>>> Sequence = Model.get('ir.sequence')
>>> sequence = Sequence(name="Lot", code='stock.lot')
>>> sequence.save()
Set default sequence::
>>> Configuration = Model.get('product.configuration')
>>> configuration = Configuration(1)
>>> configuration.default_lot_sequence = sequence
>>> configuration.save()
Create product::
>>> ProductUom = Model.get('product.uom')
>>> ProductTemplate = Model.get('product.template')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> template = ProductTemplate()
>>> template.lot_sequence == sequence
True
>>> template.name = 'Product'
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.list_price = Decimal('20')
>>> template.save()
>>> product, = template.products
Create lot without number::
>>> Lot = Model.get('stock.lot')
>>> lot = Lot(product=product)
>>> lot.save()
>>> lot.number
'1'
+8
View File
@@ -325,4 +325,12 @@ def suite():
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite('scenario_stock_lot_number.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite('scenario_stock_lot_move_add.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite
+1
View File
@@ -5,6 +5,7 @@ depends:
product
stock
xml:
ir.xml
stock.xml
product.xml
message.xml
+3
View File
@@ -7,4 +7,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="lot"/>
<newline/>
</xpath>
<xpath expr="/form/group[@id='buttons']" position="inside">
<button name="add_lots_wizard" icon="tryton-add"/>
</xpath>
</data>
+3
View File
@@ -5,4 +5,7 @@ this repository contains the full copyright notices and license terms. -->
<xpath expr="/tree/field[@name='product']" position="after">
<field name="lot" expand="1"/>
</xpath>
<xpath expr="/tree" position="inside">
<button name="add_lots_wizard"/>
</xpath>
</data>
+9
View File
@@ -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_sequence']" position="after">
<label name="default_lot_sequence"/>
<field name="default_lot_sequence"/>
</xpath>
</data>
+20
View File
@@ -0,0 +1,20 @@
<?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. -->
<form>
<label name="product"/>
<field name="product" colspan="3"/>
<label name="quantity"/>
<field name="quantity" symbol="unit"/>
<label name="quantity_remaining" string="Remaining:"/>
<field name="quantity_remaining" symbol="unit"/>
<field name="lots" colspan="4"/>
<group col="-1" colspan="2" id="duplicate_lot">
<button name="duplicate_lot" icon="tryton-copy"/>
<field name="duplicate_lot_number" xexpand="0"/>
<label name="duplicate_lot_number" string="times"/>
</group>
</form>
@@ -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. -->
<tree editable="1">
<field name="parent" expand="1"/>
<field name="number" expand="2"/>
<field name="product" expand="1"/>
<field name="quantity"/>
</tree>
+2
View File
@@ -6,6 +6,8 @@ this repository contains the full copyright notices and license terms. -->
<page string="Lots" name="lot_required">
<label name="lot_required"/>
<field name="lot_required" yexpand="0"/>
<label name="lot_sequence"/>
<field name="lot_sequence"/>
</page>
</xpath>
</data>