Implement unit load manual creation/production.

This commit refs #509
This commit is contained in:
Sergio Morillo 2015-11-13 18:54:55 +01:00
parent 18c66f39c6
commit d925e23b90
8 changed files with 285 additions and 113 deletions

View File

@ -17,3 +17,9 @@ class Configuration:
domain=[('code', '=', 'stock.unit_load'),
('company', 'in',
[Eval('context', {}).get('company', -1), None])]))
ul_production_type = fields.Property(
fields.Selection([('location', 'Location')], 'Production type', required=True))
@classmethod
def default_ul_production_type(cls):
return 'location'

View File

@ -106,10 +106,6 @@ msgctxt "field:stock.unit_load,uom_digits:"
msgid "UOM Digits"
msgstr "Dígitos UdM"
msgctxt "field:stock.unit_load,production_locations:"
msgid "Production locations"
msgstr "Ubs. de producción"
msgctxt "field:stock.unit_load,write_date:"
msgid "Write Date"
msgstr "Fecha modificación"

View File

@ -53,38 +53,65 @@ Create product::
>>> product.template = template
>>> product.save()
Check sequence for unit load::
>>> UnitLoad = Model.get('stock.unit_load')
>>> unit_load = UnitLoad()
>>> unit_load.product = product
>>> unit_load.save()
>>> unit_load.code
u'1'
Get stock locations::
>>> Location = Model.get('stock.location')
>>> production_loc = Location(name='Production', code='PL')
>>> production_loc.type = 'production'
>>> production_loc.save()
>>> production_loc, = Location.find([('type', '=', 'production')])
>>> warehouse_loc, = Location.find([('code', '=', 'WH')])
>>> storage_loc, = Location.find([('code', '=', 'STO')])
>>> output_loc, = Location.find([('code', '=', 'OUT')])
Create an unit load::
>>> UnitLoad = Model.get('stock.unit_load')
>>> unit_load = UnitLoad()
>>> unit_load.company != None
True
>>> unit_load.production_type = 'location'
>>> unit_load.production_location = output_loc
>>> unit_load.product = product
>>> len(unit_load.production_moves)
0
>>> unit_load.save() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
UserError: ...
>>> unit_load.production_location = production_loc
>>> unit_load.save()
>>> unit_load.code != None
True
Add moves::
>>> move = unit_load.moves.new()
>>> move.planned_date = today
>>> move.product = product
>>> move.quantity = Decimal(35)
>>> move.from_location = production_loc
>>> unit_load.production_state
'running'
>>> unit_load.quantity = Decimal('35.0')
>>> len(unit_load.production_moves)
1
>>> move = unit_load.production_moves[0]
>>> move.planned_date == today
True
>>> move.product == unit_load.product
True
>>> move.quantity
35.0
>>> move.from_location == production_loc
True
>>> not move.to_location
True
>>> move.to_location = storage_loc
>>> move.currency = company.currency
>>> move.currency == unit_load.company.currency
True
>>> unit_load.save()
>>> unit_load.state
u'draft'
>>> unit_load.moves[0].state
>>> unit_load.production_state
'running'
>>> len(unit_load.moves)
1
>>> len(unit_load.production_moves)
1
>>> unit_load.production_moves[0].state
u'draft'
Check computed fields::
@ -98,6 +125,8 @@ Check computed fields::
u'assigned'
>>> unit_load.moves[0].state
u'assigned'
>>> unit_load.production_state
'running'
>>> len(unit_load.ul_moves)
1
>>> unit_load.uom.rec_name
@ -119,6 +148,8 @@ State error::
>>> unit_load.click('do')
>>> unit_load.state
u'done'
>>> unit_load.production_state
'done'
>>> unit_load.quantity
35.0
>>> unit_load.forecast_quantity
@ -148,15 +179,6 @@ Date error::
>>> unit_load.last_date == tomorrow
True
Production locations::
>>> unit_load.reload()
>>> len(unit_load.production_locations)
1
>>> unit_load.production_locations[0].code
u'PL'
Cancel last move::
>>> unit_load.location.id == output_loc.id

View File

