[ADD] history_ids/negative quants/FIFO uom

bzr revid: jco@openerp.com-20130620164106-pyhvwat7s1iptyel
This commit is contained in:
Josse Colpaert 2013-06-20 18:41:06 +02:00
parent 70c9ee6ab8
commit 55fd6f7245
3 changed files with 149 additions and 47 deletions

View File

@ -67,6 +67,7 @@ Dashboard / Reports for Purchase Management will include:
'res_config_view.xml',
],
'test': [
'test/fifo_price.yml',
'test/fifo_returns.yml',
'test/costmethodchange.yml',
'test/process/cancel_order.yml',
@ -80,7 +81,6 @@ Dashboard / Reports for Purchase Management will include:
'test/ui/duplicate_order.yml',
'test/ui/delete_order.yml',
'test/average_price.yml',
'test/fifo_price.yml',
'test/lifo_price.yml',
],
'demo': [

View File

@ -111,17 +111,17 @@
I confirm outgoing shipment of 20 kg of FIFO Ice Cream.
-
!workflow {model: stock.picking, action: button_confirm, ref: outgoing_fifo_shipment}
-
I assign this outgoing shipment
-
!python {model: stock.picking}: |
self.action_assign(cr, uid, [ref("outgoing_fifo_shipment")])
-
Process the delivery of the outgoing shipment
-
!python {model: stock.partial.picking}: |
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [ref("outgoing_fifo_shipment")], 'default_type':'out'})
self.do_partial(cr, uid, [partial_id])
-
Check 2 stock move matchings were created
-
!python {model: stock.picking}: |
assert len(self.browse(cr, uid, ref("outgoing_fifo_shipment")).move_lines[0].matching_ids_out) == 2, "It should have created 2 "
-
Check product standard price changed to 65.0
-
@ -146,16 +146,16 @@
-
!workflow {model: stock.picking, action: button_confirm, ref: outgoing_fifo_shipment_uom}
-
I assign this outgoing shipment
-
!python {model: stock.picking}: |
self.action_assign(cr, uid, [ref("outgoing_fifo_shipment_uom")])
-
Process the delivery of the outgoing shipment
-
!python {model: stock.partial.picking}: |
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [ref("outgoing_fifo_shipment_uom")], 'default_type':'out'})
self.do_partial(cr, uid, [partial_id])
-
Check 1 stock move matching was created
-
!python {model: stock.picking}: |
assert len(self.browse(cr, uid, ref("outgoing_fifo_shipment_uom")).move_lines[0].matching_ids_out) == 1, 'Should have created 1 matching'
-
Check product price changed to 80.0
-
@ -228,6 +228,15 @@
product_uom: product.product_uom_kgm
product_qty: 49.5
type: out
-
I confirm outgoing shipment of 49.5 kg
-
!workflow {model: stock.picking, action: button_confirm, ref: outgoing_fifo_shipment_cur}
-
I assign this outgoing shipment
-
!python {model: stock.picking}: |
self.action_assign(cr, uid, [ref("outgoing_fifo_shipment_cur")])
-
Process the delivery of the outgoing shipment
-
@ -237,6 +246,7 @@
-
Check rounded price is 102 euro
-
!python {model: product.product}: |
assert round(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price) == 102, "Price after doing other currency is wrong"
-
Do a delivery of an extra 10 kg
@ -256,6 +266,11 @@
I confirm outgoing shipment of 10 kg of FIFO Ice Cream.
-
!workflow {model: stock.picking, action: button_confirm, ref: outgoing_fifo_shipment_ret}
-
I assign this outgoing shipment
-
!python {model: stock.picking}: |
self.action_assign(cr, uid, [ref("outgoing_fifo_shipment_ret")])
-
Process the delivery of the outgoing shipment
-

View File

