Files
tryton/modules/production/production.py
T

884 lines
32 KiB
Python
Raw Normal View History

2014-12-02 12:55:39 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2020-10-11 16:12:36 +02:00
from collections import defaultdict
from datetime import timedelta
2012-05-13 23:00:54 +02:00
from decimal import Decimal
from itertools import chain, groupby
2012-05-13 23:00:54 +02:00
2021-02-25 21:13:56 +01:00
from sql.conditionals import Coalesce
from sql.functions import CharLength
2016-06-24 09:22:52 +02:00
from trytond.i18n import gettext
from trytond.model import (
Index, ModelSQL, ModelView, Workflow, dualmethod, fields)
2020-04-10 18:53:44 +02:00
from trytond.modules.company.model import employee_field, set_employee
2020-04-23 21:44:00 +02:00
from trytond.modules.product import price_digits, round_price
2020-07-25 08:19:31 +02:00
from trytond.modules.stock.shipment import ShipmentAssignMixin
2023-01-24 10:53:23 +01:00
from trytond.pool import Pool, PoolMeta
2021-12-11 17:58:43 +01:00
from trytond.pyson import Bool, Eval, If
from trytond.transaction import Transaction
2015-01-06 23:30:51 +01:00
from .exceptions import CostWarning
2012-05-13 23:00:54 +02:00
2020-07-25 08:19:31 +02:00
class Production(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
2012-05-13 23:00:54 +02:00
"Production"
2012-09-24 22:12:50 +02:00
__name__ = 'production'
_rec_name = 'number'
_assign_moves_field = 'inputs'
2012-05-13 23:00:54 +02:00
number = fields.Char("Number", readonly=True)
reference = fields.Char(
"Reference",
2012-05-13 23:00:54 +02:00
states={
'readonly': ~Eval('state').in_(['request', 'draft']),
})
2012-05-13 23:00:54 +02:00
planned_date = fields.Date('Planned Date',
states={
'readonly': Eval('state').in_(['cancelled', 'done']),
})
effective_date = fields.Date('Effective Date',
states={
'readonly': Eval('state').in_(['cancelled', 'done']),
})
2016-06-24 09:22:52 +02:00
planned_start_date = fields.Date('Planned Start Date',
states={
'readonly': ~Eval('state').in_(['request', 'draft']),
'required': Bool(Eval('planned_date')),
})
2016-06-24 09:22:52 +02:00
effective_start_date = fields.Date('Effective Start Date',
states={
'readonly': Eval('state').in_(['cancelled', 'running', 'done']),
})
2012-05-13 23:00:54 +02:00
company = fields.Many2One('company.company', 'Company', required=True,
states={
'readonly': ~Eval('state').in_(['request', 'draft']),
})
2012-05-13 23:00:54 +02:00
warehouse = fields.Many2One('stock.location', 'Warehouse', required=True,
domain=[
('type', '=', 'warehouse'),
],
states={
'readonly': (~Eval('state').in_(['request', 'draft'])
| Eval('inputs', [-1]) | Eval('outputs', [-1])),
})
2012-05-13 23:00:54 +02:00
location = fields.Many2One('stock.location', 'Location', required=True,
domain=[
('type', '=', 'production'),
],
states={
'readonly': (~Eval('state').in_(['request', 'draft'])
| Eval('inputs', [-1]) | Eval('outputs', [-1])),
})
2012-05-13 23:00:54 +02:00
product = fields.Many2One('product.product', 'Product',
domain=[
2017-01-13 20:20:10 +01:00
('producible', '=', True),
2012-05-13 23:00:54 +02:00
],
states={
'readonly': ~Eval('state').in_(['request', 'draft']),
},
context={
'company': Eval('company', -1),
},
depends={'company'})
2012-05-13 23:00:54 +02:00
bom = fields.Many2One('production.bom', 'BOM',
domain=[
('output_products', '=', Eval('product', 0)),
],
states={
'readonly': (~Eval('state').in_(['request', 'draft'])
| ~Eval('warehouse', 0) | ~Eval('location', 0)),
2012-05-13 23:00:54 +02:00
'invisible': ~Eval('product'),
})
2012-05-13 23:00:54 +02:00
uom_category = fields.Function(fields.Many2One(
2023-08-20 12:17:27 +02:00
'product.uom.category', "UoM Category",
help="The category of Unit of Measure."),
2014-01-26 14:29:24 +01:00
'on_change_with_uom_category')
2023-08-19 15:22:30 +02:00
unit = fields.Many2One(
'product.uom', "Unit",
2012-05-13 23:00:54 +02:00
domain=[
('category', '=', Eval('uom_category')),
],
states={
'readonly': ~Eval('state').in_(['request', 'draft']),
'required': Bool(Eval('bom')),
2012-10-15 22:57:13 +02:00
'invisible': ~Eval('product'),
})
2021-08-30 00:27:13 +02:00
quantity = fields.Float(
2023-08-19 15:22:30 +02:00
"Quantity", digits='unit',
2012-05-13 23:00:54 +02:00
states={
'readonly': ~Eval('state').in_(['request', 'draft']),
'required': Bool(Eval('bom')),
2012-10-15 22:57:13 +02:00
'invisible': ~Eval('product'),
2021-08-30 00:27:13 +02:00
})
2015-01-06 23:30:51 +01:00
cost = fields.Function(fields.Numeric('Cost', digits=price_digits,
2014-01-26 14:29:24 +01:00
readonly=True), 'get_cost')
2012-05-13 23:00:54 +02:00
inputs = fields.One2Many('stock.move', 'production_input', 'Inputs',
domain=[
2015-11-15 11:25:30 -03:00
('shipment', '=', None),
2021-10-03 13:45:32 +02:00
('from_location', 'child_of', [Eval('warehouse', -1)], 'parent'),
('to_location', '=', Eval('location', -1)),
2014-12-03 16:13:57 +01:00
('company', '=', Eval('company', -1)),
2012-05-13 23:00:54 +02:00
],
states={
'readonly': (~Eval('state').in_(['request', 'draft', 'waiting'])
| ~Eval('warehouse') | ~Eval('location')),
})
2012-05-13 23:00:54 +02:00
outputs = fields.One2Many('stock.move', 'production_output', 'Outputs',
domain=[
2015-11-15 11:25:30 -03:00
('shipment', '=', None),
2021-10-03 13:45:32 +02:00
('from_location', '=', Eval('location', -1)),
2020-10-11 23:46:52 +02:00
['OR',
2021-10-03 13:45:32 +02:00
('to_location', 'child_of', [Eval('warehouse', -1)], 'parent'),
('to_location.waste_warehouses', '=', Eval('warehouse', -1)),
2020-10-11 23:46:52 +02:00
],
2014-12-03 16:13:57 +01:00
('company', '=', Eval('company', -1)),
2012-05-13 23:00:54 +02:00
],
states={
'readonly': (Eval('state').in_(['done', 'cancelled'])
| ~Eval('warehouse') | ~Eval('location')),
})
2020-04-10 18:53:44 +02:00
assigned_by = employee_field("Assigned By")
run_by = employee_field("Run By")
done_by = employee_field("Done By")
2012-05-13 23:00:54 +02:00
state = fields.Selection([
('request', 'Request'),
('draft', 'Draft'),
('waiting', 'Waiting'),
('assigned', 'Assigned'),
('running', 'Running'),
('done', 'Done'),
('cancelled', 'Cancelled'),
2022-02-15 00:08:47 +01:00
], 'State', readonly=True, sort=False)
2020-04-13 14:49:07 +02:00
origin = fields.Reference(
"Origin", selection='get_origin',
2020-04-13 14:49:07 +02:00
states={
'readonly': ~Eval('state').in_(['request', 'draft']),
})
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
def __setup__(cls):
cls.number.search_unaccented = False
cls.reference.search_unaccented = False
2012-09-24 22:12:50 +02:00
super(Production, cls).__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (t.reference, Index.Similarity())),
Index(
t,
(t.state, Index.Equality()),
where=t.state.in_([
'request', 'draft', 'waiting', 'assigned',
'running'])),
})
2021-02-25 21:13:56 +01:00
cls._order = [
('effective_date', 'ASC NULLS LAST'),
('id', 'ASC'),
]
2012-09-24 22:12:50 +02:00
cls._transitions |= set((
2012-05-13 23:00:54 +02:00
('request', 'draft'),
('draft', 'waiting'),
('waiting', 'assigned'),
('assigned', 'running'),
('running', 'done'),
('running', 'waiting'),
('assigned', 'waiting'),
('waiting', 'waiting'),
('waiting', 'draft'),
('request', 'cancelled'),
('draft', 'cancelled'),
('waiting', 'cancelled'),
('assigned', 'cancelled'),
('cancelled', 'draft'),
2012-05-13 23:00:54 +02:00
))
2012-09-24 22:12:50 +02:00
cls._buttons.update({
2012-05-13 23:00:54 +02:00
'cancel': {
'invisible': ~Eval('state').in_(['request', 'draft',
'assigned']),
2018-02-19 11:38:52 +01:00
'depends': ['state'],
2012-05-13 23:00:54 +02:00
},
'draft': {
'invisible': ~Eval('state').in_(['request', 'waiting',
'cancelled']),
'icon': If(Eval('state') == 'cancelled',
2012-05-13 23:00:54 +02:00
'tryton-clear',
If(Eval('state') == 'request',
2018-08-19 20:02:58 +02:00
'tryton-forward',
'tryton-back')),
2018-02-19 11:38:52 +01:00
'depends': ['state'],
2012-05-13 23:00:54 +02:00
},
2015-03-16 13:00:10 +01:00
'reset_bom': {
'invisible': (~Eval('bom')
| ~Eval('state').in_(['request', 'draft', 'waiting'])),
2018-02-19 11:38:52 +01:00
'depends': ['state', 'bom'],
2015-03-16 13:00:10 +01:00
},
2012-05-13 23:00:54 +02:00
'wait': {
'invisible': ~Eval('state').in_(['draft', 'assigned',
'waiting', 'running']),
'icon': If(Eval('state').in_(['assigned', 'running']),
2018-08-19 20:02:58 +02:00
'tryton-back',
2012-05-13 23:00:54 +02:00
If(Eval('state') == 'waiting',
'tryton-clear',
2018-08-19 20:02:58 +02:00
'tryton-forward')),
2018-02-19 11:38:52 +01:00
'depends': ['state'],
2012-05-13 23:00:54 +02:00
},
'run': {
'invisible': Eval('state') != 'assigned',
2018-02-19 11:38:52 +01:00
'depends': ['state'],
2012-05-13 23:00:54 +02:00
},
'done': {
'invisible': Eval('state') != 'running',
2018-02-19 11:38:52 +01:00
'depends': ['state'],
2012-05-13 23:00:54 +02:00
},
2012-09-10 15:28:23 +02:00
'assign_wizard': {
'invisible': Eval('state') != 'waiting',
2018-02-19 11:38:52 +01:00
'depends': ['state'],
2012-09-10 15:28:23 +02:00
},
2012-05-13 23:00:54 +02:00
'assign_try': {},
'assign_force': {},
})
2020-03-04 18:53:32 +01:00
def get_rec_name(self, name):
items = []
if self.number:
items.append(self.number)
if self.reference:
items.append('[%s]' % self.reference)
if not items:
items.append('(%s)' % self.id)
return ' '.join(items)
@classmethod
def search_rec_name(cls, name, clause):
if clause[1].startswith('!') or clause[1].startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
('number',) + tuple(clause[1:]),
('reference',) + tuple(clause[1:]),
]
@classmethod
def __register__(cls, module_name):
2023-08-19 15:22:30 +02:00
table_h = cls.__table_handler__(module_name)
# Migration from 6.8: rename uom to unit
if (table_h.column_exist('uom')
and not table_h.column_exist('unit')):
table_h.column_rename('uom', 'unit')
super(Production, cls).__register__(module_name)
2023-01-19 11:25:25 +01:00
table = cls.__table__()
2016-06-24 09:22:52 +02:00
cursor = Transaction().connection.cursor()
# Migration from 5.6: rename state cancel to cancelled
cursor.execute(*table.update(
[table.state], ['cancelled'],
where=table.state == 'cancel'))
@classmethod
def order_number(cls, tables):
table, _ = tables[None]
return [CharLength(table.number), table.number]
2021-02-25 21:13:56 +01:00
@classmethod
def order_effective_date(cls, tables):
table, _ = tables[None]
return [Coalesce(
table.effective_start_date, table.effective_date,
table.planned_start_date, table.planned_date)]
2012-09-24 22:12:50 +02:00
@staticmethod
def default_state():
2012-05-13 23:00:54 +02:00
return 'draft'
2012-09-24 22:12:50 +02:00
@classmethod
def default_warehouse(cls):
Location = Pool().get('stock.location')
return Location.get_default_warehouse()
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
def default_location(cls):
Location = Pool().get('stock.location')
warehouse_id = cls.default_warehouse()
2012-05-13 23:00:54 +02:00
if warehouse_id:
2012-09-24 22:12:50 +02:00
warehouse = Location(warehouse_id)
2012-05-13 23:00:54 +02:00
return warehouse.production_location.id
2012-09-24 22:12:50 +02:00
@staticmethod
def default_company():
2012-05-13 23:00:54 +02:00
return Transaction().context.get('company')
@fields.depends('product', 'bom')
def compute_lead_time(self, pattern=None):
pattern = pattern.copy() if pattern is not None else {}
if self.product:
2016-06-24 09:22:52 +02:00
pattern.setdefault('bom', self.bom.id if self.bom else None)
for line in self.product.production_lead_times:
2016-06-24 09:22:52 +02:00
if line.match(pattern):
return line.lead_time or timedelta()
return timedelta()
@fields.depends(
'planned_date', 'state', 'product', methods=['compute_lead_time'])
def set_planned_start_date(self):
if self.state in {'request', 'draft'}:
if self.planned_date and self.product:
self.planned_start_date = (
self.planned_date - self.compute_lead_time())
else:
self.planned_start_date = self.planned_date
@fields.depends(methods=['set_planned_start_date'])
def on_change_planned_date(self):
self.set_planned_start_date()
2016-06-24 09:22:52 +02:00
@fields.depends(
'planned_date', 'planned_start_date', methods=['compute_lead_time'])
def on_change_planned_start_date(self, pattern=None):
if self.planned_start_date and self.product:
planned_date = self.planned_start_date + self.compute_lead_time()
if (not self.planned_date
or self.planned_date < planned_date):
self.planned_date = planned_date
2020-04-13 14:49:07 +02:00
@classmethod
def _get_origin(cls):
'Return list of Model names for origin Reference'
return set()
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
2020-12-27 00:19:30 +01:00
get_name = Model.get_name
2020-04-13 14:49:07 +02:00
models = cls._get_origin()
2020-12-27 00:19:30 +01:00
return [(None, '')] + [(m, get_name(m)) for m in models]
2020-04-13 14:49:07 +02:00
2022-12-26 13:22:06 +01:00
@fields.depends(
'company', 'location', methods=['picking_location', 'output_location'])
2023-08-19 15:22:30 +02:00
def _move(self, type, product, unit, quantity):
2022-12-26 13:22:06 +01:00
pool = Pool()
Move = pool.get('stock.move')
assert type in {'input', 'output'}
2012-09-24 22:12:50 +02:00
move = Move(
product=product,
2023-08-22 13:18:33 +02:00
unit=unit,
2012-09-24 22:12:50 +02:00
quantity=quantity,
2022-12-26 13:22:06 +01:00
company=self.company,
currency=self.company.currency if self.company else None)
if type == 'input':
move.from_location = self.picking_location
move.to_location = self.location
move.production_input = self
else:
move.from_location = self.location
move.to_location = self.output_location
move.production_output = self
2012-09-24 22:12:50 +02:00
return move
2012-05-13 23:00:54 +02:00
2022-12-26 13:22:06 +01:00
@fields.depends(methods=['_move'])
def _explode_move_values(self, type, bom_io, quantity):
2023-08-20 01:39:36 +02:00
move = self._move(type, bom_io.product, bom_io.unit, quantity)
2012-09-24 22:12:50 +02:00
move.unit_price_required = move.on_change_with_unit_price_required()
2014-10-29 20:40:02 +01:00
return move
2012-05-13 23:00:54 +02:00
2022-12-26 13:22:06 +01:00
@fields.depends(
2023-08-19 15:22:30 +02:00
'bom', 'product', 'unit', 'quantity', 'company', 'inputs', 'outputs',
2022-12-26 13:22:06 +01:00
methods=['_explode_move_values'])
2012-09-24 22:12:50 +02:00
def explode_bom(self):
2012-05-13 23:00:54 +02:00
pool = Pool()
2012-09-24 22:12:50 +02:00
Uom = pool.get('product.uom')
2023-08-19 15:22:30 +02:00
if not (self.bom and self.product and self.unit):
2014-10-29 20:40:02 +01:00
return
2012-05-13 23:00:54 +02:00
2023-08-19 15:22:30 +02:00
factor = self.bom.compute_factor(
self.product, self.quantity or 0, self.unit)
2014-10-29 20:40:02 +01:00
inputs = []
2012-09-24 22:12:50 +02:00
for input_ in self.bom.inputs:
quantity = input_.compute_quantity(factor)
2022-12-26 13:22:06 +01:00
move = self._explode_move_values('input', input_, quantity)
2014-10-29 20:40:02 +01:00
if move:
inputs.append(move)
2023-08-22 13:18:33 +02:00
quantity = Uom.compute_qty(
2023-08-20 01:39:36 +02:00
input_.unit, quantity, input_.product.default_uom,
2023-08-22 13:18:33 +02:00
round=False)
2014-10-29 20:40:02 +01:00
self.inputs = inputs
2012-05-13 23:00:54 +02:00
2014-10-29 20:40:02 +01:00
outputs = []
2012-09-24 22:12:50 +02:00
for output in self.bom.outputs:
quantity = output.compute_quantity(factor)
2022-12-26 13:22:06 +01:00
move = self._explode_move_values('output', output, quantity)
2014-10-29 20:40:02 +01:00
if move:
move.unit_price = Decimal(0)
move.currency = self.company.currency
2014-10-29 20:40:02 +01:00
outputs.append(move)
self.outputs = outputs
2012-05-13 23:00:54 +02:00
2014-01-26 14:29:24 +01:00
@fields.depends('warehouse')
2012-09-24 22:12:50 +02:00
def on_change_warehouse(self):
2014-10-29 20:40:02 +01:00
self.location = None
2012-09-24 22:12:50 +02:00
if self.warehouse:
2014-10-29 20:40:02 +01:00
self.location = self.warehouse.production_location
2012-05-13 23:00:54 +02:00
@fields.depends(
2023-08-19 15:22:30 +02:00
'product', 'unit', methods=['explode_bom', 'set_planned_start_date'])
2012-09-24 22:12:50 +02:00
def on_change_product(self):
if self.product:
category = self.product.default_uom.category
2023-08-19 15:22:30 +02:00
if not self.unit or self.unit.category != category:
self.unit = self.product.default_uom
2012-05-13 23:00:54 +02:00
else:
2014-10-29 20:40:02 +01:00
self.bom = None
2023-08-19 15:22:30 +02:00
self.unit = None
2014-10-29 20:40:02 +01:00
self.explode_bom()
self.set_planned_start_date()
2012-05-13 23:00:54 +02:00
2014-01-26 14:29:24 +01:00
@fields.depends('product')
2012-09-24 22:12:50 +02:00
def on_change_with_uom_category(self, name=None):
2023-03-26 09:49:00 +02:00
return self.product.default_uom.category if self.product else None
2012-05-13 23:00:54 +02:00
@fields.depends(methods=['explode_bom', 'set_planned_start_date'])
2012-09-24 22:12:50 +02:00
def on_change_bom(self):
2014-10-29 20:40:02 +01:00
self.explode_bom()
# Product's production lead time depends on bom
self.set_planned_start_date()
2012-09-24 22:12:50 +02:00
2019-05-06 11:35:11 +02:00
@fields.depends(methods=['explode_bom'])
2023-08-19 15:22:30 +02:00
def on_change_unit(self):
2015-03-16 13:00:10 +01:00
self.explode_bom()
2012-09-24 22:12:50 +02:00
2019-05-06 11:35:11 +02:00
@fields.depends(methods=['explode_bom'])
2012-09-24 22:12:50 +02:00
def on_change_quantity(self):
2015-03-16 13:00:10 +01:00
self.explode_bom()
2020-12-05 12:09:10 +01:00
@ModelView.button_change(methods=['explode_bom'])
2015-03-16 13:00:10 +01:00
def reset_bom(self):
self.explode_bom()
2012-09-24 22:12:50 +02:00
def get_cost(self, name):
cost = Decimal(0)
for input_ in self.inputs:
if input_.state == 'cancelled':
continue
cost_price = input_.get_cost_price()
2012-09-24 22:12:50 +02:00
cost += (Decimal(str(input_.internal_quantity)) * cost_price)
2020-04-23 21:44:00 +02:00
return round_price(cost)
2012-05-13 23:00:54 +02:00
2014-01-26 14:29:24 +01:00
@fields.depends('inputs')
2012-09-24 22:12:50 +02:00
def on_change_with_cost(self):
Uom = Pool().get('product.uom')
2012-05-13 23:00:54 +02:00
cost = Decimal(0)
2012-09-24 22:12:50 +02:00
if not self.inputs:
2012-05-13 23:00:54 +02:00
return cost
2012-09-24 22:12:50 +02:00
for input_ in self.inputs:
if (input_.product is None
2023-08-22 13:18:33 +02:00
or input_.unit is None
or input_.quantity is None
or input_.state == 'cancelled'):
2012-05-13 23:00:54 +02:00
continue
2012-09-24 22:12:50 +02:00
product = input_.product
2023-08-22 13:18:33 +02:00
quantity = Uom.compute_qty(
input_.unit, input_.quantity, product.default_uom)
2012-05-13 23:00:54 +02:00
cost += Decimal(str(quantity)) * product.cost_price
return cost
@dualmethod
def set_moves(cls, productions):
pool = Pool()
Move = pool.get('stock.move')
to_save = []
for production in productions:
if not production.bom:
if production.product:
move = production._move(
2023-08-19 15:22:30 +02:00
'output', production.product, production.unit,
production.quantity)
if move:
move.unit_price = Decimal(0)
move.currency = production.company.currency
to_save.append(move)
continue
factor = production.bom.compute_factor(
2023-08-19 15:22:30 +02:00
production.product, production.quantity, production.unit)
for input_ in production.bom.inputs:
quantity = input_.compute_quantity(factor)
product = input_.product
2020-10-12 19:32:02 +02:00
move = production._move(
2023-08-20 01:39:36 +02:00
'input', product, input_.unit, quantity)
2012-09-24 22:12:50 +02:00
if move:
to_save.append(move)
for output in production.bom.outputs:
quantity = output.compute_quantity(factor)
product = output.product
2022-12-26 13:22:06 +01:00
move = production._move(
2023-08-20 01:39:36 +02:00
'output', product, output.unit, quantity)
if move:
2012-10-15 22:57:13 +02:00
move.unit_price = Decimal(0)
move.currency = production.company.currency
to_save.append(move)
Move.save(to_save)
cls._set_move_planned_date(productions)
2012-05-13 23:00:54 +02:00
@classmethod
def set_cost_from_moves(cls):
pool = Pool()
Move = pool.get('stock.move')
productions = set()
moves = Move.search([
('production_cost_price_updated', '=', True),
('production_input', '!=', None),
],
order=[('effective_date', 'ASC')])
for move in moves:
if move.production_input not in productions:
cls.__queue__.set_cost([move.production_input])
productions.add(move.production_input)
Move.write(moves, {'production_cost_price_updated': False})
2018-06-18 21:49:37 +02:00
@classmethod
def set_cost(cls, productions):
pool = Pool()
Uom = pool.get('product.uom')
Move = pool.get('stock.move')
Warning = pool.get('res.user.warning')
2018-06-18 21:49:37 +02:00
moves = []
for production in productions:
sum_ = Decimal(0)
prices = {}
2020-10-11 16:12:36 +02:00
cost = production.cost
input_quantities = defaultdict(Decimal)
input_costs = defaultdict(Decimal)
for input_ in production.inputs:
if input_.state == 'cancelled':
continue
cost_price = input_.get_cost_price()
2020-10-11 16:12:36 +02:00
input_quantities[input_.product] += (
Decimal(str(input_.internal_quantity)))
input_costs[input_.product] += (
Decimal(str(input_.internal_quantity)) * cost_price)
outputs = []
2018-06-18 21:49:37 +02:00
for output in production.outputs:
if (output.to_location.type == 'lost_found'
or output.state == 'cancelled'):
2020-10-11 23:46:52 +02:00
continue
2020-10-11 16:12:36 +02:00
product = output.product
if input_quantities.get(output.product):
cost_price = (
input_costs[product] / input_quantities[product])
unit_price = round_price(Uom.compute_price(
2023-08-22 13:18:33 +02:00
product.default_uom, cost_price, output.unit))
if (output.unit_price != unit_price
or output.currency != production.company.currency):
2020-10-11 16:12:36 +02:00
output.unit_price = unit_price
output.currency = production.company.currency
2020-10-11 16:12:36 +02:00
moves.append(output)
cost -= min(
unit_price * Decimal(str(output.quantity)), cost)
else:
outputs.append(output)
for output in outputs:
product = output.product
2021-09-19 00:32:31 +02:00
list_price = product.list_price_used
if list_price is None:
warning_name = Warning.format(
'production_missing_list_price', [product])
if Warning.check(warning_name):
raise CostWarning(warning_name,
gettext(
'production.msg_missing_product_list_price',
product=product.rec_name,
production=production.rec_name))
continue
product_price = (Decimal(str(output.quantity))
* Uom.compute_price(
2023-08-22 13:18:33 +02:00
product.default_uom, list_price, output.unit))
prices[output] = product_price
sum_ += product_price
if not sum_ and production.product:
prices.clear()
2020-10-11 16:12:36 +02:00
for output in outputs:
if output.product == production.product:
quantity = Uom.compute_qty(
2023-08-22 13:18:33 +02:00
output.unit, output.quantity,
output.product.default_uom, round=False)
quantity = Decimal(str(quantity))
prices[output] = quantity
sum_ += quantity
2020-10-11 16:12:36 +02:00
for output in outputs:
if sum_:
ratio = prices.get(output, 0) / sum_
else:
2020-10-11 16:12:36 +02:00
ratio = Decimal(1) / len(outputs)
if not output.quantity:
unit_price = Decimal(0)
else:
quantity = Decimal(str(output.quantity))
unit_price = round_price(cost * ratio / quantity)
if (output.unit_price != unit_price
or output.currency != production.company.currency):
output.unit_price = unit_price
output.currency = production.company.currency
2018-06-18 21:49:37 +02:00
moves.append(output)
Move.save(moves)
2012-09-24 22:12:50 +02:00
@classmethod
2013-01-06 22:18:13 +01:00
def create(cls, vlist):
2012-09-24 22:12:50 +02:00
Config = Pool().get('production.configuration')
2012-05-13 23:00:54 +02:00
2013-01-06 22:18:13 +01:00
vlist = [x.copy() for x in vlist]
2012-09-24 22:12:50 +02:00
config = Config(1)
2021-04-03 14:27:10 +02:00
default_company = cls.default_company()
2013-01-06 22:18:13 +01:00
for values in vlist:
if values.get('number') is None:
2021-04-03 14:27:10 +02:00
values['number'] = config.get_multivalue(
'production_sequence',
company=values.get('company', default_company)).get()
2013-01-06 22:18:13 +01:00
productions = super(Production, cls).create(vlist)
for production in productions:
production._set_move_planned_date()
return productions
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
2013-12-19 14:45:14 +01:00
def write(cls, *args):
super(Production, cls).write(*args)
for production in sum(args[::2], []):
2012-09-24 22:12:50 +02:00
production._set_move_planned_date()
2012-05-13 23:00:54 +02:00
2015-11-23 10:17:20 +01:00
@classmethod
def copy(cls, productions, default=None):
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
default.setdefault('number', None)
2021-04-05 16:24:13 +02:00
default.setdefault('assigned_by')
default.setdefault('run_by')
default.setdefault('done_by')
default.setdefault('inputs.origin', None)
default.setdefault('outputs.origin', None)
2015-11-23 10:17:20 +01:00
return super(Production, cls).copy(productions, default=default)
2012-09-24 22:12:50 +02:00
def _get_move_planned_date(self):
2012-05-13 23:00:54 +02:00
"Return the planned dates for input and output moves"
2016-06-24 09:22:52 +02:00
return self.planned_start_date, self.planned_date
2012-05-13 23:00:54 +02:00
@dualmethod
def _set_move_planned_date(cls, productions):
2012-05-13 23:00:54 +02:00
"Set planned date of moves for the shipments"
pool = Pool()
2012-09-24 22:12:50 +02:00
Move = pool.get('stock.move')
to_write = []
for production in productions:
dates = production._get_move_planned_date()
input_date, output_date = dates
inputs = [m for m in production.inputs
if m.state not in {'done', 'cancelled'}]
if inputs:
to_write.append(inputs)
to_write.append({
'planned_date': input_date,
})
outputs = [m for m in production.outputs
if m.state not in {'done', 'cancelled'}]
if outputs:
to_write.append(outputs)
to_write.append({
'planned_date': output_date,
})
if to_write:
Move.write(*to_write)
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
2012-05-13 23:00:54 +02:00
@ModelView.button
@Workflow.transition('cancelled')
2012-09-24 22:12:50 +02:00
def cancel(cls, productions):
2012-05-13 23:00:54 +02:00
pool = Pool()
2012-09-24 22:12:50 +02:00
Move = pool.get('stock.move')
2012-12-27 21:54:21 +01:00
Move.cancel([m for p in productions
for m in p.inputs + p.outputs])
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
2012-05-13 23:00:54 +02:00
@ModelView.button
@Workflow.transition('draft')
2012-09-24 22:12:50 +02:00
def draft(cls, productions):
2012-05-13 23:00:54 +02:00
pool = Pool()
2012-09-24 22:12:50 +02:00
Move = pool.get('stock.move')
to_draft, to_delete = [], []
for production in productions:
for move in chain(production.inputs, production.outputs):
2020-06-06 10:30:11 +01:00
if move.state != 'cancelled':
to_draft.append(move)
else:
to_delete.append(move)
Move.draft(to_draft)
Move.delete(to_delete)
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
2012-05-13 23:00:54 +02:00
@ModelView.button
@Workflow.transition('waiting')
2012-09-24 22:12:50 +02:00
def wait(cls, productions):
2012-05-13 23:00:54 +02:00
pool = Pool()
2012-09-24 22:12:50 +02:00
Move = pool.get('stock.move')
2012-12-27 21:54:21 +01:00
Move.draft([m for p in productions
for m in p.inputs + p.outputs])
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
2012-05-13 23:00:54 +02:00
@Workflow.transition('assigned')
2020-04-10 18:53:44 +02:00
@set_employee('assigned_by')
2012-09-24 22:12:50 +02:00
def assign(cls, productions):
2020-07-25 08:19:31 +02:00
pool = Pool()
Move = pool.get('stock.move')
Move.assign([m for p in productions for m in p.assign_moves])
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
2012-05-13 23:00:54 +02:00
@ModelView.button
@Workflow.transition('running')
2020-04-10 18:53:44 +02:00
@set_employee('run_by')
2012-09-24 22:12:50 +02:00
def run(cls, productions):
2012-05-13 23:00:54 +02:00
pool = Pool()
2012-09-24 22:12:50 +02:00
Move = pool.get('stock.move')
2016-06-24 09:22:52 +02:00
Date = pool.get('ir.date')
2012-12-27 21:54:21 +01:00
Move.do([m for p in productions for m in p.inputs])
for company, productions in groupby(
productions, key=lambda p: p.company):
with Transaction().set_context(company=company.id):
today = Date.today()
cls.write([p for p in productions if not p.effective_start_date], {
'effective_start_date': today,
})
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
2012-05-13 23:00:54 +02:00
@ModelView.button
@Workflow.transition('done')
2020-04-10 18:53:44 +02:00
@set_employee('done_by')
2012-09-24 22:12:50 +02:00
def done(cls, productions):
2012-05-13 23:00:54 +02:00
pool = Pool()
2012-09-24 22:12:50 +02:00
Move = pool.get('stock.move')
Date = pool.get('ir.date')
2018-06-18 21:49:37 +02:00
cls.set_cost(productions)
2012-12-27 21:54:21 +01:00
Move.do([m for p in productions for m in p.outputs])
for company, productions in groupby(
productions, key=lambda p: p.company):
with Transaction().set_context(company=company.id):
today = Date.today()
cls.write([p for p in productions if not p.effective_date], {
'effective_date': today,
})
2012-05-13 23:00:54 +02:00
2012-09-24 22:12:50 +02:00
@classmethod
2020-07-25 08:19:31 +02:00
@ModelView.button_action('production.wizard_production_assign')
2018-09-19 00:15:58 +02:00
def assign_wizard(cls, productions):
2012-09-10 15:28:23 +02:00
pass
2020-07-25 08:19:31 +02:00
@dualmethod
2012-05-13 23:00:54 +02:00
@ModelView.button
2012-09-24 22:12:50 +02:00
def assign_try(cls, productions):
2012-05-13 23:00:54 +02:00
pool = Pool()
2012-09-24 22:12:50 +02:00
Move = pool.get('stock.move')
to_assign = [
m for p in productions for m in p.assign_moves
if m.assignation_required]
if Move.assign_try(to_assign):
2012-09-24 22:12:50 +02:00
cls.assign(productions)
2012-05-13 23:00:54 +02:00
else:
to_assign = []
for production in productions:
if any(
m.state in {'staging', 'draft'}
for m in production.assign_moves
if m.assignation_required):
continue
to_assign.append(production)
if to_assign:
cls.assign(to_assign)
@classmethod
def _get_reschedule_planned_start_dates_domain(cls, date):
2021-12-21 23:15:23 +01:00
context = Transaction().context
return [
2021-12-21 23:15:23 +01:00
('company', '=', context.get('company')),
('state', '=', 'waiting'),
('planned_start_date', '<', date),
]
@classmethod
def _get_reschedule_planned_dates_domain(cls, date):
context = Transaction().context
return [
('company', '=', context.get('company')),
('state', '=', 'running'),
('planned_date', '<', date),
]
@classmethod
def reschedule(cls, date=None):
pool = Pool()
Date = pool.get('ir.date')
if date is None:
date = Date.today()
to_reschedule_start_date = cls.search(
cls._get_reschedule_planned_start_dates_domain(date))
to_reschedule_planned_date = cls.search(
cls._get_reschedule_planned_dates_domain(date))
for production in to_reschedule_start_date:
production.planned_start_date = date
production.on_change_planned_start_date()
for production in to_reschedule_planned_date:
production.planned_date = date
cls.save(to_reschedule_start_date + to_reschedule_planned_date)
2020-10-12 19:32:02 +02:00
@property
2022-12-26 13:22:06 +01:00
@fields.depends('warehouse')
2020-10-12 19:32:02 +02:00
def picking_location(self):
if self.warehouse:
return (self.warehouse.production_picking_location
or self.warehouse.storage_location)
@property
2022-12-26 13:22:06 +01:00
@fields.depends('warehouse')
2020-10-12 19:32:02 +02:00
def output_location(self):
if self.warehouse:
return (self.warehouse.production_output_location
or self.warehouse.storage_location)
2023-01-24 10:53:23 +01:00
class Production_Lot(metaclass=PoolMeta):
__name__ = 'production'
@classmethod
@ModelView.button
@Workflow.transition('done')
def done(cls, productions):
pool = Pool()
Lot = pool.get('stock.lot')
Move = pool.get('stock.move')
lots, moves = [], []
for production in productions:
for move in production.outputs:
if not move.lot and move.product.lot_is_required(
move.from_location, move.to_location):
move.add_lot()
if move.lot:
lots.append(move.lot)
moves.append(move)
Lot.save(lots)
Move.save(moves)
super().done(productions)