[IMP] Remove prints, add docs, refactor variables in price_calculation, uom_compute when negative stock for average

bzr revid: jco@openerp.com-20130531124132-rd0nolxaqgxph40b
This commit is contained in:
Josse Colpaert 2013-05-31 14:41:32 +02:00
parent fb2e5292d8
commit c9e96aa1f2
7 changed files with 115 additions and 93 deletions

View File

@ -0,0 +1,46 @@
Average price
Normal case:
------------
= When the product is purchased by purchase order and leaves towards a customer with a delivery order. We assume also
that accounting entries are generated in real-time.
- When the products are received, the stock move gets the unit price and UoM of the purchase order in company currency. The standard price of the product is updated with
(qty available * standard price + incoming qty * purchase price) / (qty available + incoming qty)
- In the stock journal, the accounting items will be generated based on this (price on move is price of purchase order)
- When a delivery order is made, it is going out at cost price (= average price which was updated during incoming move)
- When generating outgoing accounting entries, this is the total amount based on this average cost price
Case of production:
-------------------
In case of produced goods, the incoming stock move of the finished product and accounting entries can not just invent a cost price like the sum of the cost price of the parts in the BoM, as cost methods tend to be a lot more complicated than this.
When the finished good is produced, we will put the cost price from the product.
In the product form there is a link next to the cost price where the user can update it. This will also generate accounting entries as the stock will be valued differently.
Case of no purchase order
-------------------------
When no purchase order is given, the price on the stock move and generated entries is the cost price on the product
Returned Goods / Scrap / ...
----------------------------
For returning goods to supplier, the price on the original purchase order is put on the stock and account moves. That way, this would have the same effect as cancelling the original in move.
Scrap is an outgoing move at cost price.
Negative stock
--------------
If your stock is negative and you receive, the price of the product becomes the price on the purchase order.
Extra
-----
- standard price, costing method and valuation (real_time or manual) are properties. This means it is possible to use the same product in different companies with different price, valuation and costing methods.
- UoMs: On the stock move, the price is in units of the stock move, so this will get converted to the product UoM and reverse
- Currency: On the stock move, the currency is the company currency

View File

@ -0,0 +1,50 @@
FIFO/LIFO
In order to activate FIFO/LIFO, the costing method in the product form should be fifo/lifo. This is only possible when cost methods are checked under Settings > Purchase
Normal case:
------------
= When product is purchased by purchase order and leaves towards a customer with a delivery order. We assume also
that accounting entries are generated in real-time.
- When product is received, the stock move gets the unit price and UoM of the purchase order
- In the stock journal, the accounting items will be generated based on this.
- When a delivery order is made, the FIFO/LIFO algorithm is used to check which in moves correspond to this out move. A weighted average is calculated
based on the different in moves which would theoretically have gone out according to the FIFO/LIFO algorithm. This average becomes also the new cost price on the product.
Technically, these calculated matchings are saved in stock_move_matching which makes further FIFO/LIFO calculations easier.
- When generating accounting entries, the stock.move.matching table is used, to generate 1 account move line per matching. That way, one stock move will have one account
move with multiple account move lines with the amounts from the matchings.
Case of production:
-------------------
In case of produced goods, the incoming stock move of the finished product and accounting entries can not just invent a cost price like the sum of the cost price of the parts in the BoM, as costing methods tend to be a lot more complicated than this.
On the stock move, we will put the cost price of the product.
Case of no purchase order
-------------------------
When no purchase order is given, the price on the stock move and generated entries is the standard price on the product.
Returned Goods / Scrap / ...
----------------------------
Returned goods to supplier have the same calculations as a normal out, same with scrap.
Negative stocks
---------------
When an out move makes the stock (quantity on hand) become negative, the cost price of the product is not updated and stock move matchings will only be created for the moves that can be matched. (until stock is zero) If the quantity on hand became negative and afterwards we get an incoming move, the system will try to match the previous outgoing move(s) as much as possible with the incoming move. These matches will generate also the necessary accounting entries.
Inter-company
-------------
cost price, costing method and valuation (real_time or manual_periodic) are properties (are different according to the company) => when you receive/ship goods, it depends on the company of the stock move as what costing method needs to be used.
Not possible to create a stock move between two locations of different companies. You need a transit location in between. This new constraint removes the requirement for a currency or two prices on one stock move.
UoMs: As quantities need to be matched between outgoing and ingoing stock moves which can have different UoMs, it will take all these conversions into account
Currency: On the stock move, the currency is the company currency

