Files
tryton/inventory.py
T

371 lines
12 KiB
Python
Raw Normal View History

2008-10-08 12:19:58 +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.
2012-03-25 00:18:19 +01:00
from trytond.model import Workflow, ModelView, ModelSQL, fields
2010-01-20 11:04:28 +01:00
from trytond.pyson import Not, Equal, Eval, Or, Bool
2013-09-06 22:34:48 +02:00
from trytond import backend
2010-08-19 22:22:44 +02:00
from trytond.transaction import Transaction
2011-07-03 13:19:14 +02:00
from trytond.pool import Pool
2008-05-06 11:24:28 +02:00
2012-09-24 22:18:17 +02:00
__all__ = ['Inventory', 'InventoryLine']
2008-05-07 02:18:29 +02:00
STATES = {
2010-01-20 11:04:28 +01:00
'readonly': Not(Equal(Eval('state'), 'draft')),
2008-05-07 02:18:29 +02:00
}
2011-08-04 16:57:42 +02:00
DEPENDS = ['state']
2008-05-06 11:24:28 +02:00
2008-05-08 17:33:44 +02:00
2012-03-25 00:18:19 +01:00
class Inventory(Workflow, ModelSQL, ModelView):
2008-05-06 11:24:28 +02:00
'Stock Inventory'
2012-09-24 22:18:17 +02:00
__name__ = 'stock.inventory'
2008-05-06 11:24:28 +02:00
location = fields.Many2One(
'stock.location', 'Location', required=True,
2008-11-11 23:33:38 +01:00
domain=[('type', '=', 'storage')], states={
2010-01-20 11:04:28 +01:00
'readonly': Or(Not(Equal(Eval('state'), 'draft')),
Bool(Eval('lines'))),
2011-08-04 16:57:42 +02:00
},
depends=['state', 'lines'])
date = fields.Date('Date', required=True, states={
2010-01-20 11:04:28 +01:00
'readonly': Or(Not(Equal(Eval('state'), 'draft')),
Bool(Eval('lines'))),
2011-08-04 16:57:42 +02:00
},
depends=['state', 'lines'])
2008-05-07 14:56:54 +02:00
lost_found = fields.Many2One(
'stock.location', 'Lost and Found', required=True,
2011-08-04 16:57:42 +02:00
domain=[('type', '=', 'lost_found')], states=STATES, depends=DEPENDS)
2008-05-06 11:24:28 +02:00
lines = fields.One2Many(
2011-08-04 16:57:42 +02:00
'stock.inventory.line', 'inventory', 'Lines', states=STATES,
depends=DEPENDS)
2010-01-20 11:04:28 +01:00
company = fields.Many2One('company.company', 'Company', required=True,
states={
'readonly': Or(Not(Equal(Eval('state'), 'draft')),
Bool(Eval('lines'))),
2011-08-04 16:57:42 +02:00
},
depends=['state', 'lines'])
2008-10-24 13:55:56 +02:00
state = fields.Selection([
2008-11-04 23:38:48 +01:00
('draft', 'Draft'),
2008-10-24 13:55:56 +02:00
('done', 'Done'),
2009-03-20 13:09:19 +01:00
('cancel', 'Canceled'),
2012-03-05 10:53:08 +01:00
], 'State', readonly=True, select=True)
2008-05-06 11:24:28 +02:00
2012-09-24 22:18:17 +02:00
@classmethod
def __setup__(cls):
super(Inventory, cls).__setup__()
cls._order.insert(0, ('date', 'DESC'))
cls._error_messages.update({
2013-02-24 16:18:17 +01:00
'delete_cancel': ('Inventory "%s" must be cancelled before '
'deletion.'),
2013-06-21 23:24:17 +02:00
'unique_line': ('Line "%s" is not unique '
'on Inventory "%s".'),
})
2012-09-24 22:18:17 +02:00
cls._transitions |= set((
2012-03-25 00:18:19 +01:00
('draft', 'done'),
('draft', 'cancel'),
))
2012-09-24 22:18:17 +02:00
cls._buttons.update({
2012-03-25 00:18:19 +01:00
'confirm': {
'invisible': Eval('state').in_(['done', 'cancel']),
},
'cancel': {
'invisible': Eval('state').in_(['cancel', 'done']),
},
'complete_lines': {
'readonly': Eval('state') != 'draft',
},
})
2008-05-07 14:56:54 +02:00
2012-09-24 22:18:17 +02:00
@classmethod
def __register__(cls, module_name):
2013-09-06 22:34:48 +02:00
TableHandler = backend.get('TableHandler')
2012-09-24 22:18:17 +02:00
super(Inventory, cls).__register__(module_name)
2010-08-19 22:22:44 +02:00
cursor = Transaction().cursor
# Add index on create_date
2012-09-24 22:18:17 +02:00
table = TableHandler(cursor, cls, module_name)
table.index_action('create_date', action='add')
2012-09-24 22:18:17 +02:00
@staticmethod
def default_state():
2008-11-04 23:38:48 +01:00
return 'draft'
2008-05-06 11:24:28 +02:00
2012-09-24 22:18:17 +02:00
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
2008-05-07 14:56:54 +02:00
2012-09-24 22:18:17 +02:00
@staticmethod
def default_company():
2012-03-27 10:44:04 +02:00
return Transaction().context.get('company')
2008-05-22 01:58:47 +02:00
2012-09-24 22:18:17 +02:00
@classmethod
def default_lost_found(cls):
Location = Pool().get('stock.location')
locations = Location.search(cls.lost_found.domain)
if len(locations) == 1:
return locations[0].id
2008-11-11 23:33:38 +01:00
2012-09-24 22:18:17 +02:00
@classmethod
def delete(cls, inventories):
# Cancel before delete
2012-09-24 22:18:17 +02:00
cls.cancel(inventories)
for inventory in inventories:
if inventory.state != 'cancel':
2012-09-24 22:18:17 +02:00
cls.raise_user_error('delete_cancel', inventory.rec_name)
super(Inventory, cls).delete(inventories)
2012-09-24 22:18:17 +02:00
@classmethod
2012-03-25 00:18:19 +01:00
@ModelView.button
@Workflow.transition('done')
2012-09-24 22:18:17 +02:00
def confirm(self, inventories):
2013-01-01 22:24:15 +01:00
Move = Pool().get('stock.move')
move_ids = []
2012-09-24 22:18:17 +02:00
for inventory in inventories:
2013-06-21 23:24:17 +02:00
keys = set()
2012-03-25 00:18:19 +01:00
for line in inventory.lines:
2013-06-21 23:24:17 +02:00
key = line.unique_key
if key in keys:
self.raise_user_error('unique_line',
(line.rec_name, inventory.rec_name))
keys.add(key)
2013-01-01 22:24:15 +01:00
move_id = line.create_move()
if move_id:
move_ids.append(move_id)
if move_ids:
Move.do(Move.browse(move_ids))
2008-10-08 12:19:58 +02:00
2012-09-24 22:18:17 +02:00
@classmethod
2012-03-25 00:18:19 +01:00
@ModelView.button
@Workflow.transition('cancel')
2012-09-24 22:18:17 +02:00
def cancel(self, inventories):
Line = Pool().get("stock.inventory.line")
Line.cancel_move([l for i in inventories for l in i.lines])
2008-05-07 14:56:54 +02:00
2012-09-24 22:18:17 +02:00
@classmethod
def copy(cls, inventories, default=None):
pool = Pool()
Date = pool.get('ir.date')
Line = pool.get('stock.inventory.line')
2008-12-05 13:54:13 +01:00
2008-11-04 23:38:48 +01:00
if default is None:
default = {}
default = default.copy()
2012-09-24 22:18:17 +02:00
default['date'] = Date.today()
2012-03-27 10:44:04 +02:00
default['lines'] = None
2008-12-05 13:54:13 +01:00
2012-09-24 22:18:17 +02:00
new_inventories = []
for inventory in inventories:
new_inventory, = super(Inventory, cls).copy([inventory],
default=default)
Line.copy(inventory.lines,
default={
'inventory': new_inventory.id,
'move': None,
})
cls.complete_lines([new_inventory])
new_inventories.append(new_inventory)
return new_inventories
2008-12-05 13:54:13 +01:00
2012-12-25 11:28:05 +01:00
@staticmethod
def complete_lines(inventories):
'''
Complete or update the inventories
'''
2011-07-03 13:19:14 +02:00
pool = Pool()
2012-09-24 22:18:17 +02:00
Line = pool.get('stock.inventory.line')
Product = pool.get('product.product')
2008-05-08 14:16:28 +02:00
2013-06-21 23:24:17 +02:00
to_create = []
2008-05-08 14:16:28 +02:00
for inventory in inventories:
# Compute product quantities
2010-08-19 22:22:44 +02:00
with Transaction().set_context(stock_date_end=inventory.date):
2012-09-24 22:18:17 +02:00
pbl = Product.products_by_location([inventory.location.id])
# Index some data
product2uom = {}
product2type = {}
product2consumable = {}
2012-09-24 22:18:17 +02:00
for product in Product.browse([line[1] for line in pbl]):
2008-08-22 11:16:10 +02:00
product2uom[product.id] = product.default_uom.id
product2type[product.id] = product.type
product2consumable[product.id] = product.consumable
product_qty = {}
for (location, product), quantity in pbl.iteritems():
product_qty[product] = (quantity, product2uom[product])
# Update existing lines
for line in inventory.lines:
2009-09-23 16:42:40 +02:00
if not (line.product.active and
line.product.type == 'goods'
and not line.product.consumable):
2012-09-24 22:18:17 +02:00
Line.delete([line])
2009-09-23 16:42:40 +02:00
continue
if line.product.id in product_qty:
quantity, uom_id = product_qty.pop(line.product.id)
elif line.product.id in product2uom:
quantity, uom_id = 0.0, product2uom[line.product.id]
else:
quantity, uom_id = 0.0, line.product.default_uom.id
2012-09-24 22:18:17 +02:00
values = line.update_values4complete(quantity, uom_id)
if values:
2012-09-24 22:18:17 +02:00
Line.write([line], values)
# Create lines if needed
for product_id in product_qty:
if (product2type[product_id] != 'goods'
2013-01-01 22:23:32 +01:00
or product2consumable[product_id]):
continue
quantity, uom_id = product_qty[product_id]
2012-09-24 22:18:17 +02:00
values = Line.create_values4complete(product_id, inventory,
quantity, uom_id)
2013-01-06 22:18:13 +01:00
to_create.append(values)
2013-06-21 23:24:17 +02:00
if to_create:
Line.create(to_create)
2008-11-04 23:38:48 +01:00
2009-02-28 17:04:50 +01:00
class InventoryLine(ModelSQL, ModelView):
2008-11-04 23:38:48 +01:00
'Stock Inventory Line'
2012-09-24 22:18:17 +02:00
__name__ = 'stock.inventory.line'
2008-11-04 23:38:48 +01:00
_rec_name = 'product'
product = fields.Many2One('product.product', 'Product', required=True,
domain=[
('type', '=', 'goods'),
('consumable', '=', False),
],
on_change=['product'])
2010-03-11 16:54:32 +01:00
uom = fields.Function(fields.Many2One('product.uom', 'UOM'), 'get_uom')
unit_digits = fields.Function(fields.Integer('Unit Digits'),
'get_unit_digits')
expected_quantity = fields.Float('Expected Quantity', required=True,
2010-03-11 22:35:29 +01:00
digits=(16, Eval('unit_digits', 2)), readonly=True,
depends=['unit_digits'])
quantity = fields.Float('Quantity', required=True,
digits=(16, Eval('unit_digits', 2)), depends=['unit_digits'])
2008-11-04 23:38:48 +01:00
move = fields.Many2One('stock.move', 'Move', readonly=True)
2009-01-08 20:04:31 +01:00
inventory = fields.Many2One('stock.inventory', 'Inventory', required=True,
ondelete='CASCADE')
2008-11-04 23:38:48 +01:00
2012-09-24 22:18:17 +02:00
@classmethod
def __setup__(cls):
super(InventoryLine, cls).__setup__()
cls._sql_constraints += [
2008-11-04 23:38:48 +01:00
('check_line_qty_pos',
'CHECK(quantity >= 0.0)', 'Line quantity must be positive.'),
2012-09-24 22:18:17 +02:00
]
cls._order.insert(0, ('product', 'ASC'))
2008-11-04 23:38:48 +01:00
2013-06-21 23:24:17 +02:00
@classmethod
def __register__(cls, module_name):
2013-09-06 22:34:48 +02:00
TableHandler = backend.get('TableHandler')
2013-06-21 23:24:17 +02:00
cursor = Transaction().cursor
super(InventoryLine, cls).__register__(module_name)
table = TableHandler(cursor, cls, module_name)
# Migration from 2.8: Remove constraint inventory_product_uniq
table.drop_constraint('inventory_product_uniq')
2012-09-24 22:18:17 +02:00
@staticmethod
def default_unit_digits():
2008-11-04 23:38:48 +01:00
return 2
2012-09-24 22:18:17 +02:00
@staticmethod
def default_expected_quantity():
return 0.
2012-09-24 22:18:17 +02:00
def on_change_product(self):
change = {}
change['unit_digits'] = 2
if self.product:
change['uom'] = self.product.default_uom.id
change['uom.rec_name'] = self.product.default_uom.rec_name
change['unit_digits'] = self.product.default_uom.digits
return change
2008-11-04 23:38:48 +01:00
2013-06-21 23:24:17 +02:00
def get_rec_name(self, name):
return self.product.rec_name
2012-09-24 22:18:17 +02:00
def get_uom(self, name):
return self.product.default_uom.id
2008-11-04 23:38:48 +01:00
2012-09-24 22:18:17 +02:00
def get_unit_digits(self, name):
return self.product.default_uom.digits
2008-11-04 23:38:48 +01:00
2013-06-21 23:24:17 +02:00
@property
def unique_key(self):
return (self.product,)
2012-09-24 22:18:17 +02:00
@classmethod
def cancel_move(cls, lines):
Move = Pool().get('stock.move')
2012-12-27 21:53:07 +01:00
Move.cancel([l.move for l in lines if l.move])
2012-09-24 22:18:17 +02:00
Move.delete([l.move for l in lines if l.move])
cls.write([l for l in lines if l.move], {
2012-03-27 10:44:04 +02:00
'move': None,
2010-08-19 22:22:44 +02:00
})
2008-11-04 23:38:48 +01:00
2013-06-21 23:24:17 +02:00
def _get_move(self):
'''
2013-06-21 23:24:17 +02:00
Return Move instance for the inventory line
'''
2012-09-24 22:18:17 +02:00
pool = Pool()
Move = pool.get('stock.move')
Uom = pool.get('product.uom')
2012-09-24 22:18:17 +02:00
delta_qty = Uom.compute_qty(self.uom,
self.expected_quantity - self.quantity,
self.uom)
2008-11-04 23:38:48 +01:00
if delta_qty == 0.0:
return
2013-06-21 23:24:17 +02:00
from_location = self.inventory.location
to_location = self.inventory.lost_found
2008-11-04 23:38:48 +01:00
if delta_qty < 0:
(from_location, to_location, delta_qty) = \
(to_location, from_location, -delta_qty)
2013-06-21 23:24:17 +02:00
move = Move(
from_location=from_location,
to_location=to_location,
quantity=delta_qty,
product=self.product,
uom=self.uom,
company=self.inventory.company,
effective_date=self.inventory.date,
)
return move
def create_move(self):
'''
Create move for an inventory line and return id
'''
self.move = self._get_move()
2012-09-24 22:18:17 +02:00
self.save()
2013-06-21 23:24:17 +02:00
return self.move.id if self.move else None
2008-11-04 23:38:48 +01:00
2012-09-24 22:18:17 +02:00
def update_values4complete(self, quantity, uom_id):
'''
Return update values to complete inventory
'''
2012-09-24 22:18:17 +02:00
values = {}
# if nothing changed, no update
2012-09-24 22:18:17 +02:00
if self.quantity == self.expected_quantity == quantity \
and self.uom.id == uom_id:
return values
values['expected_quantity'] = quantity
values['uom'] = uom_id
# update also quantity field if not edited
2012-09-24 22:18:17 +02:00
if self.quantity == self.expected_quantity:
values['quantity'] = max(quantity, 0.0)
return values
2012-09-24 22:18:17 +02:00
@classmethod
def create_values4complete(cls, product_id, inventory, quantity, uom_id):
'''
Return create values to complete inventory
'''
return {
'inventory': inventory.id,
'product': product_id,
'expected_quantity': quantity,
'quantity': max(quantity, 0.0),
'uom': uom_id,
}