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 ##############################################################################
24 from openerp.osv import fields, osv
25 from openerp.tools.translate import _
26 from openerp import SUPERUSER_ID
28 _logger = logging.getLogger(__name__)
30 #----------------------------------------------------------
32 #----------------------------------------------------------
34 class stock_location(osv.osv):
35 _inherit = "stock.location"
38 'valuation_in_account_id': fields.many2one('account.account', 'Stock Valuation Account (Incoming)', domain=[('type', '=', 'other')],
39 help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
40 "this account will be used to hold the value of products being moved from an internal location "
41 "into this location, instead of the generic Stock Output Account set on the product. "
42 "This has no effect for internal locations."),
43 'valuation_out_account_id': fields.many2one('account.account', 'Stock Valuation Account (Outgoing)', domain=[('type', '=', 'other')],
44 help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
45 "this account will be used to hold the value of products being moved out of this location "
46 "and into an internal location, instead of the generic Stock Output Account set on the product. "
47 "This has no effect for internal locations."),
50 #----------------------------------------------------------
52 #----------------------------------------------------------
54 class stock_quant(osv.osv):
55 _inherit = "stock.quant"
57 def _get_inventory_value(self, cr, uid, quant, context=None):
58 if quant.product_id.cost_method in ('real'):
59 return quant.cost * quant.qty
60 return super(stock_quant, self)._get_inventory_value(cr, uid, quant, context=context)
62 def _price_update(self, cr, uid, quant_ids, newprice, context=None):
63 ''' 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
67 super(stock_quant, self)._price_update(cr, uid, quant_ids, newprice, context=context)
69 for quant in self.browse(cr, uid, quant_ids, context=context):
70 move = self._get_latest_move(cr, uid, quant, context=context)
71 # this is where we post accounting entries for adjustment
72 ctx['force_valuation_amount'] = newprice - quant.cost
73 self._account_entry_move(cr, uid, [quant], move, context=ctx)
74 #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
75 #1) the product cost's method is 'real'
76 #2) we just fixed a negative quant caused by an outgoing shipment
77 if quant.product_id.cost_method == 'real' and quant.location_id.usage != 'internal':
78 self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context)
80 def _account_entry_move(self, cr, uid, quants, move, context=None):
82 Accounting Valuation Entries
84 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)
85 move: Move to use. browse record
89 location_obj = self.pool.get('stock.location')
90 location_from = move.location_id
91 location_to = quants[0].location_id
92 company_from = location_obj._location_owner(cr, uid, location_from, context=context)
93 company_to = location_obj._location_owner(cr, uid, location_to, context=context)
94 if company_from == company_to:
97 if move.product_id.valuation != 'real_time':
101 #if the quant isn't owned by the company, we don't make any valuation entry
104 #we don't make any stock valuation for negative quants because the valuation is already made for the counterpart.
105 #At that time the valuation will be made at the product cost price and afterward there will be new accounting entries
106 #to make the adjustments when we know the real cost price.
109 # Create Journal Entry for products arriving in the company
112 ctx['force_company'] = company_to.id
113 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx)
114 if location_from and location_from.usage == 'customer':
115 #goods returned from customer
116 self._create_account_move_line(cr, uid, quants, move, acc_dest, acc_valuation, journal_id, context=ctx)
118 self._create_account_move_line(cr, uid, quants, move, acc_src, acc_valuation, journal_id, context=ctx)
120 # Create Journal Entry for products leaving the company
123 ctx['force_company'] = company_from.id
124 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx)
125 if location_to and location_to.usage == 'supplier':
126 #goods returned to supplier
127 self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_src, journal_id, context=ctx)
129 self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_dest, journal_id, context=ctx)
131 def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location=False, context=None):
132 quant = super(stock_quant, self)._quant_create(cr, uid, qty, move, lot_id, owner_id, src_package_id, dest_package_id, force_location, context=context)
133 if move.product_id.valuation == 'real_time':
134 self._account_entry_move(cr, uid, [quant], move, context)
137 def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
138 res = super(stock_quant, self).move_quants_write(cr, uid, quants, move, location_dest_id, dest_package_id, context=context)
139 if move.product_id.valuation == 'real_time':
140 self._account_entry_move(cr, uid, quants, move, context=context)
144 def _get_accounting_data_for_valuation(self, cr, uid, move, context=None):
146 Return the accounts and journal to use to post Journal Entries for the real-time
147 valuation of the quant.
149 :param context: context dictionary that can explicitly mention the company to consider via the 'force_company' key
150 :returns: journal_id, source account, destination account, valuation account
151 :raise: osv.except_osv() is any mandatory account or journal is not defined.
153 product_obj = self.pool.get('product.product')
154 accounts = product_obj.get_product_accounts(cr, uid, move.product_id.id, context)
155 if move.location_id.valuation_out_account_id:
156 acc_src = move.location_id.valuation_out_account_id.id
158 acc_src = accounts['stock_account_input']
160 if move.location_dest_id.valuation_in_account_id:
161 acc_dest = move.location_dest_id.valuation_in_account_id.id
163 acc_dest = accounts['stock_account_output']
165 acc_valuation = accounts.get('property_stock_valuation_account_id', False)
166 journal_id = accounts['stock_journal']
168 if not all([acc_src, acc_dest, acc_valuation, journal_id]):
169 raise osv.except_osv(_('Error!'), _('''One of the following information is missing on the product or product category and prevents the accounting valuation entries to be created:
170 Stock Input Account: %s
171 Stock Output Account: %s
172 Stock Valuation Account: %s
174 ''') % (acc_src, acc_dest, acc_valuation, journal_id))
175 return journal_id, acc_src, acc_dest, acc_valuation
177 def _prepare_account_move_line(self, cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=None):
179 Generate the account.move.line values to post to track the stock valuation difference due to the
180 processing of the given quant.
184 currency_obj = self.pool.get('res.currency')
185 if context.get('force_valuation_amount'):
186 valuation_amount = context.get('force_valuation_amount')
188 valuation_amount = move.product_id.cost_method == 'real' and cost or move.product_id.standard_price
189 #the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
190 #the company currency... so we need to use round() before creating the accounting entries.
191 valuation_amount = currency_obj.round(cr, uid, move.company_id.currency_id, valuation_amount * qty)
192 partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False
195 'product_id': move.product_id.id,
197 'product_uom_id': move.product_id.uom_id.id,
198 'ref': move.picking_id and move.picking_id.name or False,
200 'partner_id': partner_id,
201 'debit': valuation_amount > 0 and valuation_amount or 0,
202 'credit': valuation_amount < 0 and -valuation_amount or 0,
203 'account_id': debit_account_id,
207 'product_id': move.product_id.id,
209 'product_uom_id': move.product_id.uom_id.id,
210 'ref': move.picking_id and move.picking_id.name or False,
212 'partner_id': partner_id,
213 'credit': valuation_amount > 0 and valuation_amount or 0,
214 'debit': valuation_amount < 0 and -valuation_amount or 0,
215 'account_id': credit_account_id,
217 return [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
219 def _create_account_move_line(self, cr, uid, quants, move, credit_account_id, debit_account_id, journal_id, context=None):
220 #group quants by cost
223 if quant_cost_qty.get(quant.cost):
224 quant_cost_qty[quant.cost] += quant.qty
226 quant_cost_qty[quant.cost] = quant.qty
227 move_obj = self.pool.get('account.move')
228 for cost, qty in quant_cost_qty.items():
229 move_lines = self._prepare_account_move_line(cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=context)
230 return move_obj.create(cr, uid, {'journal_id': journal_id,
231 'line_id': move_lines,
232 'period_id': self.pool.get('account.period').find(cr, uid, move.date, context=context)[0],
234 'ref': move.picking_id and move.picking_id.name}, context=context)
236 #def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None):
237 # move = self._get_latest_move(cr, uid, to_solve_quant, context=context)
238 # quant_neg_position = quant_neg.negative_dest_location_id.usage
239 # 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)
240 # #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
241 # #1) there isn't any negative quant anymore
242 # #2) the product cost's method is 'real'
243 # #3) we just fixed a negative quant caused by an outgoing shipment
244 # if not remaining_to_solve_quant and move.product_id.cost_method == 'real' and quant_neg_position != 'internal':
245 # self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context)
246 # return remaining_solving_quant, remaining_to_solve_quant
248 class stock_move(osv.osv):
249 _inherit = "stock.move"
251 def action_done(self, cr, uid, ids, context=None):
252 self.product_price_update_before_done(cr, uid, ids, context=context)
253 super(stock_move, self).action_done(cr, uid, ids, context=context)
254 self.product_price_update_after_done(cr, uid, ids, context=context)
256 def _store_average_cost_price(self, cr, uid, move, context=None):
257 ''' move is a browe record '''
258 product_obj = self.pool.get('product.product')
260 if any([q.qty <= 0 for q in move.quant_ids]):
261 #if there is a negative quant, the standard price shouldn't be updated
263 #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€
264 average_valuation_price = 0.0
265 for q in move.quant_ids:
266 average_valuation_price += q.qty * q.cost
267 average_valuation_price = average_valuation_price / move.product_qty
268 # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
269 product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=context)
270 self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context)
272 def product_price_update_before_done(self, cr, uid, ids, context=None):
273 product_obj = self.pool.get('product.product')
274 for move in self.browse(cr, uid, ids, context=context):
275 #adapt standard price on incomming moves if the product cost_method is 'average'
276 if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'):
277 product = move.product_id
278 company_currency_id = move.company_id.currency_id.id
279 ctx = {'currency_id': company_currency_id}
280 product_avail = product.qty_available
281 if product.qty_available <= 0:
282 new_std_price = move.price_unit
284 # Get the standard price
285 amount_unit = product.standard_price
286 new_std_price = ((amount_unit * product_avail) + (move.price_unit * move.product_qty)) / (product_avail + move.product_qty)
287 # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
288 product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=context)
290 def product_price_update_after_done(self, cr, uid, ids, context=None):
292 This method adapts the price on the product when necessary
294 for move in self.browse(cr, uid, ids, context=context):
295 #adapt standard price on outgoing moves if the product cost_method is 'real', so that a return
296 #or an inventory loss is made using the last value used for an outgoing valuation.
297 if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal':
298 #store the average price of the move on the move and product form
299 self._store_average_cost_price(cr, uid, move, context=context)