View File

@ -121,8 +121,7 @@
Check 2 stock move matchings were created
-
!python {model: stock.picking}: |
if len(self.browse(cr, uid, ref("outgoing_fifo_shipment")).move_lines[0].matching_ids_out) != 2:
print 'Should have created 2 matchings'
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
-
@ -206,11 +205,7 @@
-
!python {model: stock.partial.picking}: |
pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_fifo_usd")).picking_ids
print pick_ids
print uid, context
print self.pool.get('res.users').browse(cr, uid, uid).company_id.id
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]})
print partial_id
self.do_partial(cr, uid, [partial_id])
-
We create delivery order of 49.500 kg
@ -236,17 +231,7 @@
Check rounded price is 102 euro = (30 kg * )
-
!python {model: product.product}: |
print round(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price, 2)
assert round(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price) == 102
-
Print price
-
!python {model: product.product}: |
print self.browse(cr, uid, ref("product_fifo_icecream")).standard_price
print self.browse(cr, uid, ref("product_fifo_icecream")).qty_available
list = self.pool.get("stock.move").search(cr, uid, [('product_id','=', ref("product_fifo_icecream"))])
for move in self.pool.get("stock.move").browse(cr, uid, list):
print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date
-
Do a delivery of an extra 10 kg
-
@ -276,16 +261,6 @@
-
!python {model: product.product}: |
assert round(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price) == round(150.0 / 1.2834)
-
Print price
-
!python {model: product.product}: |
print self.browse(cr, uid, ref("product_fifo_icecream")).standard_price
print self.browse(cr, uid, ref("product_fifo_icecream")).qty_available
print self.browse(cr, uid, ref("product_fifo_icecream"), context={'force_company': 1}).qty_available
list = self.pool.get("stock.move").search(cr, uid, [('product_id','=', ref("product_fifo_icecream"))])
for move in self.pool.get("stock.move").browse(cr, uid, list):
print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date
-
Let us create some outs to get negative stock. Create outpicking. We create delivery order of 200 kg, but will pick only 100 kg
-
@ -326,16 +301,6 @@
line = self.browse(cr, uid, partial_id, context=context).move_ids[0]
self.pool.get("stock.partial.picking.line").write(cr, uid, [line.id], {'quantity':50})
self.do_partial(cr, uid, [partial_id])
-
Print price
-
!python {model: product.product}: |
print self.browse(cr, uid, ref("product_fifo_icecream")).standard_price
print self.browse(cr, uid, ref("product_fifo_icecream")).qty_available
print self.browse(cr, uid, ref("product_fifo_icecream"), context={'force_company': 1}).qty_available
list = self.pool.get("stock.move").search(cr, uid, [('product_id','=', ref("product_fifo_icecream"))])
for move in self.pool.get("stock.move").browse(cr, uid, list):
print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date
-
Receive purchase order with 50 kg FIFO Ice Cream at 50 euro/kg
-
@ -393,9 +358,4 @@
The price of the product should have changed back to 65.0
-
!python {model: product.product}: |
print self.browse(cr, uid, ref("product_fifo_icecream")).standard_price
print self.browse(cr, uid, ref("product_fifo_icecream")).qty_available
print self.browse(cr, uid, ref("product_fifo_icecream"), context={'force_company': 1}).qty_available
list = self.pool.get("stock.move").search(cr, uid, [('product_id','=', ref("product_fifo_icecream"))], order='date')
for move in self.pool.get("stock.move").browse(cr, uid, list):
print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date
assert self.browse(cr, uid, ref("product_fifo_icecream")).standard_price == 65.0

View File