@ -46,13 +46,10 @@ def create_unit_load(config=None, product=None, do_state=True):
production_loc.type = 'production'
production_loc.save()
move = unit_load.moves.new()
move.planned_date = today
move.product = unit_load.product
move.quantity = Decimal(35)
move.from_location = production_loc
unit_load.production_location = production_loc
unit_load.quantity = Decimal(35)
move = unit_load.production_moves[0]
move.to_location = storage_loc
move.currency = move.company.currency
unit_load.save()
if do_state:
unit_load.click('do')

View File

@ -3,7 +3,7 @@ version=3.6.1
depends:
ir
res
stock
production
extras_depend:
stock_lot

View File

@ -6,11 +6,9 @@ from itertools import groupby
from sql import Literal, Null
from sql.aggregate import Max
from sql.conditionals import Coalesce
from sql.functions import Now
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Not, Eval, Bool, Or, Equal, Date
from trytond.pyson import Not, Eval, Bool, Or, Equal, Date, If
from trytond.model import fields, ModelView, ModelSQL
from trytond.tools import grouped_slice, reduce_ids
from trytond.transaction import Transaction
from trytond.wizard import Wizard, StateTransition, StateView, Button
@ -19,8 +17,8 @@ __all__ = ['UnitLoad', 'UnitLoadMove', 'MoveUnitLoad',
__metaclass__ = PoolMeta
UL_STATES = {'readonly': Bool(Eval('moves', [0]))}
UL_DEPENDS = ['moves']
MOVE_CHANGES = ['product', 'uom', 'production_type', 'production_location', 'warehouse',
'production_moves', 'moves', 'production_state', 'company', 'quantity']
class UnitLoad(ModelView, ModelSQL):
@ -34,6 +32,11 @@ class UnitLoad(ModelView, ModelSQL):
code_readonly = fields.Function(fields.Boolean('Code Readonly'),
'get_code_readonly')
code_length = fields.Integer('Code Length', select=True, readonly=True)
company = fields.Many2One('company.company', 'Company', required=True,
states={'readonly': Eval('state') != 'draft'},
domain=[('id', If(Eval('context', {}).contains('company'), '=', '!='),
Eval('context', {}).get('company', -1))],
depends=['state'], select=True)
product = fields.Many2One('product.product', 'Product',
select=True, required=True,
ondelete='RESTRICT')
@ -41,13 +44,18 @@ class UnitLoad(ModelView, ModelSQL):
'on_change_with_uom')
uom_digits = fields.Function(fields.Integer('UOM Digits'),
'on_change_with_uom_digits')
quantity = fields.Function(fields.Float('Quantity'),
'get_quantity',
searcher='search_quantity')
forecast_quantity = fields.Function(fields.Float('Forecast Quantity'),
'get_quantity',
searcher='search_quantity')
moves = fields.One2Many('stock.move', 'unit_load', 'Moves', readonly=True)
quantity = fields.Function(fields.Float('Quantity', digits=(16, Eval('uom_digits', 2)),
states={'readonly': Eval('production_state') == 'done'},
depends=['uom_digits', 'production_state']),
'get_quantity', setter='set_quantity', searcher='search_quantity')
forecast_quantity = fields.Function(
fields.Float('Forecast Quantity', digits=(16, Eval('uom_digits', 2)),
depends=['uom_digits']),
'get_quantity', searcher='search_quantity')
moves = fields.One2Many('stock.move', 'unit_load', 'Moves',
readonly=True,
domain=[('company', '=', Eval('company', -1))],
depends=['company'])
location = fields.Function(fields.Many2One('stock.location', 'Location',
depends=['moves']),
'get_location')
@ -59,22 +67,61 @@ class UnitLoad(ModelView, ModelSQL):
domain=[('unit_load', '=', Eval('id'))],
depends=['id']),
'get_last_moves')
ul_moves = fields.Function(
fields.One2Many('stock.unit_load.move', 'unit_load', 'UL moves',
readonly=True,
context={'unit_load': Eval('id')},
depends=['id']),
'get_ul_moves')
ul_moves = fields.One2Many('stock.unit_load.move', 'unit_load', 'UL moves', readonly=True,
states={'invisible': Eval('production_state') != 'done'},
depends=['production_state'])
state = fields.Function(fields.Selection([
('staging', 'Staging'),
('draft', 'Draft'),
('assigned', 'Assigned'),
('done', 'Done'),
('cancel', 'Canceled')], 'State', select=True, readonly=True),
'get_state', searcher='search_state')
production_locations = fields.Function(
fields.One2Many('stock.location', None, 'Production locations'),
'get_production_locations')
'on_change_with_state', searcher='search_state')
production_type = fields.Function(
fields.Selection([('location', 'Location')], 'Production type',
states={'readonly': (Eval('production_state') == 'done')},
depends=['production_state']),
'get_production_type', setter='set_production_type')
warehouse = fields.Many2One('stock.location', 'Warehouse',
domain=[('type', '=', 'warehouse')],
states={'readonly': Eval('production_state') == 'done'},
depends=['production_state'])
warehouse_production = fields.Function(
fields.Many2One('stock.location', 'Warehouse production',
domain=[('type', '=', 'production')]),
'on_change_with_warehouse_production')
production_location = fields.Many2One('stock.location', 'Production location',
domain=[('type', '=', 'production'),
If(Bool(Eval('warehouse')),
('parent', 'child_of', Eval('warehouse_production')),
())],
states={'readonly': (Eval('production_state') == 'done'),
'required': Eval('production_type') == 'location',
'invisible': Eval('production_type') != 'location'},
depends=['production_state', 'production_type', 'warehouse',
'warehouse_production'])
production_state = fields.Function(
fields.Selection([('running', 'Running'),
('done', 'Done')], 'Production state'),
'get_production_state')
production_moves = fields.Function(
fields.One2Many('stock.move', 'unit_load', 'Production moves',
domain=[['OR',
[('product', '=', Eval('product')),
('from_location.type', '=', 'production'),
('to_location.type', '=', 'storage')],
[('product', '!=', Eval('product')),
['OR',
[('from_location.type', '=', 'production'),
('to_location.type', '=', 'storage')],
[('to_location.type', '=', 'production'),
('from_location.type', '=', 'storage')]]
]],
('company', '=', Eval('company', -1))],
states={'readonly': Eval('production_state') == 'done',
'invisible': Eval('production_state') == 'done'},
depends=['production_state', 'company', 'product']),
'get_production_moves', setter='set_production_moves')
@classmethod
def __setup__(cls):
@ -89,13 +136,13 @@ class UnitLoad(ModelView, ModelSQL):
'move_try': {'icon': 'tryton-go-next',
'invisible': Eval('state') != 'done'},
'assign': {'icon': 'tryton-go-next',
'invisible': Or(Eval('state') != 'draft', Not(Bool(Eval('moves', []))))},
'invisible': (Eval('state') != 'draft')},
'do': {'icon': 'tryton-ok',
'invisible': Eval('state') != 'assigned'},
'cancel': {'icon': 'tryton-cancel',
'invisible': Or(Eval('state').in_(['cancel', 'done']), Not(Bool(Eval('moves', []))))},
'invisible': Eval('state').in_(['cancel', 'done'])},
'draft': {'icon': 'tryton-clear',
'invisible': Eval('state') != 'cancel'}
'invisible': Eval('state') != 'cancel'},
})
@staticmethod
@ -107,6 +154,10 @@ class UnitLoad(ModelView, ModelSQL):
def get_code_readonly(self, name):
return True
@staticmethod
def default_company():
return Transaction().context.get('company')
@classmethod
def default_state(cls):
return 'draft'
@ -243,6 +294,10 @@ class UnitLoad(ModelView, ModelSQL):
location_ids = list(location_ids)
return location_ids, wh_to_add, storage_to_remove
@classmethod
def set_quantity(cls, records, name, value):
pass
@classmethod
def search_quantity(cls, name, domain=None):
location_ids = Transaction().context.get('locations')
@ -314,19 +369,13 @@ class UnitLoad(ModelView, ModelSQL):
locations[key[-1]] = key[0]
return locations
def get_ul_moves(self, name=None):
pool = Pool()
Move = pool.get('stock.unit_load.move')
if not Transaction().context.get('unit_load'):
Transaction().context['unit_load'] = self.id
moves = Move.search([])
return [m.id for m in moves]
def get_last_date(self, name=None):
if not self.moves:
return None
return max(m.effective_date or m.planned_date for m in self.moves if m.state != 'cancel')
_moves = [m for m in self.moves if m.state != 'cancel' and m.product.id == self.product.id]
if not _moves:
return None
return max(m.effective_date or m.planned_date for m in _moves)
def get_last_moves(self, name=None):
if not self.moves:
@ -344,31 +393,52 @@ class UnitLoad(ModelView, ModelSQL):
values.append(move.id)
return values
def get_production_type(self, name=None):
if self.production_location:
return 'location'
return None
@classmethod
def get_production_locations(cls, records, name=None):
def default_production_type(cls):
pool = Pool()
Move = pool.get('stock.move')
Location = pool.get('stock.location')
Configuration = pool.get('stock.configuration')
return Configuration(1).ul_production_type or 'location'
move = Move.__table__()
ul = cls.__table__()
location = Location.__table__()
cursor = Transaction().cursor
@classmethod
def set_production_type(cls, records, name, value):
pass
values = {r.id: [] for r in records}
for sub_ids in grouped_slice([r.id for r in records]):
red_sql = reduce_ids(ul.id, sub_ids)
cursor.execute(*move.join(ul, condition=(move.unit_load == ul.id)
).join(location, condition=(move.from_location == location.id)
).select(ul.id, move.from_location,
where=red_sql & (location.type == 'production') &
(move.state != 'cancel'))
)
for ul, loc in cursor.fetchall():
values[ul].append(loc)
@fields.depends('warehouse')
def on_change_with_warehouse_production(self, name=None):
if self.warehouse:
return self.warehouse.production_location.id
return None
@classmethod
def default_production_state(cls):
return 'running'
def get_production_state(self, name=None):
if (self.production_moves and
all(m.state == 'done' for m in self.production_moves)):
return 'done'
return 'running'
def get_production_moves(self, name=None):
if not self.moves:
return []
values = [m.id for m in self.moves if m.from_location.type == 'production'] or []
if self.production_type == 'location':
values.extend([m.id for m in self.moves if m.to_location.id == self.production_location.id
and m.product.id != self.product.id])
return values
@classmethod
def set_production_moves(cls, records, name, value):
if not value:
return
cls.write(records, {'moves': value})
@classmethod
def move(cls, records, to_location, at_date=None):
pool = Pool()
@ -433,7 +503,8 @@ class UnitLoad(ModelView, ModelSQL):
def _get_group_move_key(self, moves, move):
return (move.to_location.id, move.product.id)
def get_state(self, name=None):
@fields.depends('moves')
def on_change_with_state(self, name=None):
pool = Pool()
Move = pool.get('stock.move')
@ -476,53 +547,112 @@ class UnitLoad(ModelView, ModelSQL):
moves = [m for r in records for m in r.last_moves]
Move.write(moves, {'planned_date': value})
@classmethod
def delete(cls, records):
pool = Pool()
Move = pool.get('stock.move')
Move.cancel([m for r in records for m in r.moves])
Move.delete([m for r in records for m in r.moves])
super(UnitLoad, cls).delete(records)
@classmethod
@ModelView.button_action('stock_unit_load.wizard_move_unit_load')
def move_try(cls, records):
pass
@classmethod
@ModelView.button
def draft(cls, records):
pool = Pool()
Move = pool.get('stock.move')
moves = [m for r in records for m in r.last_moves]
moves = [m for r in records for m in r.moves if m.state in ['cancel', 'assigned']]
if moves:
Move.draft(moves)
@classmethod
@ModelView.button
def cancel(cls, records):
pool = Pool()
Move = pool.get('stock.move')
moves = [m for r in records for m in r.last_moves]
moves = [m for r in records for m in r.moves if m.state in ['draft', 'assigned']]
if moves:
Move.cancel(moves)
@classmethod
@ModelView.button
def assign(cls, records):
pool = Pool()
Move = pool.get('stock.move')
moves = [m for r in records for m in r.last_moves]
moves = [m for r in records for m in r.moves if m.state == 'draft']
cls.check_origin(records)
if moves:
Move.assign(moves)
@classmethod
@ModelView.button
def do(cls, records):
pool = Pool()
Move = pool.get('stock.move')
moves = [m for r in records for m in r.last_moves]
moves = [m for r in records for m in r.moves if m.state in ['draft', 'assigned']]
cls.check_origin(records)
if moves:
Move.do(moves)
@classmethod
def check_origin(cls, records):
moves = [m for r in records for m in r.last_moves]
moves = [m for r in records for m in r.moves if m.state in ['draft', 'assigned']]
for move in moves:
if move.origin:
cls.raise_user_error('state_origin', (move.unit_load.rec_name,
move.origin.rec_name))
@fields.depends(*MOVE_CHANGES)
def on_change_product(self):
self.explode_production_moves()
@fields.depends(*MOVE_CHANGES)
def on_change_quantity(self):
self.explode_production_moves()
@fields.depends(*MOVE_CHANGES)
def on_change_production_location(self):
self.explode_production_moves()
@ModelView.button_change(*MOVE_CHANGES)
def reset_production_moves(self):
self.explode_production_moves()
def explode_production_moves(self):
if self.production_state == 'done':
return
if not self._check_production_data():
self.production_moves = []
return
move = self._get_production_move()
self.production_moves = [move]
def _get_production_move(self):
pool = Pool()
Move = pool.get('stock.move')
Date = pool.get('ir.date')
move = Move(company=self.company,
from_location=self.production_location,
product=self.product,
quantity=self.quantity,
unit_load=self,
planned_date=Date.today(),
currency=self.company.currency if self.company else None,
state='draft')
move.on_change_product()
return move
def _check_production_data(self):
return not (
not self.product or
(self.production_type == 'location' and not self.production_location) or
not self.quantity)
class UnitLoadMove(ModelSQL, ModelView):
"""Unit load movement"""
@ -550,29 +680,27 @@ class UnitLoadMove(ModelSQL, ModelView):
Move = pool.get('stock.move')
UL = pool.get('stock.unit_load')
move = Move.__table__()
ul = UL.__table__()
ul_id = Transaction().context.get('unit_load')
ul = UL(ul_id)
where_clause = (move.state != 'cancel')
if getattr(ul, 'product', None):
where_clause &= (move.product == ul.product.id)
date_column = Coalesce(move.effective_date, move.planned_date
).as_('date')
return move.select(
return move.join(ul, condition=(move.product == ul.product)
).select(
Max(move.id).as_('id'),
move.unit_load,
move.state,
move.from_location,
move.to_location,
Literal(0).as_('create_uid'),
Now().as_('create_date'),
Max(move.create_date).as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
date_column,
where=(move.unit_load == ul_id)
& (Coalesce(move.effective_date, move.planned_date) != Null)
& where_clause,
group_by=(date_column, move.unit_load, move.state, move.from_location, move.to_location))
where=(Coalesce(move.effective_date, move.planned_date) != Null)
& where_clause,
group_by=(date_column, move.unit_load, move.state,
move.from_location, move.to_location))
class MoveUnitLoadStart(ModelView):

