[MERGE] Merge from trunk-wms-loconopreport-jco
[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 import time
23
24 from openerp.osv import fields, osv
25 from openerp.tools.translate import _
26 from openerp import SUPERUSER_ID
27 import logging
28 _logger = logging.getLogger(__name__)
29
30 #----------------------------------------------------------
31 # Stock Location
32 #----------------------------------------------------------
33
34 class stock_location(osv.osv):
35     _inherit = "stock.location"
36
37     _columns = {
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."),
48     }
49
50 #----------------------------------------------------------
51 # Quants
52 #----------------------------------------------------------
53
54 class stock_quant(osv.osv):
55     _inherit = "stock.quant"
56
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)
61
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
64         '''
65         if context is None:
66             context = {}
67         super(stock_quant, self)._price_update(cr, uid, quant_ids, newprice, context=context)
68         ctx = context.copy()
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)
79
80     def _account_entry_move(self, cr, uid, quants, move, context=None):
81         """
82         Accounting Valuation Entries
83
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
86         """
87         if context is None:
88             context = {}
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:
95             return False
96
97         if move.product_id.valuation != 'real_time':
98             return False
99         for q in quants:
100             if q.owner_id:
101                 #if the quant isn't owned by the company, we don't make any valuation entry
102                 return False
103             if q.qty <= 0:
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.
107                 return False
108
109         # Create Journal Entry for products arriving in the company
110         if company_to:
111             ctx = context.copy()
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)
117             else:
118                 self._create_account_move_line(cr, uid, quants, move, acc_src, acc_valuation, journal_id, context=ctx)
119
120         # Create Journal Entry for products leaving the company
121         if company_from:
122             ctx = context.copy()
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)
128             else:
129                 self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_dest, journal_id, context=ctx)
130
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)
135         return quant
136
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)
141         return res
142
143
144     def _get_accounting_data_for_valuation(self, cr, uid, move, context=None):
145         """
146         Return the accounts and journal to use to post Journal Entries for the real-time
147         valuation of the quant.
148
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.
152         """
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
157         else:
158             acc_src = accounts['stock_account_input']
159
160         if move.location_dest_id.valuation_in_account_id:
161             acc_dest = move.location_dest_id.valuation_in_account_id.id
162         else:
163             acc_dest = accounts['stock_account_output']
164
165         acc_valuation = accounts.get('property_stock_valuation_account_id', False)
166         journal_id = accounts['stock_journal']
167
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
173     Stock Journal: %s
174     ''') % (acc_src, acc_dest, acc_valuation, journal_id))
175         return journal_id, acc_src, acc_dest, acc_valuation
176
177     def _prepare_account_move_line(self, cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=None):
178         """
179         Generate the account.move.line values to post to track the stock valuation difference due to the
180         processing of the given quant.
181         """
182         if context is None:
183             context = {}
184         currency_obj = self.pool.get('res.currency')
185         if context.get('force_valuation_amount'):
186             valuation_amount = context.get('force_valuation_amount')
187         else:
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
193         debit_line_vals = {
194                     'name': move.name,
195                     'product_id': move.product_id.id,
196                     'quantity': qty,
197                     'product_uom_id': move.product_id.uom_id.id,
198                     'ref': move.picking_id and move.picking_id.name or False,
199                     'date': move.date,
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,
204         }
205         credit_line_vals = {
206                     'name': move.name,
207                     'product_id': move.product_id.id,
208                     'quantity': qty,
209                     'product_uom_id': move.product_id.uom_id.id,
210                     'ref': move.picking_id and move.picking_id.name or False,
211                     'date': move.date,
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,
216         }
217         return [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
218
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
221         quant_cost_qty = {}
222         for quant in quants:
223             if quant_cost_qty.get(quant.cost):
224                 quant_cost_qty[quant.cost] += quant.qty
225             else:
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],
233                                       'date': move.date,
234                                       'ref': move.picking_id and move.picking_id.name}, context=context)
235
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
247
248 class stock_move(osv.osv):
249     _inherit = "stock.move"
250
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)
255
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')
259         move.refresh()
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
262             return
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)
271
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
283                 else:
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)
289
290     def product_price_update_after_done(self, cr, uid, ids, context=None):
291         '''
292         This method adapts the price on the product when necessary
293         '''
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)