diff --git a/addons/stock_account/res_config.py b/addons/stock_account/res_config.py
index bcbbab1d641..a6076bebf94 100644
--- a/addons/stock_account/res_config.py
+++ b/addons/stock_account/res_config.py
@@ -34,6 +34,8 @@ class stock_config_settings(osv.osv_memory):
help='This allows to automatically launch the invoicing wizard if the delivery is '
'to be invoiced when you send or deliver goods.\n'
'-This installs the module stock_invoice_directly.'),
+ 'module_stock_landed_costs': fields.boolean("Calculate landed costs on products",
+ help="""Install the module that allows to affect landed costs on pickings, and split them onto the different products."""),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/stock_account/res_config_view.xml b/addons/stock_account/res_config_view.xml
index 9ed855fc170..4a48349f8df 100644
--- a/addons/stock_account/res_config_view.xml
+++ b/addons/stock_account/res_config_view.xml
@@ -18,6 +18,10 @@
+
+
+
+
diff --git a/addons/stock_landed_costs/__init__.py b/addons/stock_landed_costs/__init__.py
new file mode 100644
index 00000000000..8d9926454f9
--- /dev/null
+++ b/addons/stock_landed_costs/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import product
+import stock_landed_costs
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/stock_landed_costs/__openerp__.py b/addons/stock_landed_costs/__openerp__.py
new file mode 100644
index 00000000000..bcc1ef6009b
--- /dev/null
+++ b/addons/stock_landed_costs/__openerp__.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+{
+ 'name': 'WMS Landed Costs',
+ 'version': '1.1',
+ 'author': 'OpenERP SA',
+ 'summary': 'Landed Costs',
+ 'description': """
+#TODO
+======================
+#TODO
+ """,
+ 'website': 'http://www.openerp.com',
+ 'images': [],
+ 'depends': ['stock_account'],
+ 'category': 'Warehouse Management',
+ 'sequence': 16,
+ 'demo': [
+ ],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'product_view.xml',
+ 'stock_landed_costs_view.xml',
+ 'stock_landed_costs_data.xml',
+ ],
+ 'test': [
+ ],
+ 'installable': True,
+ 'application': True,
+ 'auto_install': False,
+}
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/stock_landed_costs/product.py b/addons/stock_landed_costs/product.py
new file mode 100644
index 00000000000..93e2ec3547c
--- /dev/null
+++ b/addons/stock_landed_costs/product.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from openerp.osv import fields, osv
+
+SPLIT_METHOD = [
+ ('equal', 'Equal'),
+ ('by_quantity', 'By Quantity'),
+ ('by_current_cost_price', 'By Current Cost Price'),
+ ('by_weight', 'By Weight'),
+ ('by_volume', 'By Volume'),
+]
+
+class product_product(osv.osv):
+ _inherit = "product.product"
+
+ _columns = {
+ 'landed_cost_ok': fields.boolean('Can constitute a landed cost'),
+ 'split_method': fields.selection(SPLIT_METHOD, 'Split Method'),
+ }
+
+ _defaults = {
+ 'landed_cost_ok': False,
+ 'split_method': 'equal',
+ }
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/stock_landed_costs/product_view.xml b/addons/stock_landed_costs/product_view.xml
new file mode 100644
index 00000000000..522b93b7d89
--- /dev/null
+++ b/addons/stock_landed_costs/product_view.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ product.product.landed.cost.form
+ product.product
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/stock_landed_costs/security/ir.model.access.csv b/addons/stock_landed_costs/security/ir.model.access.csv
new file mode 100644
index 00000000000..ee279151ef5
--- /dev/null
+++ b/addons/stock_landed_costs/security/ir.model.access.csv
@@ -0,0 +1,4 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+"access_stock_landed_cost","stock.landed.cost","model_stock_landed_cost","stock.group_stock_manager",1,1,1,1
+"access_stock_landed_cost_lines","stock.landed.cost.lines","model_stock_landed_cost_lines","stock.group_stock_manager",1,1,1,1
+"access_stock_valuation_adjustment_lines","stock.valuation.adjustment.lines","model_stock_valuation_adjustment_lines","stock.group_stock_manager",1,1,1,1
diff --git a/addons/stock_landed_costs/stock_landed_costs.py b/addons/stock_landed_costs/stock_landed_costs.py
new file mode 100644
index 00000000000..4736b9bb92c
--- /dev/null
+++ b/addons/stock_landed_costs/stock_landed_costs.py
@@ -0,0 +1,320 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import time
+
+from openerp.osv import fields, osv
+import openerp.addons.decimal_precision as dp
+from openerp.tools.translate import _
+import product
+
+
+class stock_landed_cost(osv.osv):
+ _name = 'stock.landed.cost'
+ _description = 'Stock Landed Cost'
+ _inherit = 'mail.thread'
+
+ _track = {
+ 'state': {
+ 'stock_landed_costs.mt_stock_landed_cost_open': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done',
+ },
+ }
+
+ def _total_amount(self, cr, uid, ids, name, args, context=None):
+ result = {}
+ for cost in self.browse(cr, uid, ids, context=context):
+ total = 0.0
+ for line in cost.cost_lines:
+ total += line.price_unit
+ result[cost.id] = total
+ return result
+
+ def _get_cost_line(self, cr, uid, ids, context=None):
+ cost_to_recompute = []
+ for line in self.pool.get('stock.landed.cost.lines').browse(cr, uid, ids, context=context):
+ cost_to_recompute.append(line.cost_id.id)
+ return cost_to_recompute
+
+ def onchange_pickings(self, cr, uid, ids, picking_ids=None, context=None):
+ result = {'valuation_adjustment_lines': []}
+ line_obj = self.pool.get('stock.valuation.adjustment.lines')
+ picking_obj = self.pool.get('stock.picking')
+ lines = []
+ for cost in self.browse(cr, uid, ids, context=context):
+ line_ids = [line.id for line in cost.valuation_adjustment_lines]
+ line_obj.unlink(cr, uid, line_ids, context=context)
+ picking_ids = picking_ids and picking_ids[0][2] or False
+ if not picking_ids:
+ return {'value': result}
+
+ for picking in picking_obj.browse(cr, uid, picking_ids):
+ for move in picking.move_lines:
+ #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
+ if move.product_id.valuation != 'real_time' or move.product_id.cost_method != 'real':
+ continue
+ total_cost = 0.0
+ total_qty = move.product_qty
+ weight = move.product_id and move.product_id.weight * move.product_qty
+ volume = move.product_id and move.product_id.volume * move.product_qty
+ for quant in move.quant_ids:
+ total_cost += quant.cost
+ 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, flag='original')
+ lines.append(vals)
+ result['valuation_adjustment_lines'] = lines
+ return {'value': result}
+
+ _columns = {
+ 'name': fields.char('Name', size=256, required=True, states={'done': [('readonly', True)]}, track_visibility='always'),
+ 'date': fields.date('Date', required=True, states={'done': [('readonly', True)]}, track_visibility='onchange'),
+ 'picking_ids': fields.many2many('stock.picking', string='Pickings', states={'done': [('readonly', True)]}),
+ 'cost_lines': fields.one2many('stock.landed.cost.lines', 'cost_id', 'Cost Lines', states={'done': [('readonly', True)]}),
+ 'valuation_adjustment_lines': fields.one2many('stock.valuation.adjustment.lines', 'cost_id', 'Valuation Adjustments', states={'done': [('readonly', True)]}),
+ 'description': fields.text('Item Description', states={'done': [('readonly', True)]}),
+ 'amount_total': fields.function(_total_amount, type='float', string='Total', digits_compute=dp.get_precision('Account'),
+ store={
+ 'stock.landed.cost': (lambda self, cr, uid, ids, c={}: ids, ['cost_lines'], 20),
+ 'stock.landed.cost.lines': (_get_cost_line, ['price_unit', 'quantity', 'cost_id'], 20),
+ }, track_visibility='always'
+ ),
+ 'state': fields.selection([('draft', 'Draft'), ('done', 'Posted'), ('cancel', 'Cancelled')], 'State', readonly=True, track_visibility='onchange'),
+ 'account_move_id': fields.many2one('account.move', 'Journal Entry', readonly=True),
+ 'account_journal_id': fields.many2one('account.journal', 'Account Journal', required=True),
+ }
+
+ _defaults = {
+ 'state': 'draft',
+ 'date': fields.date.context_today,
+ }
+
+ def copy(self, cr, uid, id, default=None, context=None):
+ default = {} if default is None else default.copy()
+ default.update({
+ 'account_move_id': False,
+ })
+ return super(stock_landed_cost, self).copy(cr, uid, id, default=default, context=context)
+
+ def _create_accounting_entries(self, cr, uid, line, move_id, context=None):
+ product_obj = self.pool.get('product.product')
+ cost_product = line.cost_line_id and line.cost_line_id.product_id
+ if not cost_product:
+ return False
+ accounts = product_obj.get_product_accounts(cr, uid, line.product_id.id, context=context)
+ credit_account_id = accounts['stock_account_input']
+ debit_account_id = cost_product.property_account_expense and cost_product.property_account_expense.id or cost_product.categ_id.property_account_expense_categ.id
+ if not credit_account_id:
+ raise osv.except_osv(_('Error!'), _('Please configure Stock Input Account for product: %s.') % (line.product_id.name))
+ if not debit_account_id:
+ raise osv.except_osv(_('Error!'), _('Please configure Stock Expense Account for product: %s.') % (cost_product.name))
+ return self._create_account_move_line(cr, uid, line, move_id, credit_account_id, debit_account_id, context=context)
+
+ def _create_account_move_line(self, cr, uid, line, move_id, credit_account_id, debit_account_id, context=None):
+ """
+ Generate the account.move.line values to track the landed cost.
+ """
+ aml_obj = self.pool.get('account.move.line')
+ aml_obj.create(cr, uid, {
+ 'name': line.name,
+ 'move_id': move_id,
+ 'product_id': line.product_id.id,
+ 'quantity': line.quantity,
+ 'debit': line.additional_landed_cost,
+ 'account_id': debit_account_id
+ }, context=context)
+ aml_obj.create(cr, uid, {
+ 'name': line.name,
+ 'move_id': move_id,
+ 'product_id': line.product_id.id,
+ 'quantity': line.quantity,
+ 'credit': line.additional_landed_cost,
+ 'account_id': credit_account_id
+ }, context=context)
+ return True
+
+ def _create_account_move(self, cr, uid, cost, context=None):
+ vals = {
+ 'journal_id': cost.account_journal_id.id,
+ 'period_id': self.pool.get('account.period').find(cr, uid, cost.date, context=context)[0],
+ 'date': cost.date,
+ 'ref': cost.name
+ }
+ return self.pool.get('account.move').create(cr, uid, vals, context=context)
+
+ def button_validate(self, cr, uid, ids, context=None):
+ quant_obj = self.pool.get('stock.quant')
+ for cost in self.browse(cr, uid, ids, context=context):
+ if not cost.valuation_adjustment_lines:
+ raise osv.except_osv(_('Error!'), _('You cannot validate a landed cost which has no valuation line.'))
+ move_id = self._create_account_move(cr, uid, cost, context=context)
+ quant_dict = {}
+ for line in cost.valuation_adjustment_lines:
+ if not line.move_id:
+ continue
+ per_unit = line.final_cost / line.quantity
+ diff = per_unit - line.former_cost_per_unit
+ quants = [quant for quant in line.move_id.quant_ids]
+ for quant in quants:
+ if quant.id not in quant_dict:
+ quant_dict[quant.id] = quant.cost + diff
+ else:
+ quant_dict[quant.id] += diff
+ for key, value in quant_dict.items():
+ quant_obj.write(cr, uid, quant.id, {'cost': value}, context=context)
+ self._create_accounting_entries(cr, uid, line, move_id, context=context)
+ self.write(cr, uid, cost.id, {'state': 'done', 'account_move_id': move_id}, context=context)
+ return True
+
+ def button_cancel(self, cr, uid, ids, context=None):
+ self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
+ return True
+
+ def compute_landed_cost(self, cr, uid, ids, context=None):
+ line_obj = self.pool.get('stock.valuation.adjustment.lines')
+ for cost in self.browse(cr, uid, ids, context=None):
+ total_qty = 0.0
+ total_cost = 0.0
+ total_weight = 0.0
+ total_volume = 0.0
+ total_line = 0.0
+ for line in cost.valuation_adjustment_lines:
+ if line.flag == 'original':
+ total_qty += line.quantity
+ total_cost += line.former_cost
+ total_weight += line.weight
+ total_volume += line.volume
+ total_line += 1
+
+ unlink_ids = line_obj.search(cr, uid, [('cost_id', 'in', ids), ('flag', '=', 'duplicate')], context=context)
+ line_obj.unlink(cr, uid, unlink_ids, context=context)
+ for cost in self.browse(cr, uid, ids, context=None):
+ count = 0.0
+ for line in cost.cost_lines:
+ count += 1
+ for valuation in cost.valuation_adjustment_lines:
+ if count == 1:
+ line_obj.write(cr, uid, valuation.id, {'cost_line_id': line.id}, context=context)
+ continue
+ line_obj.copy(cr, uid, valuation.id, default={'cost_line_id': line.id, 'flag': 'duplicate'}, context=context)
+
+ for cost in self.browse(cr, uid, ids, context=None):
+ dict = {}
+ for line in cost.cost_lines:
+ for valuation in cost.valuation_adjustment_lines:
+ value = 0.0
+ if valuation.cost_line_id and valuation.cost_line_id.id == line.id:
+ if line.split_method == 'by_quantity' and total_qty:
+ per_unit = (line.price_unit / total_qty)
+ value = valuation.quantity * per_unit
+ elif line.split_method == 'by_weight' and total_weight:
+ per_unit = (line.price_unit / total_weight)
+ value = valuation.weight * per_unit
+ elif line.split_method == 'by_volume' and total_volume:
+ per_unit = (line.price_unit / total_volume)
+ value = valuation.volume * per_unit
+ elif line.split_method == 'equal':
+ value = (line.price_unit / total_line)
+ elif line.split_method == 'by_current_cost_price' and total_cost:
+ per_unit = (line.price_unit / total_cost)
+ value = valuation.former_cost * per_unit
+ else:
+ value = (line.price_unit / total_line)
+
+ if valuation.id not in dict:
+ dict[valuation.id] = value
+ else:
+ dict[valuation.id] += value
+
+ for key, value in dict.items():
+ line_obj.write(cr, uid, key, {'additional_landed_cost': value}, context=context)
+ return True
+
+
+class stock_landed_cost_lines(osv.osv):
+ _name = 'stock.landed.cost.lines'
+ _description = 'Stock Landed Cost Lines'
+
+ def onchange_product_id(self, cr, uid, ids, product_id=False, context=None):
+ result = {}
+ if not product_id:
+ return {'value': {'quantity': 0.0, 'price_unit': 0.0}}
+
+ product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
+ result['name'] = product.name
+ result['split_method'] = product.split_method
+ result['price_unit'] = product.standard_price
+ result['account_id'] = product.property_account_expense and cost_product.property_account_expense.id or cost_product.categ_id.property_account_expense_categ.id
+ return {'value': result}
+
+ _columns = {
+ 'name': fields.char('Description', size=256),
+ 'cost_id': fields.many2one('stock.landed.cost', 'Landed Cost', required=True, ondelete='cascade'),
+ 'product_id': fields.many2one('product.product', 'Product', required=True),
+ 'price_unit': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')),
+ 'split_method': fields.selection(product.SPLIT_METHOD, string='Split Method', required=True),
+ 'account_id': fields.many2one('account.account', 'Account', domain=[('type', '<>', 'view'), ('type', '<>', 'closed')]),
+ }
+
+class stock_valuation_adjustment_lines(osv.osv):
+ _name = 'stock.valuation.adjustment.lines'
+ _description = 'Stock Valuation Adjustment Lines'
+
+ def _amount_final(self, cr, uid, ids, name, args, context=None):
+ result = {}
+ for line in self.browse(cr, uid, ids, context=context):
+ result[line.id] = {
+ 'former_cost_per_unit': 0.0,
+ 'final_cost': 0.0,
+ }
+ result[line.id]['former_cost_per_unit'] = (line.former_cost / line.quantity if line.quantity else 1.0)
+ result[line.id]['final_cost'] = (line.former_cost + line.additional_landed_cost)
+ return result
+
+ def _get_name(self, cr, uid, ids, name, arg, context=None):
+ res = {}
+ for line in self.browse(cr, uid, ids, context=context):
+ res[line.id] = line.product_id.code or line.product_id.name or ''
+ if line.cost_line_id:
+ res[line.id] += ' - ' + line.cost_line_id.name
+ return res
+
+ _columns = {
+ 'name': fields.function(_get_name, type='char', string='Description', store=True),
+ 'cost_id': fields.many2one('stock.landed.cost', 'Landed Cost', required=True, ondelete='cascade'),
+ 'cost_line_id': fields.many2one('stock.landed.cost.lines', 'Cost Line', readonly=True),
+ 'move_id': fields.many2one('stock.move', 'Stock Move', readonly=True),
+ 'product_id': fields.many2one('product.product', 'Product', required=True),
+ 'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
+ 'weight': fields.float('Weight', digits_compute=dp.get_precision('Product Unit of Measure')),
+ 'volume': fields.float('Volume', digits_compute=dp.get_precision('Product Unit of Measure')),
+ 'former_cost': fields.float('Former Cost', digits_compute=dp.get_precision('Product Price')),
+ '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),
+ 'additional_landed_cost': fields.float('Additional Landed Cost', digits_compute=dp.get_precision('Product Price')),
+ 'final_cost': fields.function(_amount_final, multi='cost', string='Final Cost', type='float', digits_compute=dp.get_precision('Account'), store=True),
+ 'flag': fields.selection([('original', 'Original'), ('duplicate', 'Duplicate')], 'Flag', readonly=True),
+ }
+
+ _defaults = {
+ 'quantity': 1.0,
+ 'weight': 1.0,
+ 'volume': 1.0,
+ 'flag': 'original',
+ }
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/stock_landed_costs/stock_landed_costs_data.xml b/addons/stock_landed_costs/stock_landed_costs_data.xml
new file mode 100644
index 00000000000..db965fb516c
--- /dev/null
+++ b/addons/stock_landed_costs/stock_landed_costs_data.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Open
+ stock.landed.cost
+ Landed cost is opened
+
+
+
+
diff --git a/addons/stock_landed_costs/stock_landed_costs_view.xml b/addons/stock_landed_costs/stock_landed_costs_view.xml
new file mode 100644
index 00000000000..d1165f6e6c6
--- /dev/null
+++ b/addons/stock_landed_costs/stock_landed_costs_view.xml
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+ stock.landed.cost.form
+ stock.landed.cost
+
+
+
+
+
+
+
+ stock.landed.cost.tree
+ stock.landed.cost
+
+
+
+
+
+
+
+
+
+
+
+ stock.landed.cost.search
+ stock.landed.cost
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Landed Costs
+ stock.landed.cost
+ form
+ tree,form
+ {}
+
+