@ -75,7 +75,6 @@
return_id = self.create(cr, uid, {}, context={'active_model':'stock.picking', 'active_id': pick_ids[0].id})
res = self.create_returns(cr, uid, [return_id], context={'active_id': pick_ids[0].id})
return_move_id = self.pool.get("stock.move").search(cr, uid, [('move_returned_from','=', pick_ids[0].move_lines[0].id)])
movepick_obj = self.pool.get("stock.partial.move")
movepick_id = movepick_obj.create(cr, uid, {}, context={'active_model': 'stock.move', 'active_ids': return_move_id})
movepick_obj.do_partial(cr, uid, [movepick_id])
@ -83,15 +82,4 @@
Check the standard price of the product (fifo return icecream) changed to 70.0
-
!python {model: product.product}: |
print self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price
assert self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price == 70.0, 'Standard price should have changed to 70.0!'
-
Print price
-
!python {model: product.product}: |
print self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price
print self.browse(cr, uid, ref("product_fiforet_icecream")).qty_available
print self.browse(cr, uid, ref("product_fiforet_icecream"), context={'force_company': 1}).qty_available
list = self.pool.get("stock.move").search(cr, uid, [('product_id','=', ref("product_fiforet_icecream"))])
for move in self.pool.get("stock.move").browse(cr, uid, list):
print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date
assert self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price == 70.0, 'Standard price should have changed to 70.0!'

View File

