634 lines
22 KiB
Python
634 lines
22 KiB
Python
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
#this repository contains the full copyright notices and license terms.
|
|
from decimal import Decimal
|
|
|
|
from trytond.model import ModelView, ModelSQL, Workflow, fields
|
|
from trytond.wizard import Wizard, StateTransition, StateView, Button
|
|
from trytond.pyson import Eval, Bool, If, Id
|
|
from trytond.pool import Pool
|
|
from trytond.transaction import Transaction
|
|
|
|
__all__ = ['Production', 'AssignFailed', 'Assign']
|
|
|
|
BOM_CHANGES = ['bom', 'product', 'quantity', 'uom', 'warehouse', 'location',
|
|
'company', 'inputs', 'outputs']
|
|
|
|
|
|
class Production(Workflow, ModelSQL, ModelView):
|
|
"Production"
|
|
__name__ = 'production'
|
|
_rec_name = 'code'
|
|
|
|
code = fields.Char('Code', select=True, readonly=True)
|
|
reference = fields.Char('Reference', select=1,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
},
|
|
depends=['state'])
|
|
planned_date = fields.Date('Planned Date',
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
},
|
|
depends=['state'])
|
|
effective_date = fields.Date('Effective Date',
|
|
states={
|
|
'readonly': Eval('state').in_(['cancel', 'done']),
|
|
},
|
|
depends=['state'])
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
},
|
|
depends=['state'])
|
|
warehouse = fields.Many2One('stock.location', 'Warehouse', required=True,
|
|
domain=[
|
|
('type', '=', 'warehouse'),
|
|
],
|
|
states={
|
|
'readonly': (~Eval('state').in_(['request', 'draft'])
|
|
| Eval('inputs', True) | Eval('outputs', True)),
|
|
},
|
|
depends=['state'])
|
|
location = fields.Many2One('stock.location', 'Location', required=True,
|
|
domain=[
|
|
('type', '=', 'production'),
|
|
],
|
|
states={
|
|
'readonly': (~Eval('state').in_(['request', 'draft'])
|
|
| Eval('inputs', True) | Eval('outputs', True)),
|
|
},
|
|
depends=['state'])
|
|
product = fields.Many2One('product.product', 'Product',
|
|
domain=[
|
|
('type', '!=', 'service'),
|
|
],
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
})
|
|
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)),
|
|
'invisible': ~Eval('product'),
|
|
},
|
|
depends=['product'])
|
|
uom_category = fields.Function(fields.Many2One(
|
|
'product.uom.category', 'Uom Category'),
|
|
'on_change_with_uom_category')
|
|
uom = fields.Many2One('product.uom', 'Uom',
|
|
domain=[
|
|
('category', '=', Eval('uom_category')),
|
|
],
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
'required': Bool(Eval('bom')),
|
|
'invisible': ~Eval('product'),
|
|
},
|
|
depends=['uom_category'])
|
|
unit_digits = fields.Function(fields.Integer('Unit Digits'),
|
|
'on_change_with_unit_digits')
|
|
quantity = fields.Float('Quantity',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
'required': Bool(Eval('bom')),
|
|
'invisible': ~Eval('product'),
|
|
},
|
|
depends=['unit_digits'])
|
|
cost = fields.Function(fields.Numeric('Cost', digits=(16, 4),
|
|
readonly=True), 'get_cost')
|
|
inputs = fields.One2Many('stock.move', 'production_input', 'Inputs',
|
|
domain=[
|
|
('from_location', 'child_of', [Eval('warehouse')], 'parent'),
|
|
('to_location', '=', Eval('location')),
|
|
],
|
|
states={
|
|
'readonly': (~Eval('state').in_(['request', 'draft', 'waiting'])
|
|
| ~Eval('warehouse') | ~Eval('location')),
|
|
},
|
|
depends=['warehouse', 'location'])
|
|
outputs = fields.One2Many('stock.move', 'production_output', 'Outputs',
|
|
domain=[
|
|
('from_location', '=', Eval('location')),
|
|
('to_location', 'child_of', [Eval('warehouse')], 'parent'),
|
|
],
|
|
states={
|
|
'readonly': (Eval('state').in_(['done', 'cancel'])
|
|
| ~Eval('warehouse') | ~Eval('location')),
|
|
},
|
|
depends=['warehouse', 'location'])
|
|
state = fields.Selection([
|
|
('request', 'Request'),
|
|
('draft', 'Draft'),
|
|
('waiting', 'Waiting'),
|
|
('assigned', 'Assigned'),
|
|
('running', 'Running'),
|
|
('done', 'Done'),
|
|
('cancel', 'Canceled'),
|
|
], 'State', readonly=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Production, cls).__setup__()
|
|
cls._error_messages.update({
|
|
'uneven_costs': ('The costs of the outputs (%(outputs)s) of '
|
|
'production "%(production)s" do not match the cost of the '
|
|
'production (%(costs)s).')
|
|
})
|
|
cls._transitions |= set((
|
|
('request', 'draft'),
|
|
('draft', 'waiting'),
|
|
('waiting', 'assigned'),
|
|
('assigned', 'running'),
|
|
('running', 'done'),
|
|
('running', 'waiting'),
|
|
('assigned', 'waiting'),
|
|
('waiting', 'waiting'),
|
|
('waiting', 'draft'),
|
|
('request', 'cancel'),
|
|
('draft', 'cancel'),
|
|
('waiting', 'cancel'),
|
|
('assigned', 'cancel'),
|
|
('cancel', 'draft'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': ~Eval('state').in_(['request', 'draft',
|
|
'assigned']),
|
|
},
|
|
'draft': {
|
|
'invisible': ~Eval('state').in_(['request', 'waiting',
|
|
'cancel']),
|
|
'icon': If(Eval('state') == 'cancel',
|
|
'tryton-clear',
|
|
If(Eval('state') == 'request',
|
|
'tryton-go-next',
|
|
'tryton-go-previous')),
|
|
},
|
|
'wait': {
|
|
'invisible': ~Eval('state').in_(['draft', 'assigned',
|
|
'waiting', 'running']),
|
|
'icon': If(Eval('state').in_(['assigned', 'running']),
|
|
'tryton-go-previous',
|
|
If(Eval('state') == 'waiting',
|
|
'tryton-clear',
|
|
'tryton-go-next')),
|
|
},
|
|
'run': {
|
|
'invisible': Eval('state') != 'assigned',
|
|
},
|
|
'done': {
|
|
'invisible': Eval('state') != 'running',
|
|
},
|
|
'assign_wizard': {
|
|
'invisible': Eval('state') != 'waiting',
|
|
'readonly': ~Eval('groups', []).contains(
|
|
Id('stock', 'group_stock')),
|
|
},
|
|
'assign_try': {},
|
|
'assign_force': {},
|
|
})
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@classmethod
|
|
def default_warehouse(cls):
|
|
Location = Pool().get('stock.location')
|
|
locations = Location.search(cls.warehouse.domain)
|
|
if len(locations) == 1:
|
|
return locations[0].id
|
|
|
|
@classmethod
|
|
def default_location(cls):
|
|
Location = Pool().get('stock.location')
|
|
warehouse_id = cls.default_warehouse()
|
|
if warehouse_id:
|
|
warehouse = Location(warehouse_id)
|
|
return warehouse.production_location.id
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
def _move(self, from_location, to_location, company, product, uom,
|
|
quantity):
|
|
Move = Pool().get('stock.move')
|
|
move = Move(
|
|
product=product,
|
|
uom=uom,
|
|
quantity=quantity,
|
|
from_location=from_location,
|
|
to_location=to_location,
|
|
company=company,
|
|
currency=company.currency if company else None,
|
|
state='draft',
|
|
)
|
|
return move
|
|
|
|
def _explode_move_values(self, from_location, to_location, company,
|
|
bom_io, quantity):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
|
|
move = self._move(from_location, to_location, company,
|
|
bom_io.product, bom_io.uom, quantity)
|
|
move.from_location = from_location.id if from_location else None
|
|
move.to_location = to_location.id if to_location else None
|
|
move.unit_price_required = move.on_change_with_unit_price_required()
|
|
values = {}
|
|
for field_name, field in Move._fields.iteritems():
|
|
try:
|
|
value = getattr(move, field_name)
|
|
except AttributeError:
|
|
continue
|
|
if value and field._type in ('many2one', 'one2one'):
|
|
values[field_name] = value.id
|
|
values[field_name + '.rec_name'] = value.rec_name
|
|
else:
|
|
values[field_name] = value
|
|
return values
|
|
|
|
def explode_bom(self):
|
|
pool = Pool()
|
|
Uom = pool.get('product.uom')
|
|
Template = pool.get('product.template')
|
|
Product = pool.get('product.product')
|
|
if not (self.bom and self.product and self.uom):
|
|
return {}
|
|
inputs = {
|
|
'remove': [r.id for r in self.inputs or []],
|
|
'add': [],
|
|
}
|
|
outputs = {
|
|
'remove': [r.id for r in self.outputs or []],
|
|
'add': [],
|
|
}
|
|
changes = {
|
|
'inputs': inputs,
|
|
'outputs': outputs,
|
|
'cost': Decimal(0),
|
|
}
|
|
|
|
if self.warehouse:
|
|
storage_location = self.warehouse.storage_location
|
|
else:
|
|
storage_location = None
|
|
|
|
factor = self.bom.compute_factor(self.product, self.quantity or 0,
|
|
self.uom)
|
|
for input_ in self.bom.inputs:
|
|
quantity = input_.compute_quantity(factor)
|
|
values = self._explode_move_values(storage_location, self.location,
|
|
self.company, input_, quantity)
|
|
if values:
|
|
inputs['add'].append((-1, values))
|
|
quantity = Uom.compute_qty(input_.uom, quantity,
|
|
input_.product.default_uom)
|
|
changes['cost'] += (Decimal(str(quantity)) *
|
|
input_.product.cost_price)
|
|
|
|
if hasattr(Product, 'cost_price'):
|
|
digits = Product.cost_price.digits
|
|
else:
|
|
digits = Template.cost_price.digits
|
|
for output in self.bom.outputs:
|
|
quantity = output.compute_quantity(factor)
|
|
values = self._explode_move_values(self.location, storage_location,
|
|
self.company, output, quantity)
|
|
if values:
|
|
values['unit_price'] = Decimal(0)
|
|
if output.product.id == values.get('product') and quantity:
|
|
values['unit_price'] = Decimal(
|
|
changes['cost'] / Decimal(str(quantity))
|
|
).quantize(Decimal(str(10 ** -digits[1])))
|
|
outputs['add'].append((-1, values))
|
|
return changes
|
|
|
|
@fields.depends('warehouse')
|
|
def on_change_warehouse(self):
|
|
changes = {
|
|
'location': None,
|
|
}
|
|
if self.warehouse:
|
|
changes['location'] = self.warehouse.production_location.id
|
|
return changes
|
|
|
|
@fields.depends(*BOM_CHANGES)
|
|
def on_change_product(self):
|
|
result = {}
|
|
if self.product:
|
|
uoms = self.product.default_uom.category.uoms
|
|
if (not self.uom or self.uom not in uoms):
|
|
result['uom'] = self.product.default_uom.id
|
|
result['uom.rec_name'] = self.product.default_uom.rec_name
|
|
result['unit_digits'] = self.product.default_uom.digits
|
|
else:
|
|
result['bom'] = None
|
|
result['uom'] = None
|
|
result['uom.rec_name'] = ''
|
|
result['unit_digits'] = 2
|
|
|
|
if 'uom' in result:
|
|
self.uom = result['uom']
|
|
if 'bom' in result:
|
|
self.bom = result['bom']
|
|
result.update(self.explode_bom())
|
|
return result
|
|
|
|
@fields.depends('product')
|
|
def on_change_with_uom_category(self, name=None):
|
|
if self.product:
|
|
return self.product.default_uom.category.id
|
|
|
|
@fields.depends('uom')
|
|
def on_change_with_unit_digits(self, name=None):
|
|
if self.uom:
|
|
return self.uom.digits
|
|
return 2
|
|
|
|
@fields.depends(*BOM_CHANGES)
|
|
def on_change_bom(self):
|
|
return self.explode_bom()
|
|
|
|
@fields.depends(*BOM_CHANGES)
|
|
def on_change_uom(self):
|
|
return self.explode_bom()
|
|
|
|
@fields.depends(*BOM_CHANGES)
|
|
def on_change_quantity(self):
|
|
return self.explode_bom()
|
|
|
|
def get_cost(self, name):
|
|
cost = Decimal(0)
|
|
for input_ in self.inputs:
|
|
if input_.cost_price is not None:
|
|
cost_price = input_.cost_price
|
|
else:
|
|
cost_price = input_.product.cost_price
|
|
cost += (Decimal(str(input_.internal_quantity)) * cost_price)
|
|
return cost
|
|
|
|
@fields.depends('inputs')
|
|
def on_change_with_cost(self):
|
|
Uom = Pool().get('product.uom')
|
|
|
|
cost = Decimal(0)
|
|
if not self.inputs:
|
|
return cost
|
|
|
|
for input_ in self.inputs:
|
|
if (input_.product is None
|
|
or input_.uom is None
|
|
or input_.quantity is None):
|
|
continue
|
|
product = input_.product
|
|
quantity = Uom.compute_qty(input_.uom, input_.quantity,
|
|
product.default_uom)
|
|
cost += Decimal(str(quantity)) * product.cost_price
|
|
return cost
|
|
|
|
def set_moves(self):
|
|
pool = Pool()
|
|
Template = pool.get('product.template')
|
|
Product = pool.get('product.product')
|
|
|
|
storage_location = self.warehouse.storage_location
|
|
location = self.location
|
|
company = self.company
|
|
|
|
if not self.bom:
|
|
if self.product:
|
|
move = self._move(location, storage_location, company,
|
|
self.product, self.uom, self.quantity)
|
|
if move:
|
|
move.production_output = self
|
|
move.unit_price = Decimal(0)
|
|
move.save()
|
|
self._set_move_planned_date()
|
|
return
|
|
|
|
factor = self.bom.compute_factor(self.product, self.quantity, self.uom)
|
|
cost = Decimal(0)
|
|
for input_ in self.bom.inputs:
|
|
quantity = input_.compute_quantity(factor)
|
|
product = input_.product
|
|
move = self._move(storage_location, location, company,
|
|
product, input_.uom, quantity)
|
|
if move:
|
|
move.production_input = self
|
|
move.save()
|
|
cost += Decimal(str(quantity)) * product.cost_price
|
|
|
|
if hasattr(Product, 'cost_price'):
|
|
digits = Product.cost_price.digits
|
|
else:
|
|
digits = Template.cost_price.digits
|
|
|
|
for output in self.bom.outputs:
|
|
quantity = output.compute_quantity(factor)
|
|
product = output.product
|
|
move = self._move(location, storage_location, company,
|
|
product, output.uom, quantity)
|
|
if move:
|
|
move.production_output = self
|
|
if product == self.product:
|
|
move.unit_price = Decimal(
|
|
cost / Decimal(str(quantity))
|
|
).quantize(Decimal(str(10 ** -digits[1])))
|
|
move.save()
|
|
self._set_move_planned_date()
|
|
|
|
@classmethod
|
|
def validate(cls, productions):
|
|
super(Production, cls).validate(productions)
|
|
for production in productions:
|
|
production.check_cost()
|
|
|
|
def check_cost(self):
|
|
if self.state != 'done':
|
|
return
|
|
cost_price = Decimal(0)
|
|
for output in self.outputs:
|
|
cost_price += (Decimal(str(output.quantity))
|
|
* output.unit_price)
|
|
if not self.company.currency.is_zero(self.cost - cost_price):
|
|
self.raise_user_error('uneven_costs', {
|
|
'production': self.rec_name,
|
|
'costs': self.cost,
|
|
'outputs': cost_price,
|
|
})
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
Sequence = Pool().get('ir.sequence')
|
|
Config = Pool().get('production.configuration')
|
|
|
|
vlist = [x.copy() for x in vlist]
|
|
config = Config(1)
|
|
for values in vlist:
|
|
values['code'] = Sequence.get_id(config.production_sequence.id)
|
|
productions = super(Production, cls).create(vlist)
|
|
for production in productions:
|
|
production._set_move_planned_date()
|
|
return productions
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
super(Production, cls).write(*args)
|
|
for production in sum(args[::2], []):
|
|
production._set_move_planned_date()
|
|
|
|
def _get_move_planned_date(self):
|
|
"Return the planned dates for input and output moves"
|
|
return self.planned_date, self.planned_date
|
|
|
|
def _set_move_planned_date(self):
|
|
"Set planned date of moves for the shipments"
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
dates = self._get_move_planned_date()
|
|
input_date, output_date = dates
|
|
Move.write([m for m in self.inputs
|
|
if m.state not in ('assigned', 'done', 'cancel')], {
|
|
'planned_date': input_date,
|
|
})
|
|
Move.write([m for m in self.outputs
|
|
if m.state not in ('assigned', 'done', 'cancel')], {
|
|
'planned_date': output_date,
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancel')
|
|
def cancel(cls, productions):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.cancel([m for p in productions
|
|
for m in p.inputs + p.outputs])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, productions):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.draft([m for p in productions
|
|
for m in p.inputs + p.outputs])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('waiting')
|
|
def wait(cls, productions):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.draft([m for p in productions
|
|
for m in p.inputs + p.outputs])
|
|
|
|
@classmethod
|
|
@Workflow.transition('assigned')
|
|
def assign(cls, productions):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('running')
|
|
def run(cls, productions):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.do([m for p in productions for m in p.inputs])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
def done(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.outputs])
|
|
cls.write([p for p in productions if not p.effective_date], {
|
|
'effective_date': Date.today(),
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button_action('production.wizard_assign')
|
|
def assign_wizard(self, productions):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def assign_try(cls, productions):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
if Move.assign_try([m for p in productions
|
|
for m in p.inputs]):
|
|
cls.assign(productions)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def assign_force(cls, productions):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.assign([m for p in productions for m in p.inputs])
|
|
cls.assign(productions)
|
|
|
|
|
|
class AssignFailed(ModelView):
|
|
'Assign Production'
|
|
__name__ = 'production.assign.failed'
|
|
|
|
moves = fields.Many2Many('stock.move', None, None, 'Moves', readonly=True)
|
|
|
|
@staticmethod
|
|
def default_moves():
|
|
pool = Pool()
|
|
Production = pool.get('production')
|
|
production_id = Transaction().context.get('active_id')
|
|
if not production_id:
|
|
return []
|
|
production = Production(production_id)
|
|
return [m.id for m in production.inputs if m.state == 'draft']
|
|
|
|
|
|
class Assign(Wizard):
|
|
'Assign Production'
|
|
__name__ = 'production.assign'
|
|
|
|
start = StateTransition()
|
|
failed = StateView('production.assign.failed',
|
|
'production.assign_failed_view_form', [
|
|
Button('Force Assign', 'force', 'tryton-go-next',
|
|
states={
|
|
'invisible': ~Id('stock',
|
|
'group_stock_force_assignment').in_(
|
|
Eval('context', {}).get('groups', [])),
|
|
}),
|
|
Button('Ok', 'end', 'tryton-ok', True),
|
|
])
|
|
force = StateTransition()
|
|
|
|
def transition_start(self):
|
|
pool = Pool()
|
|
Production = pool.get('production')
|
|
|
|
if Production.assign_try(
|
|
[Production(Transaction().context['active_id'])]):
|
|
return 'end'
|
|
else:
|
|
return 'failed'
|
|
|
|
def transition_force(self):
|
|
pool = Pool()
|
|
Production = pool.get('production')
|
|
|
|
Production.assign_force(
|
|
[Production(Transaction().context['active_id'])])
|
|
return 'end'
|