[IMP] Query should pass through orm and as such the refreshes can be removed in the...
[odoo/odoo.git] / addons / stock_account / stock_account.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from openerp.osv import fields, osv
23 from openerp.tools.translate import _
24 from openerp import SUPERUSER_ID, api
25 import logging
26 _logger = logging.getLogger(__name__)
27
28
29 class stock_inventory(osv.osv):
30     _inherit = "stock.inventory"
31     _columns = {
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)"),
33     }
34
35     def post_inventory(self, cr, uid, inv, context=None):
36         if context is None:
37             context = {}
38         ctx = context.copy()
39         if inv.period_id:
40             ctx['force_period'] = inv.period_id.id
41         return super(stock_inventory, self).post_inventory(cr, uid, inv, context=ctx)
42
43
44 #----------------------------------------------------------
45 # Stock Location
46 #----------------------------------------------------------
47
48 class stock_location(osv.osv):
49     _inherit = "stock.location"
50
51     _columns = {
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."),
62     }
63
64 #----------------------------------------------------------
65 # Quants
66 #----------------------------------------------------------
67
68 class stock_quant(osv.osv):
69     _inherit = "stock.quant"
70
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)
75
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
79         '''
80         if context is None:
81             context = {}
82         super(stock_quant, self)._price_update(cr, uid, quant_ids, newprice, context=context)
83         ctx = context.copy()
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)
94
95     def _account_entry_move(self, cr, uid, quants, move, context=None):
96         """
97         Accounting Valuation Entries
98
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
101         """
102         if context is None:
103             context = {}
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)
109
110         if move.product_id.valuation != 'real_time':
111             return False
112         for q in quants:
113             if q.owner_id:
114                 #if the quant isn't owned by the company, we don't make any valuation entry
115                 return False
116             if q.qty <= 0:
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.
120                 return False
121
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):
125             ctx = context.copy()
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)
131             else:
132                 self._create_account_move_line(cr, uid, quants, move, acc_src, acc_valuation, journal_id, context=ctx)
133
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):
136             ctx = context.copy()
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)
142             else:
143                 self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_dest, journal_id, context=ctx)
144
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)
149         return quant
150
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)
155         return res
156
157
158     def _get_accounting_data_for_valuation(self, cr, uid, move, context=None):
159         """
160         Return the accounts and journal to use to post Journal Entries for the real-time
161         valuation of the quant.
162
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.
166         """
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
171         else:
172             acc_src = accounts['stock_account_input']
173
174         if move.location_dest_id.valuation_in_account_id:
175             acc_dest = move.location_dest_id.valuation_in_account_id.id
176         else:
177             acc_dest = accounts['stock_account_output']
178
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
182
183     def _prepare_account_move_line(self, cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=None):
184         """
185         Generate the account.move.line values to post to track the stock valuation difference due to the
186         processing of the given quant.
187         """
188         if context is None:
189             context = {}
190         currency_obj = self.pool.get('res.currency')
191         if context.get('force_valuation_amount'):
192             valuation_amount = context.get('force_valuation_amount')
193         else:
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
196             else:
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
202         debit_line_vals = {
203                     'name': move.name,
204                     'product_id': move.product_id.id,
205                     'quantity': qty,
206                     'product_uom_id': move.product_id.uom_id.id,
207                     'ref': move.picking_id and move.picking_id.name or False,
208                     'date': move.date,
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,
213         }
214         credit_line_vals = {
215                     'name': move.name,
216                     'product_id': move.product_id.id,
217                     'quantity': qty,
218                     'product_uom_id': move.product_id.uom_id.id,
219                     'ref': move.picking_id and move.picking_id.name or False,
220                     'date': move.date,
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,
225         }
226         return [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
227
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
230         quant_cost_qty = {}
231         for quant in quants:
232             if quant_cost_qty.get(quant.cost):
233                 quant_cost_qty[quant.cost] += quant.qty
234             else:
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,
243                                       'date': move.date,
244                                       'ref': move.picking_id and move.picking_id.name}, context=context)
245
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
257
258 class stock_move(osv.osv):
259     _inherit = "stock.move"
260
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)
265         return res
266
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')
270         if any([q.qty <= 0 for q in move.quant_ids]):
271             #if there is a negative quant, the standard price shouldn't be updated
272             return
273         #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€
274         average_valuation_price = 0.0
275         for q in move.quant_ids:
276             average_valuation_price += q.qty * q.cost
277         average_valuation_price = average_valuation_price / move.product_qty
278         # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
279         product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=context)
280         self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context)
281
282     def product_price_update_before_done(self, cr, uid, ids, context=None):
283         product_obj = self.pool.get('product.product')
284         tmpl_dict = {}
285         for move in self.browse(cr, uid, ids, context=context):
286             #adapt standard price on incomming moves if the product cost_method is 'average'
287             if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'):
288                 product = move.product_id
289                 prod_tmpl_id = move.product_id.product_tmpl_id.id
290                 qty_available = move.product_id.product_tmpl_id.qty_available
291                 if tmpl_dict.get(prod_tmpl_id):
292                     product_avail = qty_available + tmpl_dict[prod_tmpl_id]
293                 else:
294                     tmpl_dict[prod_tmpl_id] = 0
295                     product_avail = qty_available
296                 if product_avail <= 0:
297                     new_std_price = move.price_unit
298                 else:
299                     # Get the standard price
300                     amount_unit = product.standard_price
301                     new_std_price = ((amount_unit * product_avail) + (move.price_unit * move.product_qty)) / (product_avail + move.product_qty)
302                 tmpl_dict[prod_tmpl_id] += move.product_qty
303                 # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
304                 product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=context)
305
306     def product_price_update_after_done(self, cr, uid, ids, context=None):
307         '''
308         This method adapts the price on the product when necessary
309         '''
310         for move in self.browse(cr, uid, ids, context=context):
311             #adapt standard price on outgoing moves if the product cost_method is 'real', so that a return
312             #or an inventory loss is made using the last value used for an outgoing valuation.
313             if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal':
314                 #store the average price of the move on the move and product form
315                 self._store_average_cost_price(cr, uid, move, context=context)