@ -462,7 +462,6 @@ class product_product(osv.osv):
'track_outgoing': fields.boolean('Track Outgoing Lots', help="Forces to specify a Serial Number for all moves containing this product and going to a Customer Location"),
'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
#TODO: why first arg is empty?
'valuation':fields.property(type='selection', selection=[('manual_periodic', 'Periodical (manual)'),
('real_time','Real Time (automated)'),], string = 'Inventory Valuation',
help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \

View File

@ -277,7 +277,7 @@
<field name="context">{'contact_display': 'partner', 'search_default_real':1,
'search_default_location_type_internal':1,'group_by':[]}</field>
<field name="help">Stock Valuation Analysis allows you to easily check and analyse your company stock levels. The difference with the Inventory Analysis, is that
the stock is calculated based on the prices of the stock moves depending on the costing method of the product (FIFO/LIFO/Average/standard) and not just the standard price of the product.
the stock is calculated based on the prices of the stock moves depending on the cost method of the product (FIFO/LIFO/Average/standard) and not just the standard price of the product.
Sort and group by selection criteria in order to better analyse and manage your company activities. </field>
</record>
<menuitem action="action_stock_valuation_report" id="menu_action_move_match" parent="menu_traceability" sequence="3" groups="stock.group_locations"/>

View File

@ -2330,7 +2330,6 @@ class stock_move(osv.osv):
else:
account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, matches, acc_src, acc_valuation, reference_amount, reference_currency_id, 'in', context=company_ctx))]
if matches and move.product_id.cost_method in ('fifo', 'lifo'):
print "Generate accounting entries of negative matches"
outs = {}
match_obj = self.pool.get("stock.move.matching")
for match in match_obj.browse(cr, uid, matches, context=context):
@ -2443,6 +2442,7 @@ class stock_move(osv.osv):
'name': move.name,
'product_id': move.product_id and move.product_id.id or False,
'quantity': item[0],
'product_uom_id': move.product_uom.id,
'ref': move.picking_id and move.picking_id.name or False,
'date': time.strftime('%Y-%m-%d'),
'partner_id': partner_id,
@ -2453,32 +2453,13 @@ class stock_move(osv.osv):
'name': move.name,
'product_id': move.product_id and move.product_id.id or False,
'quantity': item[0],
'product_uom_id': move.product_uom.id,
'ref': move.picking_id and move.picking_id.name or False,
'date': time.strftime('%Y-%m-%d'),
'partner_id': partner_id,
'credit': item[1],
'account_id': src_account_id,
}
# if we are posting to accounts in a different currency, provide correct values in both currencies correctly
# when compatible with the optional secondary currency on the account.
# Financial Accounts only accept amounts in secondary currencies if there's no secondary currency on the account
# or if it's the same as that of the secondary amount being posted.
#TODO -> might need to be changed still for fifolifo
account_obj = self.pool.get('account.account')
src_acct, dest_acct = account_obj.browse(cr, uid, [src_account_id, dest_account_id], context=context)
src_main_currency_id = src_acct.currency_id and src_acct.currency_id.id or src_acct.company_id.currency_id.id
dest_main_currency_id = dest_acct.currency_id and dest_acct.currency_id.id or dest_acct.company_id.currency_id.id
cur_obj = self.pool.get('res.currency')
if reference_currency_id != src_main_currency_id:
# fix credit line:
credit_line_vals['credit'] = cur_obj.compute(cr, uid, reference_currency_id, src_main_currency_id, reference_amount, context=context)
if (not src_acct.currency_id) or src_acct.currency_id.id == reference_currency_id:
credit_line_vals.update(currency_id=reference_currency_id, amount_currency=-reference_amount)
if reference_currency_id != dest_main_currency_id:
# fix debit line:
debit_line_vals['debit'] = cur_obj.compute(cr, uid, reference_currency_id, dest_main_currency_id, reference_amount, context=context)
if (not dest_acct.currency_id) or dest_acct.currency_id.id == reference_currency_id:
debit_line_vals.update(currency_id=reference_currency_id, amount_currency=reference_amount)
res += [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
return res
@ -2689,25 +2670,25 @@ class stock_move(osv.osv):
for move in self.browse(cr, uid, ids, context=context):
# Initialize variables
res[move.id] = []
product_qty = move.product_qty
product_uom = move.product_uom.id
move_qty = move.product_qty
move_uom = move.product_uom.id
company_id = move.company_id.id
ctx = context.copy()
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
ctx['force_company'] = move.company_id.id
product = product_obj.browse(cr, uid, move.product_id.id, context=ctx)
cost_method = product.cost_method
product_uom_qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id, round=False)
product_uom_qty = uom_obj._compute_qty(cr, uid, move_uom, move_qty, product.uom_id.id, round=False)
if not product.id in product_avail:
product_avail[product.id] = product.qty_available
product_avail[product.id] = product.qty_available
# Check if out -> do stock move matchings and if fifo/lifo -> update price
# only update the cost price on the product form on stock moves of type == 'out' because if a valuation has to be made without PO,
# 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], product_qty, fifo,
product_uom, move.company_id.currency_id.id, context=ctx) #TODO Would be better to use price_currency_id for migration?
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
@ -2725,12 +2706,11 @@ class stock_move(osv.osv):
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)
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'),
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(product_qty) + str(product_uom) + str(move.company_id.currency_id.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,
product_uom)
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)
#Adjust product_avail when not average and move returned from
if (not move.move_returned_from or product.cost_method != 'average'):
@ -2739,16 +2719,16 @@ class stock_move(osv.osv):
#Check if in => if price 0.0, take standard price / Update price when average price and price on move != standard price
if move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal':
if move.price_unit == 0.0:
new_price = uom_obj._compute_price(cr, uid, product.uom_id.id, product.standard_price, product_uom)
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)
elif product.cost_method == 'average':
if product_avail[product.id] >= 0.0: #TODO: Could put > instead
amount_unit = product.standard_price
move_product_price = uom_obj._compute_price(cr, uid, product_uom, move.price_unit, product.uom_id.id)
move_product_price = uom_obj._compute_price(cr, uid, move_uom, move.price_unit, product.uom_id.id)
new_std_price = ((amount_unit * product_avail[product.id])\
+ (move_product_price * product_uom_qty))/(product_avail[product.id] + product_uom_qty)
else:
new_std_price = move.price_unit
new_std_price = uom_obj._compute_price(cr, uid, move_uom, move.price_unit, product.uom_id.id)
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:
@ -2765,7 +2745,6 @@ class stock_move(osv.osv):
qty = out_qty_converted
elif qty_to_go > 0.0:
qty = qty_to_go
#revert_qty = uom_obj._compute_qty(cr, uid, move.product_uom.id, qty, out_mov.product_uom.id)
revert_qty = (qty / out_qty_converted) * out_mov.qty_remaining
matchvals = {'move_in_id': move.id, 'qty': revert_qty,
'move_out_id': out_mov.id}
@ -2785,7 +2764,7 @@ class stock_move(osv.osv):
if amount >= out_mov.product_qty:
product_obj.write(cr, uid, [product.id], {'standard_price': total_price / amount}, context=ctx)
product_avail[product.id] += product_uom_qty
#The return of average products at average price could be made optional
#The return of average products at average price (could be made optional)
if move.location_id.usage == 'internal' and move.location_dest_id.usage != 'internal' and cost_method == 'average' and move.move_returned_from:
move_orig = move.move_returned_from
new_price = uom_obj._compute_price(cr, uid, move_orig.product_uom, move_orig.price_unit, product.uom_id.id)