@ -484,15 +484,18 @@ class stock_quant(osv.osv):
_name = "stock.quant"
_description = "Quants"
_columns = {
'product_id': fields.many2one("product.product", "Product", required=True),
'location_id': fields.many2one("stock.location", "Location", required=True),
'qty': fields.float("Quantity", required=True), #should be in units of the product UoM
'package_id': fields.many2one("stock.quant.package"),
'reservation_id': fields.many2one("stock.move", "Stock Move"),
'prodlot_id': fields.many2one("stock.production.lot", "Serial Number"),
'history_ids': fields.many2many("stock.move", "quant", "move", string="Moves History"),
'price_unit': fields.float("Cost price"),
'create_date': fields.date("Created date or date the quant entered the system"),
'product_id': fields.many2one('product.product', 'Product', required=True),
'location_id': fields.many2one('stock.location', 'Location', required=True),
'qty': fields.float('Quantity', required=True), #should be in units of the product UoM
'package_id': fields.many2one('stock.quant.package'),
'reservation_id': fields.many2one('stock.move', 'Stock Move'),
'prodlot_id': fields.many2one('stock.production.lot', 'Serial Number'),
'history_ids': fields.many2many('stock.move', 'quant', 'move', string='Moves History'),
'price_unit': fields.float('Cost price'),
'create_date': fields.datetime('Created date or date the quant entered the system'),
'propagate_to_id': fields.many2one('stock.quant', 'Quant'), #When negative stock, the positive counterpart that was created
'history_ids': fields.many2many('stock.move', 'Moves History', help='Moves that operate(d) on this quant'),
#Might add date of last change of location also
}
@ -513,7 +516,7 @@ class stock_quant(osv.osv):
def split_and_assign_quants(self, cr, uid, quant_tuples, move, context=None):
"""
This method will split off the quants with the specified quantity
and assign the move to this quant by using reserved_id
and assign the move to this new quant by using reserved_id
Should be triggered when assigning the move
Should be executed on move only
@ -539,12 +542,34 @@ class stock_quant(osv.osv):
"""
Change location of quants
When a move is done, the quants will already have been split because of the previous method
=> you only need to change the location of the quant
It checks if there are any negative quants in the destination location
If so, it will reconcile with them first
Adds move to history_ids of quant
-> At the same time the reservations will be removed
:param move: browse_record
:param move: browse_record of the move to use
"""
uom_obj = self.pool.get("product.uom")
neg_quants = self.search(cr, uid, [('location_id', '=', move.location_dest_id.id), ('qty', '<', 0.0)], order = 'create_date, id') #= for location_id, no child_of?...
if neg_quants:
#now we need to reconcile the quant
qty_for_reconcile = uom_obj.compute_qty(cr, uid, ids, context=context)
qty_to_go = qty_for_reconcile
for quant in self.browse(cr, uid, neg_quants, context=context):
propagate = quant.propagate_to_id
if quant.qty < qty_to_go:
recon_qty = quant.qty
else:
recon_qty = qty_to_go
qty_to_go -= recon_qty
if qty_to_go <= 0.0:
break
self.write(cr, uid, ids, {'location_id': move.location_dest_id.id,
'reservation_id': False}, context=context)
'reservation_id': False,
'history_ids': [(4, move.id)]}, context=context)
self.pool.get("stock.move").write(cr, uid, [move.id], {"reserved_quant_ids": [(5, 0)]}, context=context)
@ -560,7 +585,7 @@ class stock_quant(osv.osv):
#TODO Normally, you should check the removal strategy now
#But we will assume it is FIFO for the moment
possible_quants = self.search(cr, uid, [('location_id', 'child_of', location_id), ('product_id','=',product_id),
('qty', '>', 0.0)], order = 'create_date, id', context=context)
('qty', '>', 0.0), ('reservation_id', '=', False)], order = 'create_date, id', context=context)
qty_todo = qty
res = []
for quant in self.browse(cr, uid, possible_quants, context=context):
@ -2245,18 +2270,20 @@ class stock_move(osv.osv):
continue
if move.state in ('confirmed', 'waiting'):
# Important: we must pass lock=True to _product_reserve() to avoid race conditions and double reservations
#res = self.pool.get('stock.location')._product_reserve(cr, uid, [move.location_id.id], move.product_id.id, move.product_qty, {'uom': move.product_uom.id}, lock=True)
# res = self.pool.get('stock.location')._product_reserve(cr, uid, [move.location_id.id], move.product_id.id, move.product_qty, {'uom': move.product_uom.id}, lock=True)
#Convert UoM qty -> check rounding now in product_reserver
qty = uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, move.product_id.uom_id.id)
res2 = quant_obj.choose_quants(cr, uid, move.location_id.id, move.product_id.id, qty, context=context)
#ASSUME: it is on the same location anyways -> when putaway works: can not be all the time
#Should group quants by location:
quants = {}
qtys = {}
qty = {}
for tuple in res2:
quant = quant_obj.browse(cr, uid, tuple[0], context=context) #should be out of the loop
quant = quant_obj.browse(cr, uid, tuple[0], context=context) #should be out of the loop for performance
if quant.location_id.id not in quants:
quants[quant.location_id.id] = []
qtys[quant.location_id.id] = []
@ -2504,6 +2531,51 @@ class stock_move(osv.osv):
return res
def check_total_qty(self, cr, uid, ids, context=None):
"""
This will check if the necessary quants for the moves have been reserved.
If not, it will retry to find the quants.
If it can not find it, it will have to create a negative quant
"""
quant_obj = self.pool.get("stock.quant")
uom_obj = self.pool.get("product.uom")
for move in self.browse(cr, uid, ids, context=context):
product_qty = 0.0
for quant in move.reserved_quant_ids:
product_qty += quant.qty
qty_from_move = uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, move.product_id.uom_id.id)
#Check if the entire quantity has been transformed in to quants
if qty_from_move > product_qty:
quant_tuples = quant_obj.choose_quants(cr, uid, move.location_id.id, move.product_id.id, qty_from_move - product_qty, context=context)
create_neg_quant = True
if quant_tuples:
quant_obj.split_and_assign_quants(cr, uid, quant_tuples, move, context=context)
product_qty = 0.0
for quant in move.reserved_quant_ids:
product_qty += quant.qty
if qty_from_move <= product_qty:
create_neg_quant = False
if create_neg_quant:
#To solve this, we should create a negative quant at destination and a positive quant at the source
vals_pos = {
'product_id': move.product_id.id,
'location_id': move.location_dest_id.id,
'qty': qty_from_move - product_qty,
'history_ids': [(4, move.id)],
}
vals_neg = {
'product_id': move.product_id.id,
'location_id': move.location_id.id,
'qty': -(qty_from_move - product_qty),
'propagate_to_id': vals_pos,
}
quant_id_pos = quant_obj.create(cr, uid, vals_pos, context=context)
quant_id_neg = quant_obj.create(cr, uid, vals_neg, context=context)
def action_done(self, cr, uid, ids, context=None):
""" Makes the move done and if all moves are done, it will finish the picking.
@return:
@ -2526,19 +2598,24 @@ class stock_move(osv.osv):
#Do price calc on move
quants = {}
for move in self.browse(cr, uid, ids, context=context):
quants.update(self._get_quants_from_pack(cr, uid, [move.id], context=context))
quants.update(self._get_quants_from_pack(cr, uid, [move.id], context=context))
print quants, self._get_quants_from_pack(cr, uid, [move.id], context=context)
if (move.location_id.usage in ['supplier']):
#Create quants
vals = {'product_id': move.product_id.id,
'location_id': move.location_dest_id.id,
'qty': uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, move.product_id.uom_id.id),
'create_date': fields.date.context_today(self, cr, uid, context=context),
'price_unit': move.price_unit,
#'create_date': fields.date.context_today(self, cr, uid, context=context),
'price_unit': uom_obj._compute_price(cr, uid, move.product_uom.id, move.price_unit, move.product_id.uom_id.id),
}
quant_id = self.pool.get("stock.quant").create(cr, uid, vals, context=context)
quants[move.id].append(quant_id)
else:
self.pool.get("stock.quant").move_quants(cr, uid, quants[move.id], move, context=context)
#move quants should resolve negative quants in destination
reconciled_quants = self.pool.get("stock.quant").move_quants(cr, uid, quants[move.id], move, context=context)
#Generate negative quants if necessary
self.check_total_qty(cr, uid, ids, context=context)
#Do price calculation on move -> Should pass Quants here -> is a dictionary
matchresults = self.price_calculation(cr, uid, ids, quants, context=context)
@ -2876,6 +2953,7 @@ class stock_move(osv.osv):
currency_obj = self.pool.get('res.currency')
matching_obj = self.pool.get('stock.move.matching')
uom_obj = self.pool.get('product.uom')
quant_obj = self.pool.get('stock.quant')
product_avail = {}
res = {}
@ -2899,27 +2977,36 @@ class stock_move(osv.osv):
# for inventories for example we want to use the last value used for an outgoing move
if move.location_id.usage == 'internal' and move.location_dest_id.usage != 'internal':
fifo = (cost_method != 'lifo')
tuples = product_obj.get_stock_matchings_fifolifo(cr, uid, [product.id], move_qty, fifo,
move_uom, move.company_id.currency_id.id, context=ctx) #TODO Would be better to use price_currency_id for migration?
#Ok -> do calculation based on quants
price_amount = 0.0
amount = 0.0
#Write stock matchings
for match in tuples:
matchvals = {'move_in_id': match[0], 'qty': match[1],
'move_out_id': move.id}
match_id = matching_obj.create(cr, uid, matchvals, context=context)
res[move.id].append(match_id)
price_amount += match[1] * match[2]
amount += match[1]
#if move.id in quants???
for quant in quant_obj.browse(cr, uid, quants[move.id], context=context):
price_amount += quant.qty * quant.price_unit
amount += quant.qty
# tuples = product_obj.get_stock_matchings_fifolifo(cr, uid, [product.id], move_qty, fifo,
# move_uom, move.company_id.currency_id.id, context=ctx) #TODO Would be better to use price_currency_id for migration?
# price_amount = 0.0
# amount = 0.0
# #Write stock matchings
# for match in tuples:
# matchvals = {'move_in_id': match[0], 'qty': match[1],
# 'move_out_id': move.id}
# match_id = matching_obj.create(cr, uid, matchvals, context=context)
# res[move.id].append(match_id)
# price_amount += match[1] * match[2]
# amount += match[1]
#Write price on out move
if product_avail[product.id] >= product_uom_qty and product.cost_method in ['fifo', 'lifo']:
if amount > 0:
self.write(cr, uid, move.id, {'price_unit': price_amount / amount}, context=context)
product_obj.write(cr, uid, product.id, {'standard_price': price_amount / product_uom_qty}, context=ctx)
self.write(cr, uid, move.id, {'price_unit': price_amount / move_qty}, context=context) #Should be converted
product_obj.write(cr, uid, product.id, {'standard_price': price_amount / amount}, context=ctx)
else:
raise osv.except_osv(_('Error'), _("Something went wrong finding stock moves ") + str(tuples) + str(self.search(cr, uid, [('company_id','=', company_id), ('qty_remaining', '>', 0), ('state', '=', 'done'),
('location_id.usage', '!=', 'internal'), ('location_dest_id.usage', '=', 'internal'), ('product_id', '=', product.id)],
order = 'date, id', context=context)) + str(move_qty) + str(move_uom) + str(move.company_id.currency_id.id))
pass
# raise osv.except_osv(_('Error'), _("Something went wrong finding quants ") + str(self.search(cr, uid, [('company_id','=', company_id), ('qty_remaining', '>', 0), ('state', '=', 'done'),
# ('location_id.usage', '!=', 'internal'), ('location_dest_id.usage', '=', 'internal'), ('product_id', '=', product.id)],
# order = 'date, id', context=context)) + str(move_qty) + str(move_uom) + str(move.company_id.currency_id.id))
else:
new_price = uom_obj._compute_price(cr, uid, product.uom_id.id, product.standard_price, move_uom)
self.write(cr, uid, move.id, {'price_unit': new_price}, context=ctx)
@ -2942,7 +3029,7 @@ class stock_move(osv.osv):
new_std_price = move_product_price
product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price}, context=ctx)
# Should create the stock move matchings for previous outs for the negative stock that can be matched with is in
if product_avail[product.id] < 0.0:
if product_avail[product.id] < 0.0: #TODO LATER
resneg = self._generate_negative_stock_matchings(cr, uid, [move.id], product, context=ctx)
res[move.id] += resneg
product_avail[product.id] += product_uom_qty