trytond-stock_location_prod.../product_limit.py
2019-01-30 22:15:52 +01:00

388 lines
14 KiB
Python

# The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from dateutil.relativedelta import relativedelta
from trytond.model import Unique
from trytond.report import Report
from trytond.transaction import Transaction
from trytond.model import ModelSQL, ModelView, fields
from trytond.pyson import Eval
from trytond.pool import Pool, PoolMeta
from trytond.wizard import StateReport
from trytond.wizard import Wizard, StateTransition, StateView, Button
__all__ = ['ProductLimit', 'ShipmentOut', 'ShipmentOutReturn', 'Location',
'ProductLimitNote', 'PrintProductLimitNote',
'PrintProductLimitNoteParam', 'DeliveryNote', 'RestockingList',
'Configuration']
class ProductLimit(ModelSQL, ModelView):
"""Location product limit"""
__name__ = 'stock.location.product_limit'
location = fields.Many2One('stock.location', 'Location',
ondelete='CASCADE', required=True,
domain=[('type', '=', 'customer'), ],
context={'product': Eval('product')},
depends=['product'])
product = fields.Many2One('product.product', 'Product',
required=True, ondelete='RESTRICT')
quantity = fields.Float('Quantity', required=True,
digits=(16, Eval('uom_digits', 2)),
depends=['uom_digits'])
uom_category = fields.Function(
fields.Many2One('product.uom.category', 'UOM Category'),
'on_change_with_uom_category')
uom = fields.Many2One('product.uom', 'UOM', required=True,
ondelete='RESTRICT', domain=[
('category', '=', Eval('uom_category'))],
depends=['uom_category'])
uom_digits = fields.Function(
fields.Integer('UOM Digits'), 'on_change_with_uom_digits')
@classmethod
def __setup__(cls):
super(ProductLimit, cls).__setup__()
t = cls.__table__()
cls._sql_constraints = [
('location_product_uniq', Unique(t, t.location, t.product),
'The pair location, product must be unique.')
]
@fields.depends('product')
def on_change_product(self):
if self.product:
self.uom = self.product.default_uom
@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_uom_digits(self, name=None):
if self.uom:
return self.uom.digits
return 2
@classmethod
def product_limits_by_location(cls, location_id, product_ids=[],
at_date=None):
pool = Pool()
Product = pool.get('product.product')
Uom = pool.get('product.uom')
context = Transaction().context.copy()
context.update({
'locations': [location_id],
'with_childs': True
})
if at_date:
context['stock_date_end'] = at_date
products_with_limit = cls.search([
('location', '=', location_id),
('product', 'in', product_ids) if product_ids else ()])
if not product_ids:
product_ids = [l.product.id for l in products_with_limit]
products = Product.browse(product_ids)
with Transaction().set_context(context):
products = Product.get_quantity(products, 'forecast_quantity')
p_forecast = {k: value for k, value in products.iteritems()
if value}
ret = []
for pl in products_with_limit:
if pl.product.id not in p_forecast:
continue
item = pl.product.rec_name
assigned = Uom.compute_qty(pl.uom, pl.quantity,
pl.product.default_uom)
stock = p_forecast[pl.product.id]
diff = assigned - stock
unit = pl.product.default_uom
ret.append({
'item': item,
'assigned': assigned,
'stock': stock,
'diff': diff,
'unit': unit
})
return ret
class ShipmentOut:
__name__ = 'stock.shipment.out'
__metaclass__ = PoolMeta
@classmethod
def __setup__(cls):
super(ShipmentOut, cls).__setup__()
cls._error_messages.update({
'product_limit_exceeded':
'The limit of the product %s is exceeded.'
})
def product_limits_by_location(self):
ProductLimit = Pool().get('stock.location.product_limit')
return ProductLimit.product_limits_by_location(
self.customer_location.id)
@classmethod
def wait(cls, shipments):
# TODO: implement with wizard in a new method 'wait_try'
# similar to 'assign_try'.
super(ShipmentOut, cls).wait(shipments)
ProductLimit = Pool().get('stock.location.product_limit')
Uom = Pool().get('product.uom')
cache = set()
for shipment in shipments:
location = shipment.customer_location
for move in shipment.outgoing_moves:
if (move.product, move.quantity) in cache:
continue
pls = ProductLimit.search([
('product', '=', move.product),
('location', '=', location)])
if len(pls) > 0:
limit = pls[0].quantity
uom = pls[0].uom
context = Transaction().context
context['product'] = move.product.id
with Transaction().set_context(context):
forecast_qty = pls[0].location.get_quantity(
[pls[0].location], 'forecast')[pls[0].location.id]
if forecast_qty > Uom.compute_qty(uom,
limit, move.product.default_uom):
cls.raise_user_warning(
'product_limit_exceeded_%s_%s' % (
shipment.id, move.product.id),
'product_limit_exceeded', move.product.name)
cache.add((move.product, move.quantity))
class ShipmentOutReturn:
__name__ = 'stock.shipment.out.return'
__metaclass__ = PoolMeta
def product_limits_by_location(self):
ProductLimit = Pool().get('stock.location.product_limit')
return ProductLimit.product_limits_by_location(
self.customer_location.id)
class Location:
__name__ = 'stock.location'
__metaclass__ = PoolMeta
limits = fields.One2Many('stock.location.product_limit', 'location',
'Limits')
limit_quantity = fields.Function(
fields.Float('Limit quantity'), 'get_limit_quantity')
diff_quantity = fields.Function(
fields.Float('Diff. quantity'), 'get_diff_quantity')
@classmethod
def get_limit_quantity(cls, locations, name):
pool = Pool()
Uom = pool.get('product.uom')
Product = pool.get('product.product')
product_id = Transaction().context.get('product')
res = dict([(l.id, 0) for l in locations])
if not product_id or not isinstance(product_id, (int, long)):
return res
default_uom = Product(product_id).default_uom
for location in locations:
if not location.limits:
continue
res[location.id] = sum(Uom.compute_qty(
l.uom, l.quantity, default_uom) for l in location.limits
if l.product.id == product_id)
return res
@classmethod
def get_diff_quantity(cls, locations, name):
return dict([(l.id, l.limit_quantity -
(l.quantity or 0)) for l in locations])
class ProductLimitNote(Report):
"""Location Product limit note"""
__name__ = 'stock.location.product_limit.note'
@classmethod
def get_context(cls, records, data):
pool = Pool()
Product = pool.get('product.product')
Company = pool.get('company.company')
Location = pool.get('stock.location')
ProductLimit = pool.get('stock.location.product_limit')
report_context = super(ProductLimitNote, cls).get_context(records,
data)
report_context['company'] = Company(data['company'])
report_context['product'] = Product(data['product'])
report_context['location'] = Location(data['location'])
def get_stock_context(start_date, end_date):
return {
'stock_date_start': start_date,
'stock_date_end': end_date,
'forecast': True}
location_id = data['location']
product_id = data['product']
with Transaction().set_context(**get_stock_context(
None, data['start_date'] + relativedelta(days=-1))):
stock = Product.products_by_location([location_id],
with_childs=True, grouping_filter=([product_id], )).get(
(location_id, product_id), 0)
with Transaction().set_context(
**get_stock_context(data['start_date'], data['end_date'])):
qties = Product.products_by_location([location_id],
with_childs=True,
grouping=('product', 'effective_date', 'origin', 'shipment'),
grouping_filter=([product_id], )
)
values = {(data['start_date'] + relativedelta(days=-1), None): stock}
for key, qty in qties.iteritems():
_origin = key[3] or key[4]
if _origin:
model, id = _origin.split(',')
_origin_value = pool.get(model)(id)
else:
_origin_value = None
values.setdefault((key[2], _origin_value), 0)
values[(key[2], _origin_value)] += qty
report_context['moves'] = cls._get_sorted_moves(values)
report_context['models'] = cls.get_models(report_context['moves'])
report_context['limit'] = ProductLimit.product_limits_by_location(
location_id, product_ids=[product_id], at_date=data['end_date'])
cumulate = 0
cumulate_moves = {}
for k, v in report_context['moves']:
cumulate_moves.setdefault(k[0], cumulate)
cumulate_moves[k[0]] += v
cumulate += v
report_context['cumulate'] = sorted([(k, v) for k, v in
cumulate_moves.iteritems()], key=lambda x: x[0])
return report_context
@classmethod
def _get_sorted_moves(cls, moves):
new_moves = [(k, v) for k, v in moves.iteritems()]
new_moves = sorted(new_moves, key=lambda x: x[0][0])
return new_moves
@classmethod
def get_models(cls, moves):
IrModel = Pool().get('ir.model')
models = [m[0][1].__name__ if m[0][1] else None for m in moves]
models = IrModel.search([
('model', 'in', models),
])
res = {None: ''}
res.update({m.model: m.name for m in models})
return res
class PrintProductLimitNoteParam(ModelView):
"""Print location product limit note param"""
__name__ = 'stock.location.product_limit.note_print.params'
start_date = fields.Date('Start date', required=True)
end_date = fields.Date('End date', required=True)
product = fields.Many2One('product.product', 'Product', required=True)
location = fields.Many2One('stock.location', 'Location', required=True,
domain=[('limits', '!=', None)])
class PrintProductLimitNote(Wizard):
"""Print Location product limit note"""
__name__ = 'stock.location.product_limit.note_print'
start = StateTransition()
params = StateView('stock.location.product_limit.note_print.params',
'stock_location_product_limit.print_params_view_form',
[Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-print', default=True)])
print_ = StateReport('stock.location.product_limit.note')
def transition_start(self):
return 'params'
def do_print_(self, action):
data = {}
if Transaction().context.get('active_ids'):
_ = Transaction().context['active_ids'].pop()
data['company'] = Transaction().context['company']
data['start_date'] = self.params.start_date
data['end_date'] = self.params.end_date
data['product'] = self.params.product.id
data['location'] = self.params.location.id
return action, data
class DeliveryNote:
__name__ = 'stock.shipment.out.delivery_note'
__metaclass__ = PoolMeta
@classmethod
def get_context(cls, records, data):
Conf = Pool().get('stock.configuration')
report_context = super(DeliveryNote, cls).get_context(records, data)
show_limit = Conf(1).show_limit
report_context['product_limits'] = {}
for r in records:
values = []
if show_limit:
values = r.product_limits_by_location()
report_context['product_limits'][r.id] = values
return report_context
class RestockingList:
__name__ = 'stock.shipment.out.return.restocking_list'
__metaclass__ = PoolMeta
@classmethod
def get_context(cls, records, data):
Conf = Pool().get('stock.configuration')
report_context = super(RestockingList, cls).get_context(records, data)
show_limit = Conf(1).show_limit
report_context['product_limits'] = {}
for r in records:
values = []
if show_limit:
values = r.product_limits_by_location()
report_context['product_limits'][r.id] = values
return report_context
class Configuration:
__name__ = 'stock.configuration'
__metaclass__ = PoolMeta
show_limit = fields.Boolean('Show product limits', help=(
'If checked a summary of product limits is shown in shipment reports.')
)
@staticmethod
def default_show_limit():
return True