View File

@ -5,5 +5,7 @@
<xpath expr="/form" position="inside">
<label name="unit_load_sequence"/>
<field name="unit_load_sequence"/>
<label name="ul_production_type"/>
<field name="ul_production_type"/>
</xpath>
</data>

View File

@ -1,14 +1,28 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<form string="Unit load">
<form string="Unit load" col="6" cursor="warehouse">
<label name="code"/>
<field name="code"/>
<label name="company"/>
<field name="company"/>
<label name="warehouse"/>
<field name="warehouse"/>
<label name="production_type"/>
<field name="production_type"/>
<group id="prod_type" colspan="2" col="2">
<label name="production_location"/>
<field name="production_location"/>
</group>
<label name="product"/>
<field name="product"/>
<newline />
<notebook>
<label name="quantity"/>
<field name="quantity"/>
<label name="uom"/>
<field name="uom"/>
<notebook colspan="6">
<page id="general" string="General" col="6">
<field name="production_moves" colspan="6"/>
<field name="ul_moves" colspan="6"/>
</page>
<page id="moves" string="Product moves">
@ -16,13 +30,20 @@
<field name="last_moves" colspan="4"/>
</page>
<page id="others" string="Other info">
<label name="warehouse_production"/>
<field name="warehouse_production"/>
<label name="location"/>
<field name="location"/>
</page>
</notebook>
<group col="4" colspan="6" id="state_buttons">
<label name="state"/>
<field name="state"/>
<group col="2" colspan="2" id="states" yfill="1">
<label name="state"/>
<field name="state"/>
<label name="production_state"/>
<field name="production_state"/>
</group>
<group col="5" colspan="2" id="buttons">
<button string="Cancel" name="cancel" />
<button string="Reset to Draft" name="draft"/>