Add wizard to add lots and sequence for lot number
issue9800 review326511002
This commit is contained in:
1 parent
437c60c871
commit
1b46c46534
18 files changed
+578
-7
No files matched your search
@@ -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
|
||||
|
||||
@@ -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')
|
||||
@@ -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>
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -5,6 +5,7 @@ depends:
|
||||
product
|
||||
stock
|
||||
xml:
|
||||
ir.xml
|
||||
stock.xml
|
||||
product.xml
|
||||
message.xml
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user