diff --git a/CHANGELOG b/CHANGELOG index 535c5642e6..6e6a890304 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +* Add Lead Time of Production +* Add Start Dates on Production + Version 4.0.0 - 2016-05-02 * Bug fixes (see mercurial logs for details) * Rename Production code into number diff --git a/__init__.py b/__init__.py index 0540b43f09..50e92f86ec 100644 --- a/__init__.py +++ b/__init__.py @@ -22,6 +22,7 @@ def register(): AssignFailed, Product, ProductBom, + ProductionLeadTime, Location, Move, module='production', type_='model') diff --git a/product.py b/product.py index a2a3f3a1b8..0b23de9589 100644 --- a/product.py +++ b/product.py @@ -3,11 +3,11 @@ from sql import Null from sql.conditionals import Case -from trytond.model import ModelView, ModelSQL, fields +from trytond.model import ModelView, ModelSQL, MatchMixin, fields from trytond.pyson import Eval, Get, If, Bool from trytond.pool import PoolMeta -__all__ = ['Product', 'ProductBom'] +__all__ = ['Product', 'ProductBom', 'ProductionLeadTime'] class Product: @@ -22,6 +22,14 @@ class Product: 'type', 'service').in_(['service', None]))), }, depends=['type']) + lead_times = fields.One2Many('production.lead_time', + 'product', 'Lead Times', order=[('sequence', 'ASC'), ('id', 'ASC')], + states={ + 'invisible': (Eval('type', 'service').in_(['service', None]) + & (Eval('_parent_template', {}).get( + 'type', 'service').in_(['service', None]))), + }, + depends=['type']) @classmethod def __setup__(cls): @@ -91,3 +99,34 @@ class ProductBom(ModelSQL, ModelView): def order_sequence(tables): table, _ = tables[None] return [Case((table.sequence == Null, 0), else_=1), table.sequence] + + +class ProductionLeadTime(ModelSQL, ModelView, MatchMixin): + 'Production Lead Time' + __name__ = 'production.lead_time' + + product = fields.Many2One('product.product', 'Product', + ondelete='CASCADE', select=True, required=True, + domain=[ + ('type', '!=', 'service'), + ]) + sequence = fields.Integer('Sequence') + bom = fields.Many2One('production.bom', 'BOM', ondelete='CASCADE', + domain=[ + ('output_products', '=', If(Bool(Eval('product')), + Eval('product', -1), + Get(Eval('_parent_product', {}), 'id', 0))), + ], + depends=['product']) + lead_time = fields.TimeDelta('Lead Time') + + @classmethod + def __setup__(cls): + super(ProductionLeadTime, cls).__setup__() + cls._order.insert(0, ('product', 'ASC')) + cls._order.insert(0, ('sequence', 'ASC')) + + @classmethod + def order_sequence(cls, tables): + table, _ = tables[None] + return [Case((table.sequence == Null, 0), else_=1), table.sequence] diff --git a/product.xml b/product.xml index e2207d2e74..1e3e3ba75f 100644 --- a/product.xml +++ b/product.xml @@ -44,5 +44,40 @@ this repository contains the full copyright notices and license terms. --> product_form + + + production.lead_time + tree + production_lead_time_list + + + + production.lead_time + tree + production_lead_time_list_sequence + + + + production.lead_time + form + production_lead_time_form + + + + + + + + + + + + + + + + + diff --git a/production.py b/production.py index c663b90774..275a193034 100644 --- a/production.py +++ b/production.py @@ -2,6 +2,8 @@ # this repository contains the full copyright notices and license terms. from decimal import Decimal +from sql import Null + from trytond.model import ModelView, ModelSQL, Workflow, fields from trytond.wizard import Wizard, StateTransition, StateView, Button from trytond.pyson import Eval, Bool, If, Id @@ -38,6 +40,17 @@ class Production(Workflow, ModelSQL, ModelView): 'readonly': Eval('state').in_(['cancel', 'done']), }, depends=['state']) + planned_start_date = fields.Date('Planned Start Date', + states={ + 'readonly': ~Eval('state').in_(['request', 'draft']), + 'required': Bool(Eval('planned_date')), + }, + depends=['state', 'planned_date']) + effective_start_date = fields.Date('Effective Start Date', + states={ + 'readonly': Eval('state').in_(['cancel', 'running', 'done']), + }, + depends=['state']) company = fields.Many2One('company.company', 'Company', required=True, states={ 'readonly': ~Eval('state').in_(['request', 'draft']), @@ -207,6 +220,7 @@ class Production(Workflow, ModelSQL, ModelView): def __register__(cls, module_name): TableHandler = backend.get('TableHandler') table_h = TableHandler(cls, module_name) + table = cls.__table__() # Migration from 3.8: rename code into number if table_h.column_exist('code'): @@ -214,6 +228,14 @@ class Production(Workflow, ModelSQL, ModelView): super(Production, cls).__register__(module_name) + # Migration from 4.0: fill planned_start_date + cursor = Transaction().connection.cursor() + cursor.execute(*table.update( + [table.planned_start_date], + [table.planned_date], + where=(table.planned_start_date == Null) + & (table.planned_date != Null))) + @staticmethod def default_state(): return 'draft' @@ -237,6 +259,17 @@ class Production(Workflow, ModelSQL, ModelView): def default_company(): return Transaction().context.get('company') + @fields.depends('planned_date', 'product', 'bom') + def on_change_with_planned_start_date(self, pattern=None): + if self.planned_date and self.product: + if pattern is None: + pattern = {} + pattern.setdefault('bom', self.bom.id if self.bom else None) + for line in self.product.lead_times: + if line.match(pattern): + return self.planned_date - line.lead_time + return self.planned_date + def _move(self, from_location, to_location, company, product, uom, quantity): Move = Pool().get('stock.move') @@ -486,7 +519,7 @@ class Production(Workflow, ModelSQL, ModelView): def _get_move_planned_date(self): "Return the planned dates for input and output moves" - return self.planned_date, self.planned_date + return self.planned_start_date, self.planned_date def _set_move_planned_date(self): "Set planned date of moves for the shipments" @@ -541,7 +574,11 @@ class Production(Workflow, ModelSQL, ModelView): def run(cls, productions): pool = Pool() Move = pool.get('stock.move') + Date = pool.get('ir.date') Move.do([m for p in productions for m in p.inputs]) + cls.write([p for p in productions if not p.effective_start_date], { + 'effective_start_date': Date.today(), + }) @classmethod @ModelView.button diff --git a/stock.py b/stock.py index 724e0132b1..abdf7ca91f 100644 --- a/stock.py +++ b/stock.py @@ -37,7 +37,7 @@ class Move: def set_effective_date(self): if not self.effective_date and self.production_input: - self.effective_date = self.production_input.effective_date + self.effective_date = self.production_input.effective_start_date if not self.effective_date and self.production_output: self.effective_date = self.production_output.effective_date super(Move, self).set_effective_date() diff --git a/tests/scenario_production.rst b/tests/scenario_production.rst index 6165f5f797..04d1a0b95a 100644 --- a/tests/scenario_production.rst +++ b/tests/scenario_production.rst @@ -13,6 +13,7 @@ Imports:: >>> from trytond.modules.production.production import BOM_CHANGES >>> today = datetime.date.today() >>> yesterday = today - relativedelta(days=1) + >>> before_yesterday = yesterday - relativedelta(days=1) Create database:: @@ -104,6 +105,13 @@ Create Bill of Material:: >>> product.boms.append(ProductBom(bom=bom)) >>> product.save() + >>> ProductionLeadTime = Model.get('production.lead_time') + >>> production_lead_time = ProductionLeadTime() + >>> production_lead_time.product = product + >>> production_lead_time.bom = bom + >>> production_lead_time.lead_time = datetime.timedelta(1) + >>> production_lead_time.save() + Create an Inventory:: >>> Inventory = Model.get('stock.inventory') @@ -130,9 +138,12 @@ Make a production:: >>> Production = Model.get('production') >>> production = Production() + >>> production.planned_date = today >>> production.product = product >>> production.bom = bom >>> production.quantity = 2 + >>> production.planned_start_date == yesterday + True >>> sorted([i.quantity for i in production.inputs]) == [10, 300] True >>> output, = production.outputs @@ -177,11 +188,12 @@ Do the production:: >>> product.quantity == 2 True -Make a production with effective date yesterday:: +Make a production with effective date yesterday and running the day before:: >>> Production = Model.get('production') >>> production = Production() >>> production.effective_date = yesterday + >>> production.effective_start_date = before_yesterday >>> production.product = product >>> production.bom = bom >>> production.quantity = 2 @@ -190,7 +202,7 @@ Make a production with effective date yesterday:: True >>> production.click('run') >>> production.reload() - >>> all(i.effective_date == yesterday for i in production.inputs) + >>> all(i.effective_date == before_yesterday for i in production.inputs) True >>> production.click('done') >>> production.reload() diff --git a/view/product_form.xml b/view/product_form.xml index f23d9c2713..aed74c069e 100644 --- a/view/product_form.xml +++ b/view/product_form.xml @@ -3,9 +3,11 @@ this repository contains the full copyright notices and license terms. --> - + + diff --git a/view/production_calendar.xml b/view/production_calendar.xml index c57ec8176a..04d8238479 100644 --- a/view/production_calendar.xml +++ b/view/production_calendar.xml @@ -1,7 +1,9 @@ - + diff --git a/view/production_form.xml b/view/production_form.xml index a4c8125a90..ba0a770f3d 100644 --- a/view/production_form.xml +++ b/view/production_form.xml @@ -8,8 +8,8 @@ this repository contains the full copyright notices and license terms. -->