This commit is contained in:
Cédric Krier
2008-07-12 01:27:59 +02:00
7 changed files with 168 additions and 121 deletions
+70 -69
View File
@@ -18,7 +18,7 @@ class Inventory(OSV):
location = fields.Many2One(
'stock.location', 'Location', required=True,
domain="[('type', '=', 'storage')]", states=STATES)
date = fields.DateTime('Date', readonly=True)
date = fields.Date('Date')
lost_found = fields.Many2One(
'stock.location', 'Lost and Found', required=True,
domain="[('type', '=', 'lost_found')]", states=STATES)
@@ -50,7 +50,7 @@ class Inventory(OSV):
return 'open'
def default_date(self, cursor, user, context=None):
return datetime.datetime.today()
return datetime.date.today()
def default_company(self, cursor, user, context=None):
if context.get('company'):
@@ -88,29 +88,16 @@ class Inventory(OSV):
location_ids = []
inv_by_loc = {}
for inventory in inventories:
location_ids.append(inventory.location.id)
inv_by_loc[inventory.location.id] = inventory.id
raw_data = product_obj.raw_products_by_location(cursor, user,
location_ids, context=context)
indexed_data = {}
for location, product, uom, qty in raw_data:
indexed_data.setdefault(location,{})[(product, uom)] = qty
for inventory in inventories:
if inventory.state != 'open':
continue
moves = []
location_data = indexed_data.get(inventory.location.id, {})
for line in inventory.lines:
key = (line.product.id, line.uom.id)
expected_qty = location_data.get(key, 0.0)
delta_qty = line.quantity - expected_qty
location_data[key] = 0
from_location = inventory.lost_found.id
to_location = inventory.location.id
delta_qty = line.expected_quantity - line.quantity
if delta_qty == 0.0:
continue
from_location = inventory.location.id
to_location = inventory.lost_found.id
if delta_qty < 0:
(from_location, to_location, delta_qty) = \
(to_location, from_location, -delta_qty)
@@ -125,31 +112,11 @@ class Inventory(OSV):
'state': 'done',
}, context=context)
moves.append(move_id)
# Create move for all missing products
for (product, uom), qty in location_data.iteritems():
if qty == 0:
continue
from_location = inventory.location.id
to_location = inventory.lost_found.id
if qty < 0:
(from_location, to_location, qty) = \
(to_location, from_location, -qty)
move_id = move_obj.create(cursor, user, {
'from_location': from_location,
'to_location': to_location,
'quantity': qty,
'product': product,
'uom': uom,
'company': inventory.company.id,
'state': 'done',
self.write(cursor, user, ids, {
'date': datetime.date.today(),
'moves': [('add', x) for x in moves],
}, context=context)
moves.append(move_id)
return self.write(cursor, user, ids, {
'date': datetime.datetime.today(),
'moves': [('add', x) for x in moves],
}, context=context)
def write(self, cursor, user, ids, vals, context=None):
if 'state' in vals:
@@ -169,9 +136,12 @@ class InventoryLine(OSV):
_description = __doc__
_rec_name = 'product'
product = fields.Many2One(
'product.product', 'Product', required=True, on_change=['product'])
uom = fields.Many2One('product.uom', 'Uom', required=True, select=1)
product = fields.Many2One('product.product', 'Product', required=True,
on_change=['product'])
uom = fields.Many2One('product.uom', 'UOM', required=True, select=1,
readonly=True)
expected_quantity = fields.Float('Expected Quantity', digits=(12, 6),
readonly=True)
quantity = fields.Float('Quantity', digits=(12, 6))
inventory = fields.Many2One('stock.inventory', 'Inventory')
@@ -211,35 +181,66 @@ class CompleteInventory(Wizard):
line_obj = self.pool.get('stock.inventory.line')
inventory_obj = self.pool.get('stock.inventory')
product_obj = self.pool.get('product.product')
uom_obj = self.pool.get('product.uom')
inventories = inventory_obj.browse(cursor, user, data['ids'],
context=context)
location_ids = []
inv_by_loc = {}
for inventory in inventories:
location_ids.append(inventory.location.id)
inv_by_loc[inventory.location.id] = inventory.id
pbl = product_obj.products_by_location(cursor, user, location_ids,
context=context)
indexed_data = {}
for line in pbl:
indexed_data.setdefault(line['location'], []).append(
(line['product'], line['uom'], line['quantity']))
for inventory in inventories:
products = [line.product.id for line in inventory.lines]
for (product, uom, qty) in indexed_data.get(inventory.location.id,[]):
# Skip products already in the inventory:
if product in products:
continue
if qty <= 0.0:
qty = 0
line_obj.create(cursor, user, {
# Compute product quantities
if inventory.date:
context = context.copy()
context['stock_date'] = inventory.date
pbl = product_obj.products_by_location(
cursor, user, [inventory.location.id],
context=context)
else:
pbl = product_obj.products_by_location(
cursor, user, [inventory.location.id], context=context)
# Index some data
uom_by_id = {}
for uom in uom_obj.browse(
cursor, user, [line['uom'] for line in pbl], context=context):
uom_by_id[uom.id] = uom
product_qty = {}
for line in pbl:
product_qty[line['product']] = (line['quantity'],
uom_by_id[line['uom']])
# Update existing lines
for line in inventory.lines:
if line.product.id in product_qty:
quantity, uom = product_qty[line.product.id]
del product_qty[line.product.id]
# if nothing as changed, continue
if line.quantity == line.expected_quantity == quantity \
and line.uom.id == uom.id:
continue
values = {'expected_quantity': quantity,
'uom': uom.id}
# update also quantity field if not edited
if line.quantity == line.expected_quantity:
values['quantity'] = quantity
else:
values = {'expected_quantity': 0.0,}
if line.quantity == line.expected_quantity:
values['quantity'] = 0
line_obj.write(
cursor, user, line.id, values, context=context)
# Create lines if needed
for product in product_qty:
quantity, uom = product_qty[product]
values = {
'product': product,
'uom': uom,
'quantity': qty,
'expected_quantity': quantity,
'quantity': quantity,
'uom': uom.id,
'inventory': inventory.id,
}, context=context)
}
line_obj.create(
cursor, user, values, context=context)
return {}
CompleteInventory()
+6 -3
View File
@@ -87,13 +87,15 @@
<field name="type">form</field>
<field name="arch" type="xml">
<![CDATA[
<form string="Inventory Line" col="6">
<form string="Inventory Line" col="4">
<label name="product"/>
<field name="product"/>
<label name="quantity"/>
<field name="quantity"/>
<label name="uom"/>
<field name="uom"/>
<label name="expected_quantity"/>
<field name="expected_quantity"/>
<label name="quantity"/>
<field name="quantity"/>
<label name="inventory"/>
<field name="inventory"/>
</form>
@@ -107,6 +109,7 @@
<![CDATA[
<tree string="Inventory Lines" editable="bottom">
<field name="product" select="1"/>
<field name="expected_quantity"/>
<field name="quantity" select="2"/>
<field name="uom" select="2"/>
</tree>
+13 -9
View File
@@ -2,6 +2,7 @@
"Wharehouse"
from trytond.osv import fields, OSV
from trytond.wizard import Wizard, WizardOSV
import datetime
STATES = {
'readonly': "not active",
@@ -77,11 +78,13 @@ class Location(OSV):
if (not context) or (not context.get('product')):
return dict([(i,0) for i in ids])
if name != 'forecast_quantity' and context.get('stock_date'):
if context['stock_date'] != datetime.date.today():
context = context.copy()
del context['stock_date']
pbl = product_obj.products_by_location(cursor, user, location_ids=ids,
product_ids=[context['product']],
forecast=(name == 'forecast_quantity'), with_childs=True,
product_ids=[context['product']], with_childs=True,
context=context)
qty_by_ltn = dict([(i['location'], i['quantity']) for i in pbl])
res = {}
for location_id in ids:
@@ -130,14 +133,14 @@ class Party(OSV):
Party()
class ChooseForecatsDateInit(WizardOSV):
_name = 'stock.location_forecast_date.init'
class ChooseStockDateInit(WizardOSV):
_name = 'stock.location_stock_date.init'
forecast_date = fields.Date(
'Forecast Date', help='Allow to compute expected '\
'stock quantities for this date.\n'\
'* An empty value is an infinite date in the future.\n'\
'* A date in the past will provide historical values.')
ChooseForecatsDateInit()
ChooseStockDateInit()
class OpenProduct(Wizard):
@@ -147,7 +150,7 @@ class OpenProduct(Wizard):
'init': {
'result': {
'type': 'form',
'object': 'stock.location_forecast_date.init',
'object': 'stock.location_stock_date.init',
'state': [
('end', 'Cancel', 'tryton-cancel'),
('open', 'Open', 'tryton-ok', True),
@@ -175,9 +178,10 @@ class OpenProduct(Wizard):
context=context)
res = act_window_obj.read(cursor, user, model_data.db_id, context=context)
context = {'locations': data['ids']}
if context == None: context = {}
context['locations'] = data['ids']
if data['form']['forecast_date']:
context['forecast_date'] = data['form']['forecast_date']
context['stock_date'] = data['form']['forecast_date']
res['context'] = str(context)
return res
+2 -2
View File
@@ -170,8 +170,8 @@
<field name="type">lost_found</field>
</record>
<record model="ir.ui.view" id="location_forecast_date_init_view_form">
<field name="model">stock.location_forecast_date.init</field>
<record model="ir.ui.view" id="location_stock_date_init_view_form">
<field name="model">stock.location_stock_date.init</field>
<field name="type">form</field>
<field name="arch" type="xml">
<![CDATA[
+2 -2
View File
@@ -297,9 +297,9 @@ class Move(OSV):
def unlink(self, cursor, user, ids, context=None):
for move in self.browse(cursor, user, ids, context=context):
if move.state != 'draft':
if move.state not in ('draft', 'cancel'):
raise ExceptORM('UserError',
'You can only delete draft moves!')
'You can only delete draft or cancelled moves!')
return super(Move, self).unlink(cursor, user, ids, context=context)
Move()
+73 -34
View File
@@ -17,11 +17,13 @@ class Product(OSV):
def get_quantity(self, cursor, user, ids, name, args, context=None):
if not (context and context.get('locations')):
return dict([(id, 0.0) for id in ids])
if name != 'forecast_quantity' and context.get('stock_date'):
if context['stock_date'] != datetime.date.today():
context = context.copy()
context['stock_date'] = datetime.date.today()
pbl = self.products_by_location(cursor, user,
location_ids=context['locations'], product_ids=ids,
forecast=(name == 'forecast_quantity'), with_childs=True,
context=context)
with_childs=True, context=context)
res = {}
for product_id in ids:
res[product_id] = 0.0
@@ -33,7 +35,7 @@ class Product(OSV):
res = True
for field, operator, operand in domain:
value = line.get(field)
if not value:
if value == None:
return False
if operator not in ("=", ">=", "<=", ">", "<", "!="):
return False
@@ -52,21 +54,35 @@ class Product(OSV):
context=context)
if name == 'forecast_quantity':
pbl = self.products_by_location(cursor, user,
location_ids=context['locations'], with_childs=True,
skip_zero=False, context=context)
for line in pbl:
line['forecast_quantity'] = line['quantity']
del line['quantity']
else:
if context.get('stock_date'):
if context['stock_date'] != datetime.date.today():
context = context.copy()
del context['stock_date']
pbl = self.products_by_location(cursor, user,
location_ids=context['locations'], with_childs=True,
skip_zero=False, context=context)
res= [line['product'] for line in pbl \
if self._search_quantity_eval_domain(line, domain)]
return [('id', 'in', res)]
def raw_products_by_location(self, cursor, user, location_ids,
product_ids=None, forecast=False, with_childs=False, context=None):
product_ids=None, with_childs=False, context=None):
"""
Return a list like : [(location, product, uom, qty)] for each
location and product given as argument. Null qty are not
returned and the tuple (location, product,uom) is unique.
location and product given as argument.
Null qty are not returned and the tuple (location, product,uom)
is unique.
If with_childs, childs location are also computed.
The key stock_date in context can be used to compute stock for
other date than today.
"""
location_obj = self.pool.get('stock.location')
if not location_ids:
@@ -75,8 +91,8 @@ class Product(OSV):
rule_obj = self.pool.get('ir.rule')
move_query, move_val = rule_obj.domain_get(cursor, user, 'stock.move')
if forecast and not (context.get('forecast_date') and \
context['forecast_date'] < datetime.date.today()):
if context.get('stock_date') and \
context['stock_date'] > datetime.date.today():
in_states = ['done', 'assigned', 'draft']
out_states = ['done', 'assigned', 'draft']
else:
@@ -120,15 +136,15 @@ class Product(OSV):
",".join(["%s" for i in product_ids]) + ")"
where_vals += product_ids
if forecast and context.get('forecast_date'):
where_clause += ' AND ('\
'(effective_date IS NULL '\
'AND ( planned_date <= %s or planned_date IS NULL)) '\
'OR effective_date <= %s'\
')'
where_date = [context['forecast_date'],context['forecast_date']]
where_clause += ' AND ('\
'(effective_date IS NULL '\
'AND ( planned_date <= %s or planned_date IS NULL)) '\
'OR effective_date <= %s'\
')'
if context.get('stock_date'):
where_date = [context['stock_date'], context['stock_date']]
else:
where_date = []
where_date = [datetime.date.today(), datetime.date.today()]
cursor.execute(select_clause % (
",".join(["%s" for i in in_states]), where_clause,
",".join(["%s" for i in out_states]), where_clause),
@@ -138,8 +154,16 @@ class Product(OSV):
return cursor.fetchall()
def products_by_location(self, cursor, user, location_ids,
product_ids=None, forecast=False, with_childs=False, context=None):
product_ids=None, with_childs=False, skip_zero=True, context=None):
"""
Return a list like :
[{location: 1, product: 1, uom: 1, qty: 2}]
for each location and product given as argument.
The key stock_date in context can be used to compute stock for other
date than today.
If with_childs, childs locations are also computed.
If skip_zero, list item with quantity equal to zero are not returned.
"""
uom_obj = self.pool.get("product.uom")
product_obj = self.pool.get("product.product")
@@ -148,8 +172,7 @@ class Product(OSV):
res = {}
raw_lines = self.raw_products_by_location(cursor, user, location_ids,
product_ids, forecast=forecast, with_childs=with_childs,
context=context)
product_ids, with_childs=with_childs, context=context)
uom_ids = []
product_ids = []
@@ -168,12 +191,27 @@ class Product(OSV):
res.setdefault(key, 0.0)
res[key] += uom_obj.compute_qty(cursor, user, uom_by_id[uom],
quantity, default_uom[product], context=context)
return [{
'location': key[0],
'product':key[1],
'uom': key[2],
'quantity': val,
} for key, val in res.iteritems()]
# Remove seen products from product_ids
if key[1] in product_ids:
product_ids.remove(key[1])
res = [{'location': key[0],
'product': key[1],
'uom': key[2],
'quantity': val} for key, val in res.iteritems()]
# Complete result with missing products if asked
if not skip_zero:
for location_id in location_ids:
for product_id in product_ids:
res.append({
'location': location_id,
'product': product_id,
'uom': default_uom[product_id].id,
'quantity': 0.0,
})
return res
def view_header_get(self, cursor, user, value, view_type='form',
context=None):
@@ -187,14 +225,14 @@ class Product(OSV):
Product()
class ChooseForecatsDateInit(WizardOSV):
_name = 'stock.product_forecast_date.init'
class ChooseStockDateInit(WizardOSV):
_name = 'stock.product_stock_date.init'
forecast_date = fields.Date(
'Forecast Date', help='Allow to compute expected '\
'stock quantities for this date.\n'\
'* An empty value is an infinite date in the future.\n'\
'* A date in the past will provide historical values.')
ChooseForecatsDateInit()
ChooseStockDateInit()
class OpenLocation(Wizard):
'Products by Locations'
@@ -203,7 +241,7 @@ class OpenLocation(Wizard):
'init': {
'result': {
'type': 'form',
'object': 'stock.product_forecast_date.init',
'object': 'stock.product_stock_date.init',
'state': [
('end', 'Cancel', 'tryton-cancel'),
('open', 'Open', 'tryton-ok', True),
@@ -231,9 +269,10 @@ class OpenLocation(Wizard):
context=context)
res = act_window_obj.read(cursor, user, model_data.db_id, context=context)
context = {'product': data['id']}
if context == None: context = {}
context['product'] = data['id']
if data['form']['forecast_date']:
context['forecast_date'] = data['form']['forecast_date']
context['stock_date'] = data['form']['forecast_date']
res['context'] = str(context)
return res
+2 -2
View File
@@ -57,8 +57,8 @@
<field name="action" ref="wizard_open_location"/>
</record>
<record model="ir.ui.view" id="product_forecast_date_init_view_form">
<field name="model">product_forecast_date.init</field>
<record model="ir.ui.view" id="product_stock_date_init_view_form">
<field name="model">product_stock_date.init</field>
<field name="type">form</field>
<field name="arch" type="xml">
<![CDATA[