kalenislims/lims_production/stock.py

571 lines
21 KiB
Python
Raw Normal View History

2017-10-08 02:23:22 +02:00
# -*- coding: utf-8 -*-
# This file is part of lims_production module for Tryton.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from decimal import Decimal
from collections import defaultdict
2017-10-08 02:23:22 +02:00
from functools import partial
from trytond.model import ModelView, ModelSQL, fields
from trytond.pyson import PYSONEncoder, Eval, Equal, Bool, Not
from trytond.transaction import Transaction
from trytond.pool import PoolMeta, Pool
2018-05-24 01:47:26 +02:00
from trytond.wizard import Wizard, StateAction
2017-10-08 02:23:22 +02:00
from trytond.modules.product import price_digits
2019-07-23 23:27:33 +02:00
from trytond.exceptions import UserError
from trytond.i18n import gettext
2017-10-08 02:23:22 +02:00
__all__ = ['PurityDegree', 'Brand', 'FamilyEquivalent', 'Template', 'Product',
2018-05-24 01:47:26 +02:00
'LotCategory', 'Lot', 'Move', 'ShipmentIn', 'MoveProductionRelated']
2017-10-08 02:23:22 +02:00
class PurityDegree(ModelSQL, ModelView):
'Purity Degree'
__name__ = 'lims.purity.degree'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
class Brand(ModelSQL, ModelView):
'Brand'
__name__ = 'lims.brand'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
class FamilyEquivalent(ModelSQL, ModelView):
'Family/Equivalent'
__name__ = 'lims.family.equivalent'
name = fields.Char('Name', required=True)
code = fields.Char('Code', required=True)
uom = fields.Many2One('product.uom', 'UoM', required=True,
domain=[('category.lims_only_available', '=', False)],
help='The UoM\'s Category selected here will determine the set '
'of Products that can be related to this Family/Equivalent.')
products = fields.One2Many('product.template', 'family_equivalent',
'Products', readonly=True)
@classmethod
def validate(cls, family_equivalents):
super(FamilyEquivalent, cls).validate(family_equivalents)
for fe in family_equivalents:
fe.check_products()
def check_products(self):
if self.products:
main_category = self.uom.category
for product in self.products:
if main_category != product.default_uom.category:
2019-07-23 23:27:33 +02:00
raise UserError(gettext(
'lims_production.msg_invalid_product_uom_category'))
2017-10-08 02:23:22 +02:00
@classmethod
def copy(cls, family_equivalents, default=None):
if default is None:
default = {}
current_default = default.copy()
current_default['products'] = None
return super(FamilyEquivalent, cls).copy(family_equivalents,
default=current_default)
2019-03-04 15:41:58 +01:00
class Template(metaclass=PoolMeta):
2017-10-08 02:23:22 +02:00
__name__ = 'product.template'
common_name = fields.Char('Common name')
chemical_name = fields.Char('Chemical name')
commercial_name = fields.Char('Commercial name')
cas_number = fields.Char('CAS number')
commercial_brand = fields.Many2One('lims.brand', 'Commercial Brand')
purity_degree = fields.Many2One('lims.purity.degree', 'Purity Degree')
family_equivalent = fields.Many2One('lims.family.equivalent',
'Family/Equivalent',
domain=[('uom.category', '=', Eval('default_uom_category'))],
depends=['default_uom_category'],
help='The UoM\'s Category of Family/Equivalent which you can '
'select here will match the UoM\'s Category of this Product.')
controlled = fields.Boolean('Controlled')
reference_material = fields.Boolean('Reference Material')
certified = fields.Boolean('Certified')
@classmethod
def search_rec_name(cls, name, clause):
Product = Pool().get('product.product')
products = Product.search(['OR',
[('code',) + tuple(clause[1:])],
[('barcode',) + tuple(clause[1:])],
], order=[])
if products:
2019-03-04 15:41:58 +01:00
return [('id', 'in', list(map(int, [product.template.id
for product in products])))]
2017-10-08 02:23:22 +02:00
return super(Template, cls).search_rec_name(name, clause)
2019-03-04 15:41:58 +01:00
class Product(metaclass=PoolMeta):
2017-10-08 02:23:22 +02:00
__name__ = 'product.product'
catalog = fields.Char('Catalog', depends=['active'],
states={'readonly': ~Eval('active', True)})
barcode = fields.Char('Bar Code', depends=['active'],
states={'readonly': ~Eval('active', True)})
common_name = fields.Function(fields.Char('Common name'),
'get_template_field', searcher='search_template_field')
chemical_name = fields.Function(fields.Char('Chemical name'),
'get_template_field', searcher='search_template_field')
commercial_name = fields.Function(fields.Char('Commercial name'),
'get_template_field', searcher='search_template_field')
cas_number = fields.Function(fields.Char('CAS number'),
'get_template_field', searcher='search_template_field')
commercial_brand = fields.Function(fields.Many2One('lims.brand',
'Commercial Brand'), 'get_template_field',
searcher='search_template_field')
purity_degree = fields.Function(fields.Many2One('lims.purity.degree',
'Purity Degree'), 'get_template_field',
searcher='search_template_field')
family_equivalent = fields.Function(fields.Many2One(
'lims.family.equivalent', 'Family/Equivalent'), 'get_template_field',
searcher='search_template_field')
controlled = fields.Function(fields.Boolean('Controlled'),
'get_template_field', searcher='search_template_field')
reference_material = fields.Function(fields.Boolean('Reference Material'),
'get_template_field', searcher='search_template_field')
certified = fields.Function(fields.Boolean('Certified'),
'get_template_field', searcher='search_template_field')
@classmethod
def search_rec_name(cls, name, clause):
res = super(Product, cls).search_rec_name(name, clause)
return ['OR',
res,
[('barcode', ) + tuple(clause[1:])]
]
@classmethod
def recompute_cost_price(cls, products):
# original function rewritten to use cost_price and
# quantity from Template
pool = Pool()
Template = pool.get('product.template')
digits = Template.cost_price.digits
write = Template.write
record = lambda p: p.template
costs = defaultdict(list)
for product in products:
if product.type == 'service':
continue
cost = getattr(product,
'recompute_cost_price_%s' % product.cost_price_method)()
cost = cost.quantize(Decimal(str(10.0 ** -digits[1])))
costs[cost].append(record(product))
if not costs:
return
to_write = []
2019-03-04 15:41:58 +01:00
for cost, records in costs.items():
2017-10-08 02:23:22 +02:00
to_write.append(records)
to_write.append({'cost_price': cost})
# Enforce check access for account_stock*
with Transaction().set_context(_check_access=True):
write(*to_write)
def recompute_cost_price_average(self):
# original function rewritten to use cost_price and
# quantity from Template
pool = Pool()
Move = pool.get('stock.move')
Currency = pool.get('currency.currency')
Uom = pool.get('product.uom')
context = Transaction().context
product_clause = ('product.template', '=', self.template.id)
moves = Move.search([
product_clause,
('state', '=', 'done'),
('company', '=', context.get('company')),
['OR',
[
('to_location.type', '=', 'storage'),
('from_location.type', '!=', 'storage'),
],
[
('from_location.type', '=', 'storage'),
('to_location.type', '!=', 'storage'),
],
],
], order=[('effective_date', 'ASC'), ('id', 'ASC')])
cost_price = Decimal(0)
quantity = 0
for move in moves:
qty = Uom.compute_qty(move.uom, move.quantity,
self.template.default_uom)
qty = Decimal(str(qty))
if move.from_location.type == 'storage':
qty *= -1
2018-05-24 01:47:26 +02:00
if (move.from_location.type in ['supplier', 'production'] or
move.to_location.type == 'supplier'):
2017-10-08 02:23:22 +02:00
with Transaction().set_context(date=move.effective_date):
unit_price = Currency.compute(
move.currency, move.unit_price,
move.company.currency, round=False)
unit_price = Uom.compute_price(move.uom, unit_price,
self.template.default_uom)
if quantity + qty != 0:
cost_price = (
(cost_price * quantity) + (unit_price * qty)
) / (quantity + qty)
quantity += qty
return cost_price
@classmethod
def get_template_field(cls, products, names):
result = {}
for name in names:
result[name] = {}
if name in ('commercial_brand', 'purity_degree',
'family_equivalent'):
for p in products:
field = getattr(p.template, name, None)
result[name][p.id] = field.id if field else None
else:
for p in products:
result[name][p.id] = getattr(p.template, name, None)
return result
@classmethod
def search_template_field(cls, name, clause):
return [('template.' + name,) + tuple(clause[1:])]
class LotCategory(ModelSQL, ModelView):
"Lot Category"
__name__ = "stock.lot.category"
_rec_name = 'name'
name = fields.Char('Name', required=True)
@classmethod
def __setup__(cls):
super(LotCategory, cls).__setup__()
cls._order.insert(0, ('name', 'ASC'))
2019-03-04 15:41:58 +01:00
class Lot(metaclass=PoolMeta):
2017-10-08 02:23:22 +02:00
__name__ = 'stock.lot'
category = fields.Many2One('stock.lot.category', 'Category')
special_category = fields.Function(fields.Char('Category'),
2018-05-24 01:47:26 +02:00
'on_change_with_special_category')
2017-10-08 02:23:22 +02:00
stability = fields.Char('Stability', depends=['special_category'],
states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
})
homogeneity = fields.Char('Homogeneity', depends=['special_category'],
states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
})
concentration = fields.Char('Concentration',
depends=['special_category'], states={
'invisible': ~Eval('special_category').in_(
['input_prod', 'domestic_use']),
})
reception_date = fields.Date('Reception date',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
})
preparation_date = fields.Date('Preparation date',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'domestic_use'))),
})
common_name = fields.Function(fields.Char('Common name',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
}), 'get_common_name')
chemical_name = fields.Function(fields.Char('Chemical name',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
}), 'get_chemical_name')
commercial_name = fields.Function(fields.Char('Commercial name',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
}), 'get_commercial_name')
cas_number = fields.Function(fields.Char('CAS number',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
}), 'get_cas_number', searcher='search_cas_number')
2017-10-08 02:23:22 +02:00
commercial_brand = fields.Function(fields.Many2One('lims.brand',
'Commercial Brand', depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
}), 'get_commercial_brand')
catalog = fields.Function(fields.Char('Catalog',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
}), 'get_catalog')
purity_degree = fields.Function(fields.Many2One('lims.purity.degree',
'Purity Degree', depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
}), 'get_purity_degree')
solvent = fields.Many2One('product.product', 'Solvent',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'domestic_use'))),
})
technician = fields.Many2One('lims.laboratory.professional', 'Technician',
depends=['special_category'], states={
'invisible': ~Eval('special_category').in_(
['domestic_use', 'prod_sale']),
})
account_category = fields.Function(fields.Many2One('product.category',
'Account Category', depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod'))),
}), 'get_account_category', searcher='search_account_category')
exclusive_glp = fields.Boolean('Exclusive use GLP',
depends=['special_category'], states={
'invisible': Not(Bool(Equal(Eval('special_category'),
'input_prod')))})
2017-10-08 02:23:22 +02:00
@classmethod
def __setup__(cls):
super(Lot, cls).__setup__()
cls.expiration_date.states['invisible'] = Eval(False)
@staticmethod
def default_exclusive_glp():
return False
2020-03-05 05:24:51 +01:00
@fields.depends('category', 'product', '_parent_product.purchasable',
'_parent_product.salable')
2017-10-08 02:23:22 +02:00
def on_change_with_special_category(self, name=None):
Config = Pool().get('lims.configuration')
if self.category:
config = Config(1)
if self.category == config.lot_category_input_prod:
return 'input_prod'
elif self.category == config.lot_category_prod_sale:
return 'prod_sale'
elif self.category == config.lot_category_prod_domestic_use:
return 'domestic_use'
elif self.product:
if (self.product.purchasable and not self.product.salable):
return 'input_prod'
elif (not self.product.purchasable and self.product.salable):
return 'prod_sale'
elif (not self.product.purchasable and not self.product.salable):
return 'domestic_use'
else:
return ''
def get_common_name(self, name=None):
if self.product:
return self.product.common_name
return ''
def get_chemical_name(self, name=None):
if self.product:
return self.product.chemical_name
return ''
def get_commercial_name(self, name=None):
if self.product:
return self.product.commercial_name
return ''
def get_cas_number(self, name=None):
if self.product:
return self.product.cas_number
return ''
@classmethod
def search_cas_number(cls, name, clause):
return [('product.cas_number',) + tuple(clause[1:])]
2017-10-08 02:23:22 +02:00
def get_commercial_brand(self, name=None):
if self.product and self.product.commercial_brand:
return self.product.commercial_brand.id
return None
def get_catalog(self, name=None):
if self.product:
return self.product.catalog
return ''
def get_purity_degree(self, name=None):
if self.product and self.product.purity_degree:
return self.product.purity_degree.id
return None
def get_account_category(self, name=None):
if self.product:
return self.product.account_category.id
return None
2017-10-08 02:23:22 +02:00
@classmethod
def create(cls, vlist):
pool = Pool()
Product = pool.get('product.product')
2018-05-24 01:47:26 +02:00
Config = pool.get('lims.configuration')
2017-10-08 02:23:22 +02:00
2018-05-24 01:47:26 +02:00
config = Config(1)
2017-10-08 02:23:22 +02:00
vlist = [x.copy() for x in vlist]
for values in vlist:
if not values.get('category'):
product = Product(values['product'])
lot_category_id = None
if (product.purchasable and not product.salable):
2018-05-24 01:47:26 +02:00
lot_category_id = (config.lot_category_input_prod.id
if config.lot_category_input_prod else None)
2017-10-08 02:23:22 +02:00
elif (not product.purchasable and product.salable):
2018-05-24 01:47:26 +02:00
lot_category_id = (config.lot_category_prod_sale.id
if config.lot_category_prod_sale else None)
2017-10-08 02:23:22 +02:00
elif (not product.purchasable and not product.salable):
lot_category_id = (
2018-05-24 01:47:26 +02:00
config.lot_category_prod_domestic_use.id if
config.lot_category_prod_domestic_use else None)
2017-10-08 02:23:22 +02:00
if lot_category_id:
values['category'] = lot_category_id
return super(Lot, cls).create(vlist)
@classmethod
def search_account_category(cls, name, clause):
return [('product.' + name,) + tuple(clause[1:])]
2017-10-08 02:23:22 +02:00
2019-03-04 15:41:58 +01:00
class Move(metaclass=PoolMeta):
2017-10-08 02:23:22 +02:00
__name__ = 'stock.move'
label_quantity = fields.Float("Label Quantity",
digits=(16, Eval('unit_digits', 2)), depends=['unit_digits'])
2017-10-08 02:23:22 +02:00
origin_purchase_unit_price = fields.Numeric('Unit Price',
digits=price_digits)
origin_purchase_currency = fields.Many2One('currency.currency',
'Currency')
@fields.depends('quantity')
def on_change_quantity(self):
if self.quantity:
self.label_quantity = self.quantity
2017-10-08 02:23:22 +02:00
@classmethod
def _get_origin(cls):
models = super(Move, cls)._get_origin()
models.append('production')
return models
def _update_product_cost_price(self, direction):
# original function rewritten to use cost_price and
# quantity from Template
pool = Pool()
Uom = pool.get('product.uom')
ProductTemplate = pool.get('product.template')
Location = pool.get('stock.location')
Currency = pool.get('currency.currency')
Date = pool.get('ir.date')
if direction == 'in':
quantity = self.quantity
elif direction == 'out':
quantity = -self.quantity
context = {}
locations = Location.search([
('type', '=', 'storage'),
])
context['with_childs'] = False
context['locations'] = [l.id for l in locations]
context['stock_date_end'] = Date.today()
with Transaction().set_context(context):
template = ProductTemplate(self.product.template.id)
qty = Uom.compute_qty(self.uom, quantity, template.default_uom)
qty = Decimal(str(qty))
product_qty = template.quantity
product_qty = Decimal(str(max(product_qty, 0)))
# convert wrt currency
with Transaction().set_context(date=self.effective_date):
unit_price = Currency.compute(self.currency, self.unit_price,
self.company.currency, round=False)
# convert wrt to the uom
unit_price = Uom.compute_price(self.uom, unit_price,
template.default_uom)
if product_qty + qty != Decimal('0.0'):
new_cost_price = (
(template.cost_price * product_qty) + (unit_price * qty)
) / (product_qty + qty)
else:
new_cost_price = template.cost_price
digits = ProductTemplate.cost_price.digits
write = partial(ProductTemplate.write, [template])
new_cost_price = new_cost_price.quantize(
Decimal(str(10.0 ** -digits[1])))
write({'cost_price': new_cost_price})
2019-03-04 15:41:58 +01:00
class ShipmentIn(metaclass=PoolMeta):
2017-10-08 02:23:22 +02:00
__name__ = 'stock.shipment.in'
@classmethod
def __setup__(cls):
super(ShipmentIn, cls).__setup__()
cls.inventory_moves.states['readonly'] = Eval('state').in_(
['draft', 'cancel'])
def _get_inventory_move(self, incoming_move):
move = super(ShipmentIn, self)._get_inventory_move(incoming_move)
2017-10-08 02:23:22 +02:00
if not move:
return None
move.label_quantity = move.quantity
move.origin_purchase_currency = \
incoming_move.origin.purchase.currency.id
move.origin_purchase_unit_price = incoming_move.origin.unit_price
return move
2018-05-24 01:47:26 +02:00
class MoveProductionRelated(Wizard):
2017-10-08 02:23:22 +02:00
'Related Productions'
__name__ = 'lims.move.production_related'
start = StateAction('lims_production.act_production_related')
def do_start(self, action):
Move = Pool().get('stock.move')
move = Move(Transaction().context['active_id'])
production_id = None
if move.production_input:
production_id = move.production_input.id
elif move.production_output:
production_id = move.production_output.id
action['pyson_domain'] = PYSONEncoder().encode([
('id', '=', production_id),
])
return action, {}