Files
tryton/stock.py
T

283 lines
10 KiB
Python
Raw Normal View History

2012-05-13 23:12:11 +02: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.
2013-06-21 23:25:43 +02:00
from collections import defaultdict
2012-09-24 22:18:21 +02:00
from trytond.model import ModelView, ModelSQL, Workflow, fields
2012-05-13 23:12:11 +02:00
from trytond.pyson import Eval
2012-09-24 22:18:21 +02:00
from trytond.pool import Pool, PoolMeta
2013-06-21 23:25:43 +02:00
from trytond.transaction import Transaction
from trytond.modules.stock import StockMixin
2012-09-24 22:18:21 +02:00
__all__ = ['Lot', 'LotType', 'Move', 'ShipmentIn', 'ShipmentOut',
2013-06-21 23:25:43 +02:00
'ShipmentOutReturn',
'Period', 'PeriodCacheLot',
'Inventory', 'InventoryLine']
2012-09-24 22:18:21 +02:00
__metaclass__ = PoolMeta
2012-05-13 23:12:11 +02:00
class Lot(ModelSQL, ModelView, StockMixin):
2012-05-13 23:12:11 +02:00
"Stock Lot"
2012-09-24 22:18:21 +02:00
__name__ = 'stock.lot'
2012-05-13 23:12:11 +02:00
_rec_name = 'number'
number = fields.Char('Number', required=True, select=True)
product = fields.Many2One('product.product', 'Product', required=True)
quantity = fields.Function(fields.Float('Quantity'), 'get_quantity',
searcher='search_quantity')
forecast_quantity = fields.Function(fields.Float('Forecast Quantity'),
'get_quantity', searcher='search_quantity')
@classmethod
def get_quantity(cls, lots, name):
location_ids = Transaction().context.get('locations')
products = list(set(l.product for l in lots))
return cls._get_quantity(lots, name, location_ids, products,
grouping=('product', 'lot'))
@classmethod
def search_quantity(cls, name, domain=None):
location_ids = Transaction().context.get('locations')
return cls._search_quantity(name, location_ids, domain,
grouping=('product', 'lot'))
2012-05-13 23:12:11 +02:00
class LotType(ModelSQL, ModelView):
"Stock Lot Type"
2012-09-24 22:18:21 +02:00
__name__ = 'stock.lot.type'
2012-05-13 23:12:11 +02:00
name = fields.Char('Name', required=True, translate=True)
code = fields.Char('Code', required=True)
2012-09-24 22:18:21 +02:00
class Move:
__name__ = 'stock.move'
2012-05-13 23:12:11 +02:00
lot = fields.Many2One('stock.lot', 'Lot',
domain=[
('product', '=', Eval('product')),
],
states={
'readonly': Eval('state').in_(['cancel', 'done']),
},
depends=['state', 'product'])
2012-09-24 22:18:21 +02:00
@classmethod
def __setup__(cls):
super(Move, cls).__setup__()
cls._error_messages.update({
'lot_required': 'Lot is required for move of product "%s".',
2012-05-13 23:12:11 +02:00
})
def check_lot(self):
2012-05-13 23:12:11 +02:00
"Check if lot is required"
if (self.state == 'done'
and self.internal_quantity
and not self.lot
and self.product.lot_is_required(
self.from_location, self.to_location)):
self.raise_user_error('lot_required', self.product.rec_name)
@classmethod
def validate(cls, moves):
super(Move, cls).validate(moves)
2012-09-24 22:18:21 +02:00
for move in moves:
move.check_lot()
2012-05-13 23:12:11 +02:00
2012-09-24 22:18:21 +02:00
class ShipmentIn:
__name__ = 'stock.shipment.in'
2012-05-13 23:12:11 +02:00
2012-09-24 22:18:21 +02:00
@classmethod
def _get_inventory_moves(cls, incoming_move):
2013-03-12 10:35:40 +01:00
move = super(ShipmentIn, cls)._get_inventory_moves(incoming_move)
if move and incoming_move.lot:
move.lot = incoming_move.lot
return move
2012-05-13 23:12:11 +02:00
2012-09-24 22:18:21 +02:00
class ShipmentOut:
__name__ = 'stock.shipment.out'
2012-05-13 23:12:11 +02:00
2012-09-24 22:18:21 +02:00
@classmethod
2012-05-14 21:04:19 +02:00
@ModelView.button
@Workflow.transition('packed')
2012-09-24 22:18:21 +02:00
def pack(cls, shipments):
2012-05-13 23:12:11 +02:00
pool = Pool()
2012-09-24 22:18:21 +02:00
Uom = pool.get('product.uom')
Move = pool.get('stock.move')
2012-05-13 23:12:11 +02:00
2012-09-24 22:18:21 +02:00
super(ShipmentOut, cls).pack(shipments)
2012-05-13 23:12:11 +02:00
# Unassign move to allow update
2012-12-27 21:54:38 +01:00
Move.draft([m for s in shipments for m in s.outgoing_moves])
2012-05-13 23:12:11 +02:00
2012-05-14 21:04:19 +02:00
for shipment in shipments:
outgoing_by_product = {}
for move in shipment.outgoing_moves:
outgoing_by_product.setdefault(move.product.id,
[]).append(move)
for move in shipment.inventory_moves:
if not move.lot:
continue
2012-09-24 22:18:21 +02:00
quantity = Uom.compute_qty(move.uom, move.quantity,
2012-05-14 21:04:19 +02:00
move.product.default_uom, round=False)
outgoing_moves = outgoing_by_product[move.product.id]
while outgoing_moves and quantity > 0:
out_move = outgoing_moves.pop()
2012-09-24 22:18:21 +02:00
out_quantity = Uom.compute_qty(out_move.uom,
2012-05-14 21:04:19 +02:00
out_move.quantity, out_move.product.default_uom,
round=False)
if quantity < out_quantity:
2012-09-24 22:18:21 +02:00
outgoing_moves.extend(Move.copy([out_move], default={
'quantity': out_quantity - quantity,
}))
Move.write([out_move], {
2012-05-14 21:04:19 +02:00
'quantity': quantity,
})
2012-09-24 22:18:21 +02:00
Move.write([out_move], {
2012-05-14 21:04:19 +02:00
'lot': move.lot.id,
2012-05-13 23:12:11 +02:00
})
2012-05-14 21:04:19 +02:00
quantity -= out_quantity
assert quantity <= 0
2012-05-13 23:12:11 +02:00
2012-12-27 21:54:38 +01:00
Move.assign([m for s in shipments for m in s.outgoing_moves])
2012-05-13 23:12:11 +02:00
2012-09-24 22:18:21 +02:00
class ShipmentOutReturn:
__name__ = 'stock.shipment.out.return'
2012-05-13 23:12:11 +02:00
2012-09-24 22:18:21 +02:00
@classmethod
def _get_inventory_moves(cls, incoming_move):
2013-03-12 10:35:40 +01:00
move = super(ShipmentOutReturn,
2012-09-24 22:18:21 +02:00
cls)._get_inventory_moves(incoming_move)
2013-03-12 10:35:40 +01:00
if move and incoming_move.lot:
move.lot = incoming_move.lot
return move
2013-06-21 23:25:43 +02:00
class Period:
__name__ = 'stock.period'
lot_caches = fields.One2Many('stock.period.cache.lot', 'period',
'Lot Caches', readonly=True)
@classmethod
def groupings(cls):
return super(Period, cls).groupings() + [('product', 'lot')]
@classmethod
def get_cache(cls, grouping):
pool = Pool()
Cache = super(Period, cls).get_cache(grouping)
if grouping == ('product', 'lot'):
return pool.get('stock.period.cache.lot')
return Cache
class PeriodCacheLot(ModelSQL, ModelView):
'''
Stock Period Cache per Lot
It is used to store cached computation of stock quantities per lot.
'''
__name__ = 'stock.period.cache.lot'
period = fields.Many2One('stock.period', 'Period', required=True,
readonly=True, select=True, ondelete='CASCADE')
location = fields.Many2One('stock.location', 'Location', required=True,
readonly=True, select=True, ondelete='CASCADE')
product = fields.Many2One('product.product', 'Product', required=True,
readonly=True, ondelete='CASCADE')
lot = fields.Many2One('stock.lot', 'Lot', readonly=True,
ondelete='CASCADE')
internal_quantity = fields.Float('Internal Quantity', readonly=True)
class Inventory:
__name__ = 'stock.inventory'
@classmethod
def complete_lines(cls, inventories):
pool = Pool()
Product = pool.get('product.product')
Line = pool.get('stock.inventory.line')
super(Inventory, cls).complete_lines(inventories)
# Create and/or update lines with product that will require lot for
# their moves.
to_create = []
for inventory in inventories:
product2lines = defaultdict(list)
for line in inventory.lines:
if (line.product.lot_is_required(inventory.location,
inventory.lost_found)
or line.product.lot_is_required(inventory.lost_found,
inventory.location)):
product2lines[line.product.id].append(line)
if product2lines:
with Transaction().set_context(stock_date_end=inventory.date):
pbl = Product.products_by_location([inventory.location.id],
product_ids=product2lines.keys(),
grouping=('product', 'lot'))
product_qty = defaultdict(dict)
for (location_id, product_id, lot_id), quantity \
in pbl.iteritems():
product_qty[product_id][lot_id] = quantity
products = Product.browse(product_qty.keys())
product2uom = dict((p.id, p.default_uom.id) for p in products)
for product_id, lines in product2lines.iteritems():
quantities = product_qty[product_id]
uom_id = product2uom[product_id]
for line in lines:
lot_id = line.lot.id if line.lot else None
if lot_id in quantities:
quantity = quantities.pop(lot_id)
elif lot_id is None and quantities:
lot_id = quantities.keys()[0]
quantity = quantities.pop(lot_id)
else:
lot_id = None
quantity = 0.0
values = line.update_values4complete(quantity, uom_id)
if (values or lot_id != (line.lot.id
if line.lot else None)):
values['lot'] = lot_id
Line.write([line], values)
if quantities:
for lot_id, quantity in quantities.iteritems():
values = Line.create_values4complete(product_id,
inventory, quantity, uom_id)
values['lot'] = lot_id
to_create.append(values)
if to_create:
Line.create(to_create)
class InventoryLine:
__name__ = 'stock.inventory.line'
lot = fields.Many2One('stock.lot', 'Lot',
domain=[
('product', '=', Eval('product')),
],
depends=['product'])
@classmethod
def __setup__(cls):
super(InventoryLine, cls).__setup__()
cls._order.insert(1, ('lot', 'ASC'))
def get_rec_name(self, name):
rec_name = super(InventoryLine, self).get_rec_name(name)
if self.lot:
rec_name += ' - %s' % self.lot.rec_name
return rec_name
@property
def unique_key(self):
return super(InventoryLine, self).unique_key + (self.lot,)
2013-11-28 23:22:57 +01:00
def get_move(self):
move = super(InventoryLine, self).get_move()
2013-06-21 23:25:43 +02:00
if move:
move.lot = self.lot
return move