1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from openerp.osv import fields, osv
23 from openerp.tools.translate import _
24 from openerp import SUPERUSER_ID, api
26 _logger = logging.getLogger(__name__)
29 class stock_inventory(osv.osv):
30 _inherit = "stock.inventory"
32 'period_id': fields.many2one('account.period', 'Force Valuation Period', help="Choose the accounting period where you want to value the stock moves created by the inventory instead of the default one (chosen by the inventory end date)"),
35 def post_inventory(self, cr, uid, inv, context=None):
40 ctx['force_period'] = inv.period_id.id
41 return super(stock_inventory, self).post_inventory(cr, uid, inv, context=ctx)
44 #----------------------------------------------------------
46 #----------------------------------------------------------
48 class stock_location(osv.osv):
49 _inherit = "stock.location"
52 'valuation_in_account_id': fields.many2one('account.account', 'Stock Valuation Account (Incoming)', domain=[('type', '=', 'other')],
53 help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
54 "this account will be used to hold the value of products being moved from an internal location "
55 "into this location, instead of the generic Stock Output Account set on the product. "
56 "This has no effect for internal locations."),
57 'valuation_out_account_id': fields.many2one('account.account', 'Stock Valuation Account (Outgoing)', domain=[('type', '=', 'other')],
58 help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
59 "this account will be used to hold the value of products being moved out of this location "
60 "and into an internal location, instead of the generic Stock Output Account set on the product. "
61 "This has no effect for internal locations."),
64 #----------------------------------------------------------
66 #----------------------------------------------------------
68 class stock_quant(osv.osv):
69 _inherit = "stock.quant"
71 def _get_inventory_value(self, cr, uid, quant, context=None):
72 if quant.product_id.cost_method in ('real'):
73 return quant.cost * quant.qty
74 return super(stock_quant, self)._get_inventory_value(cr, uid, quant, context=context)
76 @api.cr_uid_ids_context
77 def _price_update(self, cr, uid, quant_ids, newprice, context=None):
78 ''' This function is called at the end of negative quant reconciliation and does the accounting entries adjustemnts and the update of the product cost price if needed
82 super(stock_quant, self)._price_update(cr, uid, quant_ids, newprice, context=context)
84 for quant in self.browse(cr, uid, quant_ids, context=context):
85 move = self._get_latest_move(cr, uid, quant, context=context)
86 # this is where we post accounting entries for adjustment
87 ctx['force_valuation_amount'] = newprice - quant.cost
88 self._account_entry_move(cr, uid, [quant], move, context=ctx)
89 #update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means
90 #1) the product cost's method is 'real'
91 #2) we just fixed a negative quant caused by an outgoing shipment
92 if quant.product_id.cost_method == 'real' and quant.location_id.usage != 'internal':
93 self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context)
95 def _account_entry_move(self, cr, uid, quants, move, context=None):
97 Accounting Valuation Entries
99 quants: browse record list of Quants to create accounting valuation entries for. Unempty and all quants are supposed to have the same location id (thay already moved in)
100 move: Move to use. browse record
104 location_obj = self.pool.get('stock.location')
105 location_from = move.location_id
106 location_to = quants[0].location_id
107 company_from = location_obj._location_owner(cr, uid, location_from, context=context)
108 company_to = location_obj._location_owner(cr, uid, location_to, context=context)
110 if move.product_id.valuation != 'real_time':
114 #if the quant isn't owned by the company, we don't make any valuation entry
117 #we don't make any stock valuation for negative quants because the valuation is already made for the counterpart.
118 #At that time the valuation will be made at the product cost price and afterward there will be new accounting entries
119 #to make the adjustments when we know the real cost price.
122 #in case of routes making the link between several warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries
123 # Create Journal Entry for products arriving in the company
124 if company_to and (move.location_id.usage not in ('internal', 'transit') and move.location_dest_id.usage == 'internal' or company_from != company_to):
126 ctx['force_company'] = company_to.id
127 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx)
128 if location_from and location_from.usage == 'customer':
129 #goods returned from customer
130 self._create_account_move_line(cr, uid, quants, move, acc_dest, acc_valuation, journal_id, context=ctx)
132 self._create_account_move_line(cr, uid, quants, move, acc_src, acc_valuation, journal_id, context=ctx)
134 # Create Journal Entry for products leaving the company
135 if company_from and (move.location_id.usage == 'internal' and move.location_dest_id.usage not in ('internal', 'transit') or company_from != company_to):
137 ctx['force_company'] = company_from.id
138 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx)
139 if location_to and location_to.usage == 'supplier':
140 #goods returned to supplier
141 self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_src, journal_id, context=ctx)
143 self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_dest, journal_id, context=ctx)
145 def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False, context=None):
146 quant = super(stock_quant, self)._quant_create(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=force_location_from, force_location_to=force_location_to, context=context)
147 if move.product_id.valuation == 'real_time':
148 self._account_entry_move(cr, uid, [quant], move, context)
151 def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
152 res = super(stock_quant, self).move_quants_write(cr, uid, quants, move, location_dest_id, dest_package_id, context=context)
153 if move.product_id.valuation == 'real_time':
154 self._account_entry_move(cr, uid, quants, move, context=context)
158 def _get_accounting_data_for_valuation(self, cr, uid, move, context=None):
160 Return the accounts and journal to use to post Journal Entries for the real-time
161 valuation of the quant.
163 :param context: context dictionary that can explicitly mention the company to consider via the 'force_company' key
164 :returns: journal_id, source account, destination account, valuation account
165 :raise: osv.except_osv() is any mandatory account or journal is not defined.
167 product_obj = self.pool.get('product.template')
168 accounts = product_obj.get_product_accounts(cr, uid, move.product_id.product_tmpl_id.id, context)
169 if move.location_id.valuation_out_account_id:
170 acc_src = move.location_id.valuation_out_account_id.id
172 acc_src = accounts['stock_account_input']
174 if move.location_dest_id.valuation_in_account_id:
175 acc_dest = move.location_dest_id.valuation_in_account_id.id
177 acc_dest = accounts['stock_account_output']
179 acc_valuation = accounts.get('property_stock_valuation_account_id', False)
180 journal_id = accounts['stock_journal']
181 return journal_id, acc_src, acc_dest, acc_valuation
183 def _prepare_account_move_line(self, cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=None):
185 Generate the account.move.line values to post to track the stock valuation difference due to the
186 processing of the given quant.
190 currency_obj = self.pool.get('res.currency')
191 if context.get('force_valuation_amount'):
192 valuation_amount = context.get('force_valuation_amount')
194 if move.product_id.cost_method == 'average':
195 valuation_amount = move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal' and cost or move.product_id.standard_price
197 valuation_amount = move.product_id.cost_method == 'real' and cost or move.product_id.standard_price
198 #the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
199 #the company currency... so we need to use round() before creating the accounting entries.
200 valuation_amount = currency_obj.round(cr, uid, move.company_id.currency_id, valuation_amount * qty)
201 partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False
204 'product_id': move.product_id.id,
206 'product_uom_id': move.product_id.uom_id.id,
207 'ref': move.picking_id and move.picking_id.name or False,
209 'partner_id': partner_id,
210 'debit': valuation_amount > 0 and valuation_amount or 0,
211 'credit': valuation_amount < 0 and -valuation_amount or 0,
212 'account_id': debit_account_id,
216 'product_id': move.product_id.id,
218 'product_uom_id': move.product_id.uom_id.id,
219 'ref': move.picking_id and move.picking_id.name or False,
221 'partner_id': partner_id,
222 'credit': valuation_amount > 0 and valuation_amount or 0,
223 'debit': valuation_amount < 0 and -valuation_amount or 0,
224 'account_id': credit_account_id,
226 return [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
228 def _create_account_move_line(self, cr, uid, quants, move, credit_account_id, debit_account_id, journal_id, context=None):
229 #group quants by cost
232 if quant_cost_qty.get(quant.cost):
233 quant_cost_qty[quant.cost] += quant.qty
235 quant_cost_qty[quant.cost] = quant.qty
236 move_obj = self.pool.get('account.move')
237 for cost, qty in quant_cost_qty.items():
238 move_lines = self._prepare_account_move_line(cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=context)
239 period_id = context.get('force_period', self.pool.get('account.period').find(cr, uid, move.date, context=context)[0])
240 move_obj.create(cr, uid, {'journal_id': journal_id,
241 'line_id': move_lines,
242 'period_id': period_id,
244 'ref': move.picking_id and move.picking_id.name}, context=context)
246 #def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None):
247 # move = self._get_latest_move(cr, uid, to_solve_quant, context=context)
248 # quant_neg_position = quant_neg.negative_dest_location_id.usage
249 # remaining_solving_quant, remaining_to_solve_quant = super(stock_quant, self)._reconcile_single_negative_quant(cr, uid, to_solve_quant, quant, quant_neg, qty, context=context)
250 # #update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means
251 # #1) there isn't any negative quant anymore
252 # #2) the product cost's method is 'real'
253 # #3) we just fixed a negative quant caused by an outgoing shipment
254 # if not remaining_to_solve_quant and move.product_id.cost_method == 'real' and quant_neg_position != 'internal':
255 # self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context)
256 # return remaining_solving_quant, remaining_to_solve_quant
258 class stock_move(osv.osv):
259 _inherit = "stock.move"
261 def action_done(self, cr, uid, ids, context=None):
262 self.product_price_update_before_done(cr, uid, ids, context=context)
263 res = super(stock_move, self).action_done(cr, uid, ids, context=context)
264 self.product_price_update_after_done(cr, uid, ids, context=context)
267 def _store_average_cost_price(self, cr, uid, move, context=None):
268 ''' move is a browe record '''
269 product_obj = self.pool.get('product.product')
271 if any([q.qty <= 0 for q in move.quant_ids]):
272 #if there is a negative quant, the standard price shouldn't be updated
274 #Note: here we can't store a quant.cost directly as we may have moved out 2 units (1 unit to 5€ and 1 unit to 7€) and in case of a product return of 1 unit, we can't know which of the 2 costs has to be used (5€ or 7€?). So at that time, thanks to the average valuation price we are storing we will svaluate it at 6€
275 average_valuation_price = 0.0
276 for q in move.quant_ids:
277 average_valuation_price += q.qty * q.cost
278 average_valuation_price = average_valuation_price / move.product_qty
279 # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
280 product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=context)
281 self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context)
283 def product_price_update_before_done(self, cr, uid, ids, context=None):
284 product_obj = self.pool.get('product.product')
286 for move in self.browse(cr, uid, ids, context=context):
287 #adapt standard price on incomming moves if the product cost_method is 'average'
288 if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'):
289 product = move.product_id
290 prod_tmpl_id = move.product_id.product_tmpl_id.id
291 qty_available = move.product_id.product_tmpl_id.qty_available
292 if tmpl_dict.get(prod_tmpl_id):
293 product_avail = qty_available + tmpl_dict[prod_tmpl_id]
295 tmpl_dict[prod_tmpl_id] = 0
296 product_avail = qty_available
297 if product_avail <= 0:
298 new_std_price = move.price_unit
300 # Get the standard price
301 amount_unit = product.standard_price
302 new_std_price = ((amount_unit * product_avail) + (move.price_unit * move.product_qty)) / (product_avail + move.product_qty)
303 tmpl_dict[prod_tmpl_id] += move.product_qty
304 # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
305 product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=context)
307 def product_price_update_after_done(self, cr, uid, ids, context=None):
309 This method adapts the price on the product when necessary
311 for move in self.browse(cr, uid, ids, context=context):
312 #adapt standard price on outgoing moves if the product cost_method is 'real', so that a return
313 #or an inventory loss is made using the last value used for an outgoing valuation.
314 if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal':
315 #store the average price of the move on the move and product form
316 self._store_average_cost_price(cr, uid, move, context=context)