kalenislims/lims_production/stock.py

571 lines
21 KiB
Python

# -*- 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
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
from trytond.wizard import Wizard, StateAction
from trytond.modules.product import price_digits
from trytond.exceptions import UserError
from trytond.i18n import gettext
__all__ = ['PurityDegree', 'Brand', 'FamilyEquivalent', 'Template', 'Product',
'LotCategory', 'Lot', 'Move', 'ShipmentIn', 'MoveProductionRelated']
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:
raise UserError(gettext(
'lims_production.msg_invalid_product_uom_category'))
@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)
class Template(metaclass=PoolMeta):
__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:
return [('id', 'in', list(map(int, [product.template.id
for product in products])))]
return super(Template, cls).search_rec_name(name, clause)
class Product(metaclass=PoolMeta):
__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 = []
for cost, records in costs.items():
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
if (move.from_location.type in ['supplier', 'production'] or
move.to_location.type == 'supplier'):
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'))
class Lot(metaclass=PoolMeta):
__name__ = 'stock.lot'
category = fields.Many2One('stock.lot.category', 'Category')
special_category = fields.Function(fields.Char('Category'),
'on_change_with_special_category')
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')
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')))})
@classmethod
def __setup__(cls):
super(Lot, cls).__setup__()
cls.expiration_date.states['invisible'] = Eval(False)
@staticmethod
def default_exclusive_glp():
return False
@fields.depends('category', 'product', '_parent_product.purchasable',
'_parent_product.salable')
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:])]
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
@classmethod
def create(cls, vlist):
pool = Pool()
Product = pool.get('product.product')
Config = pool.get('lims.configuration')
config = Config(1)
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):
lot_category_id = (config.lot_category_input_prod.id
if config.lot_category_input_prod else None)
elif (not product.purchasable and product.salable):
lot_category_id = (config.lot_category_prod_sale.id
if config.lot_category_prod_sale else None)
elif (not product.purchasable and not product.salable):
lot_category_id = (
config.lot_category_prod_domestic_use.id if
config.lot_category_prod_domestic_use else None)
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:])]
class Move(metaclass=PoolMeta):
__name__ = 'stock.move'
label_quantity = fields.Float("Label Quantity",
digits=(16, Eval('unit_digits', 2)), depends=['unit_digits'])
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
@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})
class ShipmentIn(metaclass=PoolMeta):
__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)
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
class MoveProductionRelated(Wizard):
'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, {}