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 import openerp.addons.decimal_precision as dp
24 from openerp.tools.translate import _
28 class stock_landed_cost(osv.osv):
29 _name = 'stock.landed.cost'
30 _description = 'Stock Landed Cost'
31 _inherit = 'mail.thread'
35 'stock_landed_costs.mt_stock_landed_cost_open': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done',
39 def _total_amount(self, cr, uid, ids, name, args, context=None):
41 for cost in self.browse(cr, uid, ids, context=context):
43 for line in cost.cost_lines:
44 total += line.price_unit
45 result[cost.id] = total
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
54 def get_valuation_lines(self, cr, uid, ids, picking_ids=None, context=None):
55 picking_obj = self.pool.get('stock.picking')
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':
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)
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'))
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'),
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'
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),
96 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'stock.landed.cost'),
98 'date': fields.date.context_today,
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
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
111 if not credit_account_id:
112 raise osv.except_osv(_('Error!'), _('Please configure Stock Expense Account for product: %s.') % (cost_product.name))
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)
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):
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
121 aml_obj = self.pool.get('account.move.line')
122 aml_obj.create(cr, uid, {
125 'product_id': line.product_id.id,
126 'quantity': line.quantity,
127 'debit': line.additional_landed_cost,
128 'account_id': debit_account_id
130 aml_obj.create(cr, uid, {
133 'product_id': line.product_id.id,
134 'quantity': line.quantity,
135 'credit': line.additional_landed_cost,
136 'account_id': credit_account_id
139 #Create account move lines for quants already out of stock
141 aml_obj.create(cr, uid, {
142 'name': line.name + ": " + str(qty_out) + _(' already out'),
144 'product_id': line.product_id.id,
146 'credit': line.additional_landed_cost * qty_out / line.quantity,
147 'account_id': debit_account_id
149 aml_obj.create(cr, uid, {
150 'name': line.name + ": " + str(qty_out) + _(' already out'),
152 'product_id': line.product_id.id,
154 'debit': line.additional_landed_cost * qty_out / line.quantity,
155 'account_id': already_out_account_id
159 def _create_account_move(self, cr, uid, cost, context=None):
161 'journal_id': cost.account_journal_id.id,
162 'period_id': self.pool.get('account.period').find(cr, uid, cost.date, context=context)[0],
166 return self.pool.get('account.move').create(cr, uid, vals, context=context)
168 def _check_sum(self, cr, uid, landed_cost, context=None):
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
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
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:
187 def button_validate(self, cr, uid, ids, context=None):
188 quant_obj = self.pool.get('stock.quant')
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)
195 for line in cost.valuation_adjustment_lines:
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]
202 if quant.id not in quant_dict:
203 quant_dict[quant.id] = quant.cost + diff
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)
209 for quant in line.move_id.quant_ids:
210 if quant.location_id.usage != 'internal':
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)
216 def button_cancel(self, cr, uid, ids, context=None):
217 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
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)
225 for cost in self.browse(cr, uid, ids, context=None):
226 if not cost.picking_ids:
228 picking_ids = [p.id for p in cost.picking_ids]
234 vals = self.get_valuation_lines(cr, uid, [cost.id], picking_ids=picking_ids, context=context)
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)
245 for line in cost.cost_lines:
246 for valuation in cost.valuation_adjustment_lines:
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
264 value = (line.price_unit / total_line)
266 if valuation.id not in towrite_dict:
267 towrite_dict[valuation.id] = value
269 towrite_dict[valuation.id] += value
271 for key, value in towrite_dict.items():
272 line_obj.write(cr, uid, key, {'additional_landed_cost': value}, context=context)
276 class stock_landed_cost_lines(osv.osv):
277 _name = 'stock.landed.cost.lines'
278 _description = 'Stock Landed Cost Lines'
280 def onchange_product_id(self, cr, uid, ids, product_id=False, context=None):
283 return {'value': {'quantity': 0.0, 'price_unit': 0.0}}
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}
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')]),
301 class stock_valuation_adjustment_lines(osv.osv):
302 _name = 'stock.valuation.adjustment.lines'
303 _description = 'Stock Valuation Adjustment Lines'
305 def _amount_final(self, cr, uid, ids, name, args, context=None):
307 for line in self.browse(cr, uid, ids, context=context):
309 'former_cost_per_unit': 0.0,
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)
316 def _get_name(self, cr, uid, ids, name, arg, context=None):
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
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),
345 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: