[FIX] When creating a procurement from a move, respect the right UoM conversions
[odoo/odoo.git] / addons / stock_landed_costs / stock_landed_costs.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 import openerp.addons.decimal_precision as dp
24 from openerp.tools.translate import _
25 import product
26
27
28 class stock_landed_cost(osv.osv):
29     _name = 'stock.landed.cost'
30     _description = 'Stock Landed Cost'
31     _inherit = 'mail.thread'
32
33     _track = {
34         'state': {
35             'stock_landed_costs.mt_stock_landed_cost_open': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done',
36         },
37     }
38
39     def _total_amount(self, cr, uid, ids, name, args, context=None):
40         result = {}
41         for cost in self.browse(cr, uid, ids, context=context):
42             total = 0.0
43             for line in cost.cost_lines:
44                 total += line.price_unit
45             result[cost.id] = total
46         return result
47
48     def _get_cost_line(self, cr, uid, ids, context=None):
49         cost_to_recompute = []
50         for line in self.pool.get('stock.landed.cost.lines').browse(cr, uid, ids, context=context):
51             cost_to_recompute.append(line.cost_id.id)
52         return cost_to_recompute
53
54     def get_valuation_lines(self, cr, uid, ids, picking_ids=None, context=None):
55         picking_obj = self.pool.get('stock.picking')
56         lines = []
57         if not picking_ids:
58             return lines
59
60         for picking in picking_obj.browse(cr, uid, picking_ids):
61             for move in picking.move_lines:
62                 #it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost
63                 if move.product_id.valuation != 'real_time' or move.product_id.cost_method != 'real':
64                     continue
65                 total_cost = 0.0
66                 total_qty = move.product_qty
67                 weight = move.product_id and move.product_id.weight * move.product_qty
68                 volume = move.product_id and move.product_id.volume * move.product_qty
69                 for quant in move.quant_ids:
70                     total_cost += quant.cost
71                 vals = dict(product_id=move.product_id.id, move_id=move.id, quantity=move.product_uom_qty, former_cost=total_cost * total_qty, weight=weight, volume=volume)
72                 lines.append(vals)
73         if not lines:
74             raise osv.except_osv(_('Error!'), _('The selected picking does not contain any move that would be impacted by landed costs. Landed costs are only possible for products configured in real time valuation with real price costing method. Please make sure it is the case, or you selected the correct picking'))
75         return lines
76
77     _columns = {
78         'name': fields.char('Name', track_visibility='always', readonly=True, copy=False),
79         'date': fields.date('Date', required=True, states={'done': [('readonly', True)]}, track_visibility='onchange', copy=False),
80         'picking_ids': fields.many2many('stock.picking', string='Pickings', states={'done': [('readonly', True)]}, copy=False),
81         'cost_lines': fields.one2many('stock.landed.cost.lines', 'cost_id', 'Cost Lines', states={'done': [('readonly', True)]}, copy=True),
82         'valuation_adjustment_lines': fields.one2many('stock.valuation.adjustment.lines', 'cost_id', 'Valuation Adjustments', states={'done': [('readonly', True)]}),
83         'description': fields.text('Item Description', states={'done': [('readonly', True)]}),
84         'amount_total': fields.function(_total_amount, type='float', string='Total', digits_compute=dp.get_precision('Account'),
85             store={
86                 'stock.landed.cost': (lambda self, cr, uid, ids, c={}: ids, ['cost_lines'], 20),
87                 'stock.landed.cost.lines': (_get_cost_line, ['price_unit', 'quantity', 'cost_id'], 20),
88             }, track_visibility='always'
89         ),
90         'state': fields.selection([('draft', 'Draft'), ('done', 'Posted'), ('cancel', 'Cancelled')], 'State', readonly=True, track_visibility='onchange', copy=False),
91         'account_move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, copy=False),
92         'account_journal_id': fields.many2one('account.journal', 'Account Journal', required=True),
93     }
94
95     _defaults = {
96         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'stock.landed.cost'),
97         'state': 'draft',
98         'date': fields.date.context_today,
99     }
100
101     def _create_accounting_entries(self, cr, uid, line, move_id, qty_out, context=None):
102         product_obj = self.pool.get('product.template')
103         cost_product = line.cost_line_id and line.cost_line_id.product_id
104         if not cost_product:
105             return False
106         accounts = product_obj.get_product_accounts(cr, uid, line.product_id.product_tmpl_id.id, context=context)
107         debit_account_id = accounts['property_stock_valuation_account_id']
108         already_out_account_id = accounts['stock_account_output']
109         credit_account_id = line.cost_line_id.account_id.id or cost_product.property_account_expense.id or cost_product.categ_id.property_account_expense_categ.id
110
111         if not credit_account_id:
112             raise osv.except_osv(_('Error!'), _('Please configure Stock Expense Account for product: %s.') % (cost_product.name))
113
114         return self._create_account_move_line(cr, uid, line, move_id, credit_account_id, debit_account_id, qty_out, already_out_account_id, context=context)
115
116     def _create_account_move_line(self, cr, uid, line, move_id, credit_account_id, debit_account_id, qty_out, already_out_account_id, context=None):
117         """
118         Generate the account.move.line values to track the landed cost.
119         Afterwards, for the goods that are already out of stock, we should create the out moves
120         """
121         aml_obj = self.pool.get('account.move.line')
122         aml_obj.create(cr, uid, {
123             'name': line.name,
124             'move_id': move_id,
125             'product_id': line.product_id.id,
126             'quantity': line.quantity,
127             'debit': line.additional_landed_cost,
128             'account_id': debit_account_id
129         }, context=context)
130         aml_obj.create(cr, uid, {
131             'name': line.name,
132             'move_id': move_id,
133             'product_id': line.product_id.id,
134             'quantity': line.quantity,
135             'credit': line.additional_landed_cost,
136             'account_id': credit_account_id
137         }, context=context)
138         
139         #Create account move lines for quants already out of stock
140         if qty_out > 0:
141             aml_obj.create(cr, uid, {
142                                      'name': line.name + ": " + str(qty_out) + _(' already out'),
143                                      'move_id': move_id,
144                                      'product_id': line.product_id.id,
145                                      'quantity': qty_out,
146                                      'credit': line.additional_landed_cost * qty_out / line.quantity,
147                                      'account_id': debit_account_id
148                                      }, context=context)
149             aml_obj.create(cr, uid, {
150                                      'name': line.name + ": " + str(qty_out) + _(' already out'),
151                                      'move_id': move_id,
152                                      'product_id': line.product_id.id,
153                                      'quantity': qty_out,
154                                      'debit': line.additional_landed_cost * qty_out / line.quantity,
155                                      'account_id': already_out_account_id
156                                      }, context=context)
157         return True
158
159     def _create_account_move(self, cr, uid, cost, context=None):
160         vals = {
161             'journal_id': cost.account_journal_id.id,
162             'period_id': self.pool.get('account.period').find(cr, uid, cost.date, context=context)[0],
163             'date': cost.date,
164             'ref': cost.name
165         }
166         return self.pool.get('account.move').create(cr, uid, vals, context=context)
167
168     def _check_sum(self, cr, uid, landed_cost, context=None):
169         """
170         Will check if each cost line its valuation lines sum to the correct amount
171         and if the overall total amount is correct also
172         """
173         costcor = {}
174         tot = 0
175         for valuation_line in landed_cost.valuation_adjustment_lines:
176             if costcor.get(valuation_line.cost_line_id):
177                 costcor[valuation_line.cost_line_id] += valuation_line.additional_landed_cost
178             else:
179                 costcor[valuation_line.cost_line_id] = valuation_line.additional_landed_cost
180             tot += valuation_line.additional_landed_cost
181         res = (tot == landed_cost.amount_total)
182         for costl in costcor.keys():
183             if costcor[costl] != costl.price_unit:
184                 res = False
185         return res
186
187     def button_validate(self, cr, uid, ids, context=None):
188         quant_obj = self.pool.get('stock.quant')
189
190         for cost in self.browse(cr, uid, ids, context=context):
191             if not cost.valuation_adjustment_lines or not self._check_sum(cr, uid, cost, context=context):
192                 raise osv.except_osv(_('Error!'), _('You cannot validate a landed cost which has no valid valuation lines.'))
193             move_id = self._create_account_move(cr, uid, cost, context=context)
194             quant_dict = {}
195             for line in cost.valuation_adjustment_lines:
196                 if not line.move_id:
197                     continue
198                 per_unit = line.final_cost / line.quantity
199                 diff = per_unit - line.former_cost_per_unit
200                 quants = [quant for quant in line.move_id.quant_ids]
201                 for quant in quants:
202                     if quant.id not in quant_dict:
203                         quant_dict[quant.id] = quant.cost + diff
204                     else:
205                         quant_dict[quant.id] += diff
206                 for key, value in quant_dict.items():
207                     quant_obj.write(cr, uid, key, {'cost': value}, context=context)
208                 qty_out = 0
209                 for quant in line.move_id.quant_ids:
210                     if quant.location_id.usage != 'internal':
211                         qty_out += quant.qty
212                 self._create_accounting_entries(cr, uid, line, move_id, qty_out, context=context)
213             self.write(cr, uid, cost.id, {'state': 'done', 'account_move_id': move_id}, context=context)
214         return True
215
216     def button_cancel(self, cr, uid, ids, context=None):
217         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
218         return True
219
220     def compute_landed_cost(self, cr, uid, ids, context=None):
221         line_obj = self.pool.get('stock.valuation.adjustment.lines')
222         unlink_ids = line_obj.search(cr, uid, [('cost_id', 'in', ids)], context=context)
223         line_obj.unlink(cr, uid, unlink_ids, context=context)
224         towrite_dict = {}
225         for cost in self.browse(cr, uid, ids, context=None):
226             if not cost.picking_ids:
227                 continue
228             picking_ids = [p.id for p in cost.picking_ids]
229             total_qty = 0.0
230             total_cost = 0.0
231             total_weight = 0.0
232             total_volume = 0.0
233             total_line = 0.0
234             vals = self.get_valuation_lines(cr, uid, [cost.id], picking_ids=picking_ids, context=context)
235             for v in vals:
236                 for line in cost.cost_lines:
237                     v.update({'cost_id': cost.id, 'cost_line_id': line.id})
238                     self.pool.get('stock.valuation.adjustment.lines').create(cr, uid, v, context=context)
239                 total_qty += v.get('quantity', 0.0)
240                 total_cost += v.get('former_cost', 0.0)
241                 total_weight += v.get('weight', 0.0)
242                 total_volume += v.get('volume', 0.0)
243                 total_line += 1
244
245             for line in cost.cost_lines:
246                 for valuation in cost.valuation_adjustment_lines:
247                     value = 0.0
248                     if valuation.cost_line_id and valuation.cost_line_id.id == line.id:
249                         if line.split_method == 'by_quantity' and total_qty:
250                             per_unit = (line.price_unit / total_qty)
251                             value = valuation.quantity * per_unit
252                         elif line.split_method == 'by_weight' and total_weight:
253                             per_unit = (line.price_unit / total_weight)
254                             value = valuation.weight * per_unit
255                         elif line.split_method == 'by_volume' and total_volume:
256                             per_unit = (line.price_unit / total_volume)
257                             value = valuation.volume * per_unit
258                         elif line.split_method == 'equal':
259                             value = (line.price_unit / total_line)
260                         elif line.split_method == 'by_current_cost_price' and total_cost:
261                             per_unit = (line.price_unit / total_cost)
262                             value = valuation.former_cost * per_unit
263                         else:
264                             value = (line.price_unit / total_line)
265
266                         if valuation.id not in towrite_dict:
267                             towrite_dict[valuation.id] = value
268                         else:
269                             towrite_dict[valuation.id] += value
270         if towrite_dict:
271             for key, value in towrite_dict.items():
272                 line_obj.write(cr, uid, key, {'additional_landed_cost': value}, context=context)
273         return True
274
275
276 class stock_landed_cost_lines(osv.osv):
277     _name = 'stock.landed.cost.lines'
278     _description = 'Stock Landed Cost Lines'
279
280     def onchange_product_id(self, cr, uid, ids, product_id=False, context=None):
281         result = {}
282         if not product_id:
283             return {'value': {'quantity': 0.0, 'price_unit': 0.0}}
284
285         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
286         result['name'] = product.name
287         result['split_method'] = product.split_method
288         result['price_unit'] = product.standard_price
289         result['account_id'] = product.property_account_expense and product.property_account_expense.id or product.categ_id.property_account_expense_categ.id
290         return {'value': result}
291
292     _columns = {
293         'name': fields.char('Description'),
294         'cost_id': fields.many2one('stock.landed.cost', 'Landed Cost', required=True, ondelete='cascade'),
295         'product_id': fields.many2one('product.product', 'Product', required=True),
296         'price_unit': fields.float('Cost', required=True, digits_compute=dp.get_precision('Product Price')),
297         'split_method': fields.selection(product.SPLIT_METHOD, string='Split Method', required=True),
298         'account_id': fields.many2one('account.account', 'Account', domain=[('type', '<>', 'view'), ('type', '<>', 'closed')]),
299     }
300
301 class stock_valuation_adjustment_lines(osv.osv):
302     _name = 'stock.valuation.adjustment.lines'
303     _description = 'Stock Valuation Adjustment Lines'
304
305     def _amount_final(self, cr, uid, ids, name, args, context=None):
306         result = {}
307         for line in self.browse(cr, uid, ids, context=context):
308             result[line.id] = {
309                 'former_cost_per_unit': 0.0,
310                 'final_cost': 0.0,
311             }
312             result[line.id]['former_cost_per_unit'] = (line.former_cost / line.quantity if line.quantity else 1.0)
313             result[line.id]['final_cost'] = (line.former_cost + line.additional_landed_cost)
314         return result
315
316     def _get_name(self, cr, uid, ids, name, arg, context=None):
317         res = {}
318         for line in self.browse(cr, uid, ids, context=context):
319             res[line.id] = line.product_id.code or line.product_id.name or ''
320             if line.cost_line_id:
321                 res[line.id] += ' - ' + line.cost_line_id.name
322         return res
323
324     _columns = {
325         'name': fields.function(_get_name, type='char', string='Description', store=True),
326         'cost_id': fields.many2one('stock.landed.cost', 'Landed Cost', required=True, ondelete='cascade'),
327         'cost_line_id': fields.many2one('stock.landed.cost.lines', 'Cost Line', readonly=True),
328         'move_id': fields.many2one('stock.move', 'Stock Move', readonly=True),
329         'product_id': fields.many2one('product.product', 'Product', required=True),
330         'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
331         'weight': fields.float('Weight', digits_compute=dp.get_precision('Product Unit of Measure')),
332         'volume': fields.float('Volume', digits_compute=dp.get_precision('Product Unit of Measure')),
333         'former_cost': fields.float('Former Cost', digits_compute=dp.get_precision('Product Price')),
334         'former_cost_per_unit': fields.function(_amount_final, multi='cost', string='Former Cost(Per Unit)', type='float', digits_compute=dp.get_precision('Account'), store=True),
335         'additional_landed_cost': fields.float('Additional Landed Cost', digits_compute=dp.get_precision('Product Price')),
336         'final_cost': fields.function(_amount_final, multi='cost', string='Final Cost', type='float', digits_compute=dp.get_precision('Account'), store=True),
337     }
338
339     _defaults = {
340         'quantity': 1.0,
341         'weight': 1.0,
342         'volume': 1.0,
343     }
344
345 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: