From 30b2806e587ca0e43265a07b43b43a3992222bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 11 Jul 2016 14:38:27 +0200 Subject: [PATCH 01/20] [FIX] yaml_import: forward port of saas-12 fixes fa502dcab8c6f15f68ce1faf70e5dd4daa22480b: [FIX] tools: yaml_import: typo during migration 15835236f2c2e3c76d81a8ab495f2602729ac6b8: [FIX] tools: yaml_import: fields with inverse When importing from yaml do not remove valid values for computed fields with an inverse method --- openerp/tools/yaml_import.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index a2c4426595a..356b63076bb 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -500,7 +500,7 @@ class YamlInterpreter(object): return { key: val for key, val in record_dict.iteritems() - if (key in model._columns or key in model._inherit_fields) + if (key in model._columns or key in model._inherit_fields or model._fields[key].inverse) } def process_ref(self, node, field=None): @@ -518,7 +518,7 @@ class YamlInterpreter(object): if node.use: value = [inst[node.use] for inst in instances] else: - value = instance.ids + value = instances.ids elif node.id: if field and field.type == 'reference': record = self.get_record(node.id) From 168807a255e5d786f360d2ed1ce7206ae04e2ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 12 Jul 2016 12:59:38 +0200 Subject: [PATCH 02/20] [IMP] product: get_history_price multi to ease migration This method is used only in stock_account that is about to be migrated into the new API. To ease the migration the method is converted into a multi method, using a decorator. --- addons/product/product.py | 1 + addons/stock_account/wizard/stock_valuation_history.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/product/product.py b/addons/product/product.py index df40b9a51e1..481a3838084 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -1218,6 +1218,7 @@ class product_product(osv.osv): 'company_id': company_id, }, context=context) + @api.cr_uid_ids_context def get_history_price(self, cr, uid, product_id, company_id, date=None, context=None): if context is None: context = {} diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py index ace4a3fb0bd..3096e635bdd 100644 --- a/addons/stock_account/wizard/stock_valuation_history.py +++ b/addons/stock_account/wizard/stock_valuation_history.py @@ -97,7 +97,7 @@ class stock_history(osv.osv): if line.product_id.cost_method == 'real': res[line.id] = line.quantity * line.price_unit_on_quant else: - res[line.id] = line.quantity * product_obj.get_history_price(cr, uid, line.product_id.id, line.company_id.id, date=date, context=context) + res[line.id] = line.quantity * product_obj.get_history_price(cr, uid, [line.product_id.id], line.company_id.id, date=date, context=context) return res _columns = { From 368c2346b6d8c2c880af68ac53e43506d7876852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 4 May 2016 10:55:37 +0200 Subject: [PATCH 03/20] [MOV] stock_landed_cost: reorganize module --- addons/stock_landed_costs/__init__.py | 4 +--- addons/stock_landed_costs/__openerp__.py | 7 +++---- .../stock_landed_cost_data.xml} | 9 +++++++-- addons/stock_landed_costs/models/__init__.py | 6 ++++++ addons/stock_landed_costs/{ => models}/product.py | 0 .../{ => models}/purchase_config_settings.py | 0 .../stock_landed_cost.py} | 0 .../stock_landed_costs_sequence.xml | 12 ------------ .../{product_view.xml => views/product_views.xml} | 0 .../stock_landed_cost_views.xml} | 0 10 files changed, 17 insertions(+), 21 deletions(-) rename addons/stock_landed_costs/{stock_landed_costs_data.xml => data/stock_landed_cost_data.xml} (67%) create mode 100644 addons/stock_landed_costs/models/__init__.py rename addons/stock_landed_costs/{ => models}/product.py (100%) rename addons/stock_landed_costs/{ => models}/purchase_config_settings.py (100%) rename addons/stock_landed_costs/{stock_landed_costs.py => models/stock_landed_cost.py} (100%) delete mode 100644 addons/stock_landed_costs/stock_landed_costs_sequence.xml rename addons/stock_landed_costs/{product_view.xml => views/product_views.xml} (100%) rename addons/stock_landed_costs/{stock_landed_costs_view.xml => views/stock_landed_cost_views.xml} (100%) diff --git a/addons/stock_landed_costs/__init__.py b/addons/stock_landed_costs/__init__.py index 9bc047a6ee8..0d10f2e6549 100644 --- a/addons/stock_landed_costs/__init__.py +++ b/addons/stock_landed_costs/__init__.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import product -import stock_landed_costs -import purchase_config_settings \ No newline at end of file +import models \ No newline at end of file diff --git a/addons/stock_landed_costs/__openerp__.py b/addons/stock_landed_costs/__openerp__.py index cb682699f01..71d385e298f 100644 --- a/addons/stock_landed_costs/__openerp__.py +++ b/addons/stock_landed_costs/__openerp__.py @@ -18,10 +18,9 @@ This module allows you to easily add extra costs on pickings and decide the spli ], 'data': [ 'security/ir.model.access.csv', - 'stock_landed_costs_sequence.xml', - 'product_view.xml', - 'stock_landed_costs_view.xml', - 'stock_landed_costs_data.xml', + 'data/stock_landed_cost_data.xml', + 'views/product_views.xml', + 'views/stock_landed_cost_views.xml', ], 'test': [ '../account/test/account_minimal_test.xml', diff --git a/addons/stock_landed_costs/stock_landed_costs_data.xml b/addons/stock_landed_costs/data/stock_landed_cost_data.xml similarity index 67% rename from addons/stock_landed_costs/stock_landed_costs_data.xml rename to addons/stock_landed_costs/data/stock_landed_cost_data.xml index 1f9fceffd59..014a67285f0 100644 --- a/addons/stock_landed_costs/stock_landed_costs_data.xml +++ b/addons/stock_landed_costs/data/stock_landed_cost_data.xml @@ -1,7 +1,6 @@ - Done @@ -12,6 +11,12 @@ - + + + Stock Landed Costs + stock.landed.cost + LC/%(year)s/ + 4 + diff --git a/addons/stock_landed_costs/models/__init__.py b/addons/stock_landed_costs/models/__init__.py new file mode 100644 index 00000000000..f5edd922fe2 --- /dev/null +++ b/addons/stock_landed_costs/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import product +import stock_landed_cost +import purchase_config_settings \ No newline at end of file diff --git a/addons/stock_landed_costs/product.py b/addons/stock_landed_costs/models/product.py similarity index 100% rename from addons/stock_landed_costs/product.py rename to addons/stock_landed_costs/models/product.py diff --git a/addons/stock_landed_costs/purchase_config_settings.py b/addons/stock_landed_costs/models/purchase_config_settings.py similarity index 100% rename from addons/stock_landed_costs/purchase_config_settings.py rename to addons/stock_landed_costs/models/purchase_config_settings.py diff --git a/addons/stock_landed_costs/stock_landed_costs.py b/addons/stock_landed_costs/models/stock_landed_cost.py similarity index 100% rename from addons/stock_landed_costs/stock_landed_costs.py rename to addons/stock_landed_costs/models/stock_landed_cost.py diff --git a/addons/stock_landed_costs/stock_landed_costs_sequence.xml b/addons/stock_landed_costs/stock_landed_costs_sequence.xml deleted file mode 100644 index 91b8bb65289..00000000000 --- a/addons/stock_landed_costs/stock_landed_costs_sequence.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Stock Landed Costs - stock.landed.cost - LC/%(year)s/ - 4 - - - - diff --git a/addons/stock_landed_costs/product_view.xml b/addons/stock_landed_costs/views/product_views.xml similarity index 100% rename from addons/stock_landed_costs/product_view.xml rename to addons/stock_landed_costs/views/product_views.xml diff --git a/addons/stock_landed_costs/stock_landed_costs_view.xml b/addons/stock_landed_costs/views/stock_landed_cost_views.xml similarity index 100% rename from addons/stock_landed_costs/stock_landed_costs_view.xml rename to addons/stock_landed_costs/views/stock_landed_cost_views.xml From 2ff5d2a650995422f2c6ba637fcdc3d8f63b564e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 6 Jul 2016 16:03:45 +0200 Subject: [PATCH 04/20] [REF] stock_landed_costs: pre migration cleaning * All methods are converted into an api.multi style (using ids instead of a browse record or an id) * Two methods on stock.landed.cost model that work actually on stock.move records are moved to the stock.move model * Unnecessary parameters removed from get_valuation_lines to ease the code understanding --- .../models/stock_landed_cost.py | 209 +++++++++--------- 1 file changed, 106 insertions(+), 103 deletions(-) diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index fe793f4ed22..d750b6ac547 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -30,15 +30,13 @@ class stock_landed_cost(osv.osv): cost_to_recompute.append(line.cost_id.id) return cost_to_recompute - def get_valuation_lines(self, cr, uid, ids, picking_ids=None, context=None): - picking_obj = self.pool.get('stock.picking') + def get_valuation_lines(self, cr, uid, ids, context=None): + pickings = [picking for landed_cost in self.browse(cr, uid, ids, context=context) for picking in landed_cost.picking_ids] lines = [] - if not picking_ids: - return lines - for picking in picking_obj.browse(cr, uid, picking_ids): + for picking in pickings: 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 + # 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 @@ -48,7 +46,8 @@ class stock_landed_cost(osv.osv): total_cost += quant.cost * quant.qty vals = dict(product_id=move.product_id.id, move_id=move.id, quantity=move.product_qty, former_cost=total_cost, weight=weight, volume=volume) lines.append(vals) - if not lines: + + if not lines and pickings: raise UserError(_('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')) return lines @@ -76,96 +75,8 @@ class stock_landed_cost(osv.osv): 'date': fields.date.context_today, } - def _create_accounting_entries(self, cr, uid, line, move_id, qty_out, context=None): - product_obj = self.pool.get('product.template') - cost_product = line.cost_line_id and line.cost_line_id.product_id - if not cost_product: - return False - accounts = product_obj.browse(cr, uid, line.product_id.product_tmpl_id.id, context=context).get_product_accounts() - debit_account_id = accounts.get('stock_valuation', False) and accounts['stock_valuation'].id or False - already_out_account_id = accounts['stock_output'].id - credit_account_id = line.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id - - if not credit_account_id: - raise UserError(_('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, qty_out, already_out_account_id, context=context) - - 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): - """ - Generate the account.move.line values to track the landed cost. - Afterwards, for the goods that are already out of stock, we should create the out moves - """ - aml_obj = self.pool.get('account.move.line') - user_obj = self.pool.get('res.users') - if context is None: - context = {} - ctx = context.copy() - ctx['check_move_validity'] = False - base_line = { - 'name': line.name, - 'move_id': move_id, - 'product_id': line.product_id.id, - 'quantity': line.quantity, - } - debit_line = dict(base_line, account_id=debit_account_id) - credit_line = dict(base_line, account_id=credit_account_id) - diff = line.additional_landed_cost - if diff > 0: - debit_line['debit'] = diff - credit_line['credit'] = diff - else: - # negative cost, reverse the entry - debit_line['credit'] = -diff - credit_line['debit'] = -diff - aml_obj.create(cr, uid, debit_line, context=ctx) - aml_obj.create(cr, uid, credit_line, context=ctx) - - #Create account move lines for quants already out of stock - if qty_out > 0: - debit_line = dict(base_line, - name=(line.name + ": " + str(qty_out) + _(' already out')), - quantity=qty_out, - account_id=already_out_account_id) - credit_line = dict(base_line, - name=(line.name + ": " + str(qty_out) + _(' already out')), - quantity=qty_out, - account_id=debit_account_id) - diff = diff * qty_out / line.quantity - if diff > 0: - debit_line['debit'] = diff - credit_line['credit'] = diff - else: - # negative cost, reverse the entry - debit_line['credit'] = -diff - credit_line['debit'] = -diff - aml_obj.create(cr, uid, debit_line, context=ctx) - aml_obj.create(cr, uid, credit_line, context=ctx) - - if user_obj.browse(cr, uid, [uid], context=context).company_id.anglo_saxon_accounting: - debit_line = dict(base_line, - name=(line.name + ": " + str(qty_out) + _(' already out')), - quantity=qty_out, - account_id=credit_account_id) - credit_line = dict(base_line, - name=(line.name + ": " + str(qty_out) + _(' already out')), - quantity=qty_out, - account_id=already_out_account_id) - - if diff > 0: - debit_line['debit'] = diff - credit_line['credit'] = diff - else: - # negative cost, reverse the entry - debit_line['credit'] = -diff - credit_line['debit'] = -diff - aml_obj.create(cr, uid, debit_line, context=ctx) - aml_obj.create(cr, uid, credit_line, context=ctx) - - self.pool.get('account.move').assert_balanced(cr, uid, [move_id], context=context) - return True - - def _create_account_move(self, cr, uid, cost, context=None): + def _create_account_move(self, cr, uid, ids, context=None): + cost = self.browse(cr, uid, ids, context=context)[0] vals = { 'journal_id': cost.account_journal_id.id, 'date': cost.date, @@ -173,11 +84,12 @@ class stock_landed_cost(osv.osv): } return self.pool.get('account.move').create(cr, uid, vals, context=context) - def _check_sum(self, cr, uid, landed_cost, context=None): + def _check_sum(self, cr, uid, ids, context=None): """ Will check if each cost line its valuation lines sum to the correct amount and if the overall total amount is correct also """ + landed_cost = self.browse(cr, uid, ids, context=context)[0] costcor = {} tot = 0 for valuation_line in landed_cost.valuation_adjustment_lines: @@ -201,9 +113,9 @@ class stock_landed_cost(osv.osv): for cost in self.browse(cr, uid, ids, context=context): if cost.state != 'draft': raise UserError(_('Only draft landed costs can be validated')) - if not cost.valuation_adjustment_lines or not self._check_sum(cr, uid, cost, context=context): + if not cost.valuation_adjustment_lines or not self._check_sum(cr, uid, [cost.id], context=context): raise UserError(_('You cannot validate a landed cost which has no valid valuation adjustments lines. Did you click on Compute?')) - move_id = self._create_account_move(cr, uid, cost, context=context) + move_id = self._create_account_move(cr, uid, [cost.id], context=context) for line in cost.valuation_adjustment_lines: if not line.move_id: continue @@ -250,7 +162,7 @@ class stock_landed_cost(osv.osv): for quant in line.move_id.quant_ids: if quant.location_id.usage != 'internal': qty_out += quant.qty - self._create_accounting_entries(cr, uid, line, move_id, qty_out, context=context) + line._create_accounting_entries(move_id, qty_out) self.write(cr, uid, cost.id, {'state': 'done', 'account_move_id': move_id}, context=context) self.pool.get('account.move').post(cr, uid, [move_id], context=context) return True @@ -276,13 +188,12 @@ class stock_landed_cost(osv.osv): for cost in self.browse(cr, uid, ids, context=None): if not cost.picking_ids: continue - picking_ids = [p.id for p in cost.picking_ids] total_qty = 0.0 total_cost = 0.0 total_weight = 0.0 total_volume = 0.0 total_line = 0.0 - vals = self.get_valuation_lines(cr, uid, [cost.id], picking_ids=picking_ids, context=context) + vals = self.get_valuation_lines(cr, uid, [cost.id], context=context) for v in vals: for line in cost.cost_lines: v.update({'cost_id': cost.id, 'cost_line_id': line.id}) @@ -405,3 +316,95 @@ class stock_valuation_adjustment_lines(osv.osv): 'weight': 1.0, 'volume': 1.0, } + + def _create_accounting_entries(self, cr, uid, ids, move_id, qty_out, context=None): + # TDE CLEANME: product chosen for computation ? + line = self.browse(cr, uid, ids, context=context)[0] + product_obj = self.pool.get('product.template') + cost_product = line.cost_line_id and line.cost_line_id.product_id + if not cost_product: + return False + accounts = product_obj.browse(cr, uid, line.product_id.product_tmpl_id.id, context=context).get_product_accounts() + debit_account_id = accounts.get('stock_valuation', False) and accounts['stock_valuation'].id or False + already_out_account_id = accounts['stock_output'].id + credit_account_id = line.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id + + if not credit_account_id: + raise UserError(_('Please configure Stock Expense Account for product: %s.') % (cost_product.name)) + + return self._create_account_move_line(cr, uid, [line.id], move_id, credit_account_id, debit_account_id, qty_out, already_out_account_id, context=context) + + def _create_account_move_line(self, cr, uid, ids, move_id, credit_account_id, debit_account_id, qty_out, already_out_account_id, context=None): + """ + Generate the account.move.line values to track the landed cost. + Afterwards, for the goods that are already out of stock, we should create the out moves + """ + line = self.browse(cr, uid, ids, context=context)[0] + aml_obj = self.pool.get('account.move.line') + user_obj = self.pool.get('res.users') + if context is None: + context = {} + ctx = context.copy() + ctx['check_move_validity'] = False + base_line = { + 'name': line.name, + 'move_id': move_id, + 'product_id': line.product_id.id, + 'quantity': line.quantity, + } + debit_line = dict(base_line, account_id=debit_account_id) + credit_line = dict(base_line, account_id=credit_account_id) + diff = line.additional_landed_cost + if diff > 0: + debit_line['debit'] = diff + credit_line['credit'] = diff + else: + # negative cost, reverse the entry + debit_line['credit'] = -diff + credit_line['debit'] = -diff + aml_obj.create(cr, uid, debit_line, context=ctx) + aml_obj.create(cr, uid, credit_line, context=ctx) + + #Create account move lines for quants already out of stock + if qty_out > 0: + debit_line = dict(base_line, + name=(line.name + ": " + str(qty_out) + _(' already out')), + quantity=qty_out, + account_id=already_out_account_id) + credit_line = dict(base_line, + name=(line.name + ": " + str(qty_out) + _(' already out')), + quantity=qty_out, + account_id=debit_account_id) + diff = diff * qty_out / line.quantity + if diff > 0: + debit_line['debit'] = diff + credit_line['credit'] = diff + else: + # negative cost, reverse the entry + debit_line['credit'] = -diff + credit_line['debit'] = -diff + aml_obj.create(cr, uid, debit_line, context=ctx) + aml_obj.create(cr, uid, credit_line, context=ctx) + + if user_obj.browse(cr, uid, [uid], context=context).company_id.anglo_saxon_accounting: + debit_line = dict(base_line, + name=(line.name + ": " + str(qty_out) + _(' already out')), + quantity=qty_out, + account_id=credit_account_id) + credit_line = dict(base_line, + name=(line.name + ": " + str(qty_out) + _(' already out')), + quantity=qty_out, + account_id=already_out_account_id) + + if diff > 0: + debit_line['debit'] = diff + credit_line['credit'] = diff + else: + # negative cost, reverse the entry + debit_line['credit'] = -diff + credit_line['debit'] = -diff + aml_obj.create(cr, uid, debit_line, context=ctx) + aml_obj.create(cr, uid, credit_line, context=ctx) + + self.pool.get('account.move').assert_balanced(cr, uid, [move_id], context=context) + return True From 904f14120c918e1e57278ac023b0633a11b51aed Mon Sep 17 00:00:00 2001 From: Jainik Patel Date: Wed, 6 Jul 2016 14:52:02 +0200 Subject: [PATCH 05/20] [MIG] stock_landed_costs: new API --- .../data/stock_landed_cost_data.xml | 4 +- addons/stock_landed_costs/models/product.py | 26 +- .../models/purchase_config_settings.py | 8 +- .../models/stock_landed_cost.py | 483 +++++++++--------- .../views/product_views.xml | 6 +- .../views/stock_landed_cost_views.xml | 22 +- 6 files changed, 261 insertions(+), 288 deletions(-) diff --git a/addons/stock_landed_costs/data/stock_landed_cost_data.xml b/addons/stock_landed_costs/data/stock_landed_cost_data.xml index 014a67285f0..a4462dc7771 100644 --- a/addons/stock_landed_costs/data/stock_landed_cost_data.xml +++ b/addons/stock_landed_costs/data/stock_landed_cost_data.xml @@ -1,5 +1,5 @@ - + @@ -19,4 +19,4 @@ 4 - + diff --git a/addons/stock_landed_costs/models/product.py b/addons/stock_landed_costs/models/product.py index d5b4d378e50..ca336cc7c4f 100644 --- a/addons/stock_landed_costs/models/product.py +++ b/addons/stock_landed_costs/models/product.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv +from odoo import fields, models SPLIT_METHOD = [ ('equal', 'Equal'), @@ -11,19 +11,15 @@ SPLIT_METHOD = [ ('by_volume', 'By Volume'), ] -class product_template(osv.osv): + +class ProductTemplate(models.Model): _inherit = "product.template" - _columns = { - 'landed_cost_ok': fields.boolean('Landed Costs'), - 'split_method': fields.selection(SPLIT_METHOD, 'Split Method', help="Equal : Cost will be equally divided.\n"\ - "By Quantity : Cost will be divided according to product's quantity.\n"\ - "By Current cost : Cost will be divided according to product's current cost.\n"\ - "By Weight : Cost will be divided depending on its weight.\n"\ - "By Volume : Cost will be divided depending on its volume."), - } - - _defaults = { - 'landed_cost_ok': False, - 'split_method': 'equal', - } + landed_cost_ok = fields.Boolean('Landed Costs') + split_method = fields.Selection( + selection=SPLIT_METHOD, string='Split Method', default='equal', + help="Equal : Cost will be equally divided.\n" + "By Quantity : Cost will be divided according to product's quantity.\n" + "By Current cost : Cost will be divided according to product's current cost.\n" + "By Weight : Cost will be divided depending on its weight.\n" + "By Volume : Cost will be divided depending on its volume.") diff --git a/addons/stock_landed_costs/models/purchase_config_settings.py b/addons/stock_landed_costs/models/purchase_config_settings.py index ab3dc0c562b..1809bf865d6 100644 --- a/addons/stock_landed_costs/models/purchase_config_settings.py +++ b/addons/stock_landed_costs/models/purchase_config_settings.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import api, models -from openerp.tools.translate import _ +from odoo import api, models, _ class PurchaseConfigSettings(models.TransientModel): @@ -12,13 +11,10 @@ class PurchaseConfigSettings(models.TransientModel): @api.onchange('group_costing_method') def onchange_costing_method(self): if self.group_costing_method == 0: + self.group_costing_method = 1 return { 'warning': { 'title': _('Warning!'), 'message': _('Disabling the costing methods will prevent you to use the landed costs feature.'), - }, - 'value': { - 'group_costing_method': 1 } } - return {} diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index d750b6ac547..e40a295620b 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -1,124 +1,90 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -import openerp.addons.decimal_precision as dp -from openerp.tools import float_compare, float_round -from openerp.tools.translate import _ -import product -from openerp import SUPERUSER_ID -from openerp.exceptions import UserError +from collections import defaultdict + +from odoo import api, fields, models, tools, _ +from odoo.addons import decimal_precision as dp +from odoo.addons.stock_landed_costs.models import product +from odoo.exceptions import UserError -class stock_landed_cost(osv.osv): +class LandedCost(models.Model): _name = 'stock.landed.cost' _description = 'Stock Landed Cost' _inherit = 'mail.thread' - 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 + name = fields.Char( + 'Name', default=lambda self: self.env['ir.sequence'].next_by_code('stock.landed.cost'), + copy=False, readonly=True, track_visibility='always') + date = fields.Date( + 'Date', default=fields.Date.context_today, + copy=False, required=True, states={'done': [('readonly', True)]}, track_visibility='onchange') + picking_ids = fields.Many2many( + 'stock.picking', string='Pickings', + copy=False, states={'done': [('readonly', True)]}) + cost_lines = fields.One2many( + 'stock.landed.cost.lines', 'cost_id', 'Cost Lines', + copy=True, 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.Float( + 'Total', compute='_compute_total_amount', + digits=0, store=True, track_visibility='always') + state = fields.Selection([ + ('draft', 'Draft'), + ('done', 'Posted'), + ('cancel', 'Cancelled')], 'State', default='draft', + copy=False, readonly=True, track_visibility='onchange') + account_move_id = fields.Many2one( + 'account.move', 'Journal Entry', + copy=False, readonly=True) + account_journal_id = fields.Many2one( + 'account.journal', 'Account Journal', + required=True, states={'done': [('readonly', True)]}) - 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 + @api.one + @api.depends('cost_lines.price_unit') + def _compute_total_amount(self): + self.amount_total = sum(line.price_unit for line in self.cost_lines) - def get_valuation_lines(self, cr, uid, ids, context=None): - pickings = [picking for landed_cost in self.browse(cr, uid, ids, context=context) for picking in landed_cost.picking_ids] - lines = [] + @api.multi + def unlink(self): + self.button_cancel() + return super(LandedCost, self).unlink() - for picking in pickings: - 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 - 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 * quant.qty - vals = dict(product_id=move.product_id.id, move_id=move.id, quantity=move.product_qty, former_cost=total_cost, weight=weight, volume=volume) - lines.append(vals) + @api.multi + def _track_subtype(self, init_values): + if 'state' in init_values and self.state == 'done': + return 'stock_landed_costs.mt_stock_landed_cost_open' + return super(LandedCost, self)._track_subtype(init_values) - if not lines and pickings: - raise UserError(_('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')) - return lines + @api.multi + def button_cancel(self): + if any(cost.state == 'done' for cost in self): + raise UserError( + _('Validated landed costs cannot be cancelled, but you could create negative landed costs to reverse them')) + return self.write({'state': 'cancel'}) - _columns = { - 'name': fields.char('Name', track_visibility='always', readonly=True, copy=False), - 'date': fields.date('Date', required=True, states={'done': [('readonly', True)]}, track_visibility='onchange', copy=False), - 'picking_ids': fields.many2many('stock.picking', string='Pickings', states={'done': [('readonly', True)]}, copy=False), - 'cost_lines': fields.one2many('stock.landed.cost.lines', 'cost_id', 'Cost Lines', states={'done': [('readonly', True)]}, copy=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=0, - 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', copy=False), - 'account_move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, copy=False), - 'account_journal_id': fields.many2one('account.journal', 'Account Journal', required=True, states={'done': [('readonly', True)]}), - } + @api.multi + def button_validate(self): + if any(cost.state != 'draft' for cost in self): + raise UserError(_('Only draft landed costs can be validated')) + if any(not cost.valuation_adjustment_lines for cost in self): + raise UserError(_('No valuation adjustments lines. You should maybe recompute the landed costs.')) + if not self._check_sum(): + raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.')) - _defaults = { - 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(cr, uid, 'stock.landed.cost'), - 'state': 'draft', - 'date': fields.date.context_today, - } - - def _create_account_move(self, cr, uid, ids, context=None): - cost = self.browse(cr, uid, ids, context=context)[0] - vals = { - 'journal_id': cost.account_journal_id.id, - 'date': cost.date, - 'ref': cost.name - } - return self.pool.get('account.move').create(cr, uid, vals, context=context) - - def _check_sum(self, cr, uid, ids, context=None): - """ - Will check if each cost line its valuation lines sum to the correct amount - and if the overall total amount is correct also - """ - landed_cost = self.browse(cr, uid, ids, context=context)[0] - costcor = {} - tot = 0 - for valuation_line in landed_cost.valuation_adjustment_lines: - if costcor.get(valuation_line.cost_line_id): - costcor[valuation_line.cost_line_id] += valuation_line.additional_landed_cost - else: - costcor[valuation_line.cost_line_id] = valuation_line.additional_landed_cost - tot += valuation_line.additional_landed_cost - - prec = self.pool['decimal.precision'].precision_get(cr, uid, 'Account') - # float_compare returns 0 for equal amounts - res = not bool(float_compare(tot, landed_cost.amount_total, precision_digits=prec)) - for costl in costcor.keys(): - if float_compare(costcor[costl], costl.price_unit, precision_digits=prec): - res = False - return res - - 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 cost.state != 'draft': - raise UserError(_('Only draft landed costs can be validated')) - if not cost.valuation_adjustment_lines or not self._check_sum(cr, uid, [cost.id], context=context): - raise UserError(_('You cannot validate a landed cost which has no valid valuation adjustments lines. Did you click on Compute?')) - move_id = self._create_account_move(cr, uid, [cost.id], context=context) - for line in cost.valuation_adjustment_lines: - if not line.move_id: - continue + for cost in self: + move = self.env['account.move'].create({ + 'journal_id': cost.account_journal_id.id, + 'date': cost.date, + 'ref': cost.name + }) + for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id): per_unit = line.final_cost / line.quantity diff = per_unit - line.former_cost_per_unit @@ -133,16 +99,16 @@ class stock_landed_cost(osv.osv): # an arbitrary minimum quantity set to 2.0 from which we consider we can extract a # unit and adapt the cost. curr_rounding = line.move_id.company_id.currency_id.rounding - diff_rounded = float_round(diff, precision_rounding=curr_rounding) + diff_rounded = tools.float_round(diff, precision_rounding=curr_rounding) diff_correct = diff_rounded quants = line.move_id.quant_ids.sorted(key=lambda r: r.qty, reverse=True) quant_correct = False if quants\ - and float_compare(quants[0].product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\ - and float_compare(line.quantity * diff, line.quantity * diff_rounded, precision_rounding=curr_rounding) != 0\ - and float_compare(quants[0].qty, 2.0, precision_rounding=quants[0].product_id.uom_id.rounding) >= 0: + and tools.float_compare(quants[0].product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\ + and tools.float_compare(line.quantity * diff, line.quantity * diff_rounded, precision_rounding=curr_rounding) != 0\ + and tools.float_compare(quants[0].qty, 2.0, precision_rounding=quants[0].product_id.uom_id.rounding) >= 0: # Search for existing quant of quantity = 1.0 to avoid creating a new one - quant_correct = quants.filtered(lambda r: float_compare(r.qty, 1.0, precision_rounding=quants[0].product_id.uom_id.rounding) == 0) + quant_correct = quants.filtered(lambda r: tools.float_compare(r.qty, 1.0, precision_rounding=quants[0].product_id.uom_id.rounding) == 0) if not quant_correct: quant_correct = quants[0]._quant_split(quants[0].qty - 1.0) else: @@ -153,55 +119,80 @@ class stock_landed_cost(osv.osv): quant_dict = {} for quant in quants: - quant_dict[quant.id] = quant.cost + diff + quant_dict[quant] = quant.cost + diff if quant_correct: - quant_dict[quant_correct.id] = quant_correct.cost + diff_correct - for key, value in quant_dict.items(): - quant_obj.write(cr, SUPERUSER_ID, key, {'cost': value}, context=context) + quant_dict[quant_correct] = quant_correct.cost + diff_correct + for quant, value in quant_dict.items(): + quant.sudo().write({'cost': value}) qty_out = 0 for quant in line.move_id.quant_ids: if quant.location_id.usage != 'internal': qty_out += quant.qty - line._create_accounting_entries(move_id, qty_out) - self.write(cr, uid, cost.id, {'state': 'done', 'account_move_id': move_id}, context=context) - self.pool.get('account.move').post(cr, uid, [move_id], context=context) + line._create_accounting_entries(move, qty_out) + cost.write({'state': 'done', 'account_move_id': move.id}) + move.post() return True - def button_cancel(self, cr, uid, ids, context=None): - cost = self.browse(cr, uid, ids, context=context) - if cost.state == 'done': - raise UserError(_('Validated landed costs cannot be cancelled, ' - 'but you could create negative landed costs to reverse them')) - return cost.write({'state': 'cancel'}) + def _check_sum(self): + """ Check if each cost line its valuation lines sum to the correct amount + and if the overall total amount is correct also """ + prec_digits = self.env['decimal.precision'].precision_get('Account') + for landed_cost in self: + total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost')) + if not tools.float_compare(total_amount, landed_cost.amount_total, precision_digits=prec_digits) == 0: + return False - def unlink(self, cr, uid, ids, context=None): - # cancel or raise first - self.button_cancel(cr, uid, ids, context) - return super(stock_landed_cost, self).unlink(cr, uid, ids, context=context) + val_to_cost_lines = defaultdict(lambda: 0.0) + for val_line in landed_cost.valuation_adjustment_lines: + val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost + if any(tools.float_compare(cost_line.price_unit, val_amount, precision_digits=prec_digits) != 0 + for cost_line, val_amount in val_to_cost_lines.iteritems()): + return False + return True - def compute_landed_cost(self, cr, uid, ids, context=None): - line_obj = self.pool.get('stock.valuation.adjustment.lines') - unlink_ids = line_obj.search(cr, uid, [('cost_id', 'in', ids)], context=context) - line_obj.unlink(cr, uid, unlink_ids, context=context) - digits = dp.get_precision('Product Price')(cr) - towrite_dict = {} - for cost in self.browse(cr, uid, ids, context=None): - if not cost.picking_ids: + def get_valuation_lines(self): + lines = [] + + for move in self.mapped('picking_ids').mapped('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 + vals = { + 'product_id': move.product_id.id, + 'move_id': move.id, + 'quantity': move.product_qty, + 'former_cost': sum(quant.cost * quant.qty for quant in move.quant_ids), + 'weight': move.product_id.weight * move.product_qty, + 'volume': move.product_id.volume * move.product_qty + } + lines.append(vals) + + if not lines and self.mapped('picking_ids'): + raise UserError(_('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')) + return lines + + @api.multi + def compute_landed_cost(self): + AdjustementLines = self.env['stock.valuation.adjustment.lines'] + AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink() + + digits = dp.get_precision('Product Price')(self._cr) + towrite_dict = {} + for cost in self.filtered(lambda cost: cost.picking_ids): total_qty = 0.0 total_cost = 0.0 total_weight = 0.0 total_volume = 0.0 total_line = 0.0 - vals = self.get_valuation_lines(cr, uid, [cost.id], context=context) - for v in vals: - for line in cost.cost_lines: - v.update({'cost_id': cost.id, 'cost_line_id': line.id}) - self.pool.get('stock.valuation.adjustment.lines').create(cr, uid, v, context=context) - total_qty += v.get('quantity', 0.0) - total_cost += v.get('former_cost', 0.0) - total_weight += v.get('weight', 0.0) - total_volume += v.get('volume', 0.0) + all_val_line_values = cost.get_valuation_lines() + for val_line_values in all_val_line_values: + for cost_line in cost.cost_lines: + val_line_values.update({'cost_id': cost.id, 'cost_line_id': cost_line.id}) + self.env['stock.valuation.adjustment.lines'].create(val_line_values) + total_qty += val_line_values.get('quantity', 0.0) + total_cost += val_line_values.get('former_cost', 0.0) + total_weight += val_line_values.get('weight', 0.0) + total_volume += val_line_values.get('volume', 0.0) total_line += 1 for line in cost.cost_lines: @@ -227,7 +218,7 @@ class stock_landed_cost(osv.osv): value = (line.price_unit / total_line) if digits: - value = float_round(value, precision_digits=digits[1], rounding_method='UP') + value = tools.float_round(value, precision_digits=digits[1], rounding_method='UP') fnc = min if line.price_unit > 0 else max value = fnc(value, line.price_unit - value_split) value_split += value @@ -238,123 +229,116 @@ class stock_landed_cost(osv.osv): towrite_dict[valuation.id] += value if towrite_dict: for key, value in towrite_dict.items(): - line_obj.write(cr, uid, key, {'additional_landed_cost': value}, context=context) + AdjustementLines.browse(key).write({'additional_landed_cost': value}) return True - def _track_subtype(self, cr, uid, ids, init_values, context=None): - record = self.browse(cr, uid, ids[0], context=context) - if 'state' in init_values and record.state == 'done': - return 'stock_landed_costs.mt_stock_landed_cost_open' - return super(stock_landed_cost, self)._track_subtype(cr, uid, ids, init_values, context=context) - -class stock_landed_cost_lines(osv.osv): +class LandedCostLine(models.Model): _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}} + name = fields.Char('Description') + 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('Cost', digits_compute=dp.get_precision('Product Price'), required=True) + split_method = fields.Selection(product.SPLIT_METHOD, string='Split Method', required=True) + account_id = fields.Many2one( + 'account.account', 'Account', + domain=[('internal_type', '!=', 'view'), ('internal_type', '!=', 'closed'), ('deprecated', '=', False)]) - 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_id and product.property_account_expense_id.id or product.categ_id.property_account_expense_categ_id.id - return {'value': result} + @api.onchange('product_id') + def onchange_product_id(self): + if not self.product_id: + self.quantity = 0.0 + self.name = self.product_id.name or '' + self.split_method = self.product_id.split_method or 'equal' + self.price_unit = self.product_id.standard_price or 0.0 + self.account_id = self.product_id.property_account_expense_id.id or self.product_id.categ_id.property_account_expense_categ_id.id - _columns = { - 'name': fields.char('Description'), - '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('Cost', 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=[('internal_type', '!=', 'view'), ('internal_type', '!=', 'closed'), ('deprecated', '=', False)]), - } -class stock_valuation_adjustment_lines(osv.osv): +class AdjustmentLines(models.Model): _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 + name = fields.Char( + 'Description', compute='_compute_name', store=True) + cost_id = fields.Many2one( + 'stock.landed.cost', 'Landed Cost', + ondelete='cascade', required=True) + 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', default=1.0, + digits_compute=dp.get_precision('Product Unit of Measure'), required=True) + weight = fields.Float( + 'Weight', default=1.0, + digits_compute=dp.get_precision('Product Unit of Measure')) + volume = fields.Float( + 'Volume', default=1.0, + 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.Float( + 'Former Cost(Per Unit)', compute='_compute_former_cost_per_unit', + digits=0, store=True) + additional_landed_cost = fields.Float( + 'Additional Landed Cost', + digits_compute=dp.get_precision('Product Price')) + final_cost = fields.Float( + 'Final Cost', compute='_compute_final_cost', + digits=0, store=True) - 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 + @api.one + @api.depends('cost_line_id.name', 'product_id.code', 'product_id.name') + def _compute_name(self): + name = '%s - ' % (self.cost_line_id.name if self.cost_line_id else '') + self.name = name + (self.product_id.code or self.product_id.name or '') - _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', store=True, digits=0), - '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', store=True, digits=0), - } + @api.one + @api.depends('former_cost', 'quantity') + def _compute_former_cost_per_unit(self): + self.former_cost_per_unit = self.former_cost / (self.quantity or 1.0) - _defaults = { - 'quantity': 1.0, - 'weight': 1.0, - 'volume': 1.0, - } + @api.one + @api.depends('former_cost', 'additional_landed_cost') + def _compute_final_cost(self): + self.final_cost = self.former_cost + self.additional_landed_cost - def _create_accounting_entries(self, cr, uid, ids, move_id, qty_out, context=None): + def _create_accounting_entries(self, move, qty_out): # TDE CLEANME: product chosen for computation ? - line = self.browse(cr, uid, ids, context=context)[0] - product_obj = self.pool.get('product.template') - cost_product = line.cost_line_id and line.cost_line_id.product_id + cost_product = self.cost_line_id.product_id if not cost_product: return False - accounts = product_obj.browse(cr, uid, line.product_id.product_tmpl_id.id, context=context).get_product_accounts() - debit_account_id = accounts.get('stock_valuation', False) and accounts['stock_valuation'].id or False + accounts = self.product_id.product_tmpl_id.get_product_accounts() + debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False already_out_account_id = accounts['stock_output'].id - credit_account_id = line.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id + credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id if not credit_account_id: raise UserError(_('Please configure Stock Expense Account for product: %s.') % (cost_product.name)) - return self._create_account_move_line(cr, uid, [line.id], move_id, credit_account_id, debit_account_id, qty_out, already_out_account_id, context=context) + return self._create_account_move_line(move, credit_account_id, debit_account_id, qty_out, already_out_account_id) - def _create_account_move_line(self, cr, uid, ids, move_id, credit_account_id, debit_account_id, qty_out, already_out_account_id, context=None): + def _create_account_move_line(self, move, credit_account_id, debit_account_id, qty_out, already_out_account_id): """ Generate the account.move.line values to track the landed cost. Afterwards, for the goods that are already out of stock, we should create the out moves """ - line = self.browse(cr, uid, ids, context=context)[0] - aml_obj = self.pool.get('account.move.line') - user_obj = self.pool.get('res.users') - if context is None: - context = {} - ctx = context.copy() - ctx['check_move_validity'] = False + AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False) + base_line = { - 'name': line.name, - 'move_id': move_id, - 'product_id': line.product_id.id, - 'quantity': line.quantity, + 'name': self.name, + 'move_id': move.id, + 'product_id': self.product_id.id, + 'quantity': self.quantity, } debit_line = dict(base_line, account_id=debit_account_id) credit_line = dict(base_line, account_id=credit_account_id) - diff = line.additional_landed_cost + diff = self.additional_landed_cost if diff > 0: debit_line['debit'] = diff credit_line['credit'] = diff @@ -362,20 +346,20 @@ class stock_valuation_adjustment_lines(osv.osv): # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff - aml_obj.create(cr, uid, debit_line, context=ctx) - aml_obj.create(cr, uid, credit_line, context=ctx) - - #Create account move lines for quants already out of stock + AccountMoveLine.create(debit_line) + AccountMoveLine.create(credit_line) + + # Create account move lines for quants already out of stock if qty_out > 0: debit_line = dict(base_line, - name=(line.name + ": " + str(qty_out) + _(' already out')), + name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=qty_out, account_id=already_out_account_id) credit_line = dict(base_line, - name=(line.name + ": " + str(qty_out) + _(' already out')), - quantity=qty_out, - account_id=debit_account_id) - diff = diff * qty_out / line.quantity + name=(self.name + ": " + str(qty_out) + _(' already out')), + quantity=qty_out, + account_id=debit_account_id) + diff = diff * qty_out / self.quantity if diff > 0: debit_line['debit'] = diff credit_line['credit'] = diff @@ -383,18 +367,19 @@ class stock_valuation_adjustment_lines(osv.osv): # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff - aml_obj.create(cr, uid, debit_line, context=ctx) - aml_obj.create(cr, uid, credit_line, context=ctx) + AccountMoveLine.create(debit_line) + AccountMoveLine.create(credit_line) - if user_obj.browse(cr, uid, [uid], context=context).company_id.anglo_saxon_accounting: + # TDE FIXME: oh dear + if self.env.user.company_id.anglo_saxon_accounting: debit_line = dict(base_line, - name=(line.name + ": " + str(qty_out) + _(' already out')), + name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=qty_out, account_id=credit_account_id) credit_line = dict(base_line, - name=(line.name + ": " + str(qty_out) + _(' already out')), - quantity=qty_out, - account_id=already_out_account_id) + name=(self.name + ": " + str(qty_out) + _(' already out')), + quantity=qty_out, + account_id=already_out_account_id) if diff > 0: debit_line['debit'] = diff @@ -403,8 +388,8 @@ class stock_valuation_adjustment_lines(osv.osv): # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff - aml_obj.create(cr, uid, debit_line, context=ctx) - aml_obj.create(cr, uid, credit_line, context=ctx) + AccountMoveLine.create(debit_line) + AccountMoveLine.create(credit_line) - self.pool.get('account.move').assert_balanced(cr, uid, [move_id], context=context) + move.assert_balanced() return True diff --git a/addons/stock_landed_costs/views/product_views.xml b/addons/stock_landed_costs/views/product_views.xml index c63ea9de942..0749add5b6d 100644 --- a/addons/stock_landed_costs/views/product_views.xml +++ b/addons/stock_landed_costs/views/product_views.xml @@ -1,7 +1,6 @@ - + - product.template.landed.cost.form product.template @@ -15,6 +14,5 @@ - - + diff --git a/addons/stock_landed_costs/views/stock_landed_cost_views.xml b/addons/stock_landed_costs/views/stock_landed_cost_views.xml index 4958f575c7c..50bbc957cb7 100644 --- a/addons/stock_landed_costs/views/stock_landed_cost_views.xml +++ b/addons/stock_landed_costs/views/stock_landed_cost_views.xml @@ -1,8 +1,7 @@ - + - - + stock.landed.cost.form stock.landed.cost @@ -40,7 +39,8 @@ + domain="[('landed_cost_ok', '=', True)]" + context="{'default_landed_cost_ok': True, 'form_view_ref':'stock_landed_costs.view_stock_landed_cost_type_form'}"/> @@ -52,7 +52,9 @@ - + @@ -99,7 +101,6 @@ - stock.landed.cost.tree stock.landed.cost @@ -112,7 +113,6 @@ - stock.landed.cost.search stock.landed.cost @@ -131,7 +131,6 @@ - Landed Costs stock.landed.cost @@ -145,10 +144,9 @@ - - + stock.landed.cost.type.form product.product @@ -200,7 +198,6 @@ - product.product.tree product.product @@ -212,6 +209,7 @@ + Products ir.actions.act_window @@ -245,4 +243,4 @@ - + From 9a84193f9d64f2fb2a6ade7ed3dfa5760662c004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 7 Jul 2016 09:49:18 +0200 Subject: [PATCH 06/20] [MOV] stock_dropshipping: reorganize module --- addons/stock_dropshipping/__openerp__.py | 2 +- .../{stock_dropshipping.xml => data/stock_data.xml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename addons/stock_dropshipping/{stock_dropshipping.xml => data/stock_data.xml} (100%) diff --git a/addons/stock_dropshipping/__openerp__.py b/addons/stock_dropshipping/__openerp__.py index 42835bc8ea2..b032ab6feef 100644 --- a/addons/stock_dropshipping/__openerp__.py +++ b/addons/stock_dropshipping/__openerp__.py @@ -22,7 +22,7 @@ internal transfer document is needed. """, 'website': 'https://www.odoo.com/page/warehouse', 'depends': ['purchase', 'sale_stock'], - 'data': ['stock_dropshipping.xml'], + 'data': ['data/stock_data.xml'], 'test': [ '../account/test/account_minimal_test.xml', '../stock_account/test/stock_valuation_account.xml', diff --git a/addons/stock_dropshipping/stock_dropshipping.xml b/addons/stock_dropshipping/data/stock_data.xml similarity index 100% rename from addons/stock_dropshipping/stock_dropshipping.xml rename to addons/stock_dropshipping/data/stock_data.xml From 7a15c6fe8f47595506f24f6dcffaadfd8d03d7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 7 Jul 2016 09:51:31 +0200 Subject: [PATCH 07/20] [CLN] stock_dropshipping: cleaning and linting --- addons/stock_dropshipping/data/stock_data.xml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/addons/stock_dropshipping/data/stock_data.xml b/addons/stock_dropshipping/data/stock_data.xml index 7637e4fe260..5580c4ec74c 100644 --- a/addons/stock_dropshipping/data/stock_data.xml +++ b/addons/stock_dropshipping/data/stock_data.xml @@ -1,16 +1,13 @@ - + - - Dropship DS 5 - - + Dropship @@ -19,8 +16,7 @@ - - + Drop Shipping @@ -40,4 +36,4 @@ - + From 8f1e11ab527ab17b46c1c74fcb4b5efcaeb0b45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 7 Jul 2016 15:49:11 +0200 Subject: [PATCH 08/20] [CLN] stock_picking_wave: cleaning and linting --- addons/stock_picking_wave/__openerp__.py | 20 +++++++++---------- .../data/ir_sequence_data_picking_wave.xml | 13 ------------ .../data/stock_picking_wave_data.xml | 8 ++++++++ .../models/stock_picking_wave.py | 12 +++++------ .../wizard/stock_picking_to_wave.py | 2 +- 5 files changed, 25 insertions(+), 30 deletions(-) delete mode 100644 addons/stock_picking_wave/data/ir_sequence_data_picking_wave.xml diff --git a/addons/stock_picking_wave/__openerp__.py b/addons/stock_picking_wave/__openerp__.py index 5ff2aed752e..4401f5dc6d6 100644 --- a/addons/stock_picking_wave/__openerp__.py +++ b/addons/stock_picking_wave/__openerp__.py @@ -6,19 +6,19 @@ 'version': '1.0', 'category': 'Warehouse', 'description': """ -This module adds the picking wave option in warehouse management. -================================================================= +This module adds the picking wave option in warehouse management +================================================================ """, 'website': 'https://www.odoo.com/page/warehouse', 'depends': ['stock'], - 'data': ['security/ir.model.access.csv', - 'views/stock_picking_wave_views.xml', - 'data/stock_picking_wave_data.xml', - 'data/ir_sequence_data_picking_wave.xml', - 'wizard/stock_picking_to_wave_views.xml', - ], + 'data': [ + 'security/ir.model.access.csv', + 'views/stock_picking_wave_views.xml', + 'data/stock_picking_wave_data.xml', + 'wizard/stock_picking_to_wave_views.xml', + ], 'demo': [ - 'data/stock_picking_wave_demo.xml', - ], + 'data/stock_picking_wave_demo.xml', + ], 'installable': True, } diff --git a/addons/stock_picking_wave/data/ir_sequence_data_picking_wave.xml b/addons/stock_picking_wave/data/ir_sequence_data_picking_wave.xml deleted file mode 100644 index f80128bbfb0..00000000000 --- a/addons/stock_picking_wave/data/ir_sequence_data_picking_wave.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Picking Wave - picking.wave - Wave/ - 5 - - - - diff --git a/addons/stock_picking_wave/data/stock_picking_wave_data.xml b/addons/stock_picking_wave/data/stock_picking_wave_data.xml index 2927713f6b9..bfc677faff6 100644 --- a/addons/stock_picking_wave/data/stock_picking_wave_data.xml +++ b/addons/stock_picking_wave/data/stock_picking_wave_data.xml @@ -7,4 +7,12 @@ Stage Changed + + + Picking Wave + picking.wave + Wave/ + 5 + + diff --git a/addons/stock_picking_wave/models/stock_picking_wave.py b/addons/stock_picking_wave/models/stock_picking_wave.py index d4704fa2762..f565bafa809 100644 --- a/addons/stock_picking_wave/models/stock_picking_wave.py +++ b/addons/stock_picking_wave/models/stock_picking_wave.py @@ -28,6 +28,12 @@ class StockPickingWave(models.Model): ('cancel', 'Cancelled')], default='draft', copy=False, track_visibility='onchange', required=True) + @api.model + def create(self, vals): + if vals.get('name', '/') == '/': + vals['name'] = self.env['ir.sequence'].next_by_code('picking.wave') or '/' + return super(StockPickingWave, self).create(vals) + @api.multi def confirm_picking(self): pickings_todo = self.mapped('picking_ids') @@ -46,12 +52,6 @@ class StockPickingWave(models.Model): raise UserError(_('Nothing to print.')) return self.env["report"].with_context(active_ids=pickings.ids, active_model='stock.picking').get_action(pickings, 'stock.report_picking') - @api.model - def create(self, vals): - if vals.get('name', '/') == '/': - vals['name'] = self.env['ir.sequence'].next_by_code('picking.wave') or '/' - return super(StockPickingWave, self).create(vals) - @api.multi def done(self): pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done')) diff --git a/addons/stock_picking_wave/wizard/stock_picking_to_wave.py b/addons/stock_picking_wave/wizard/stock_picking_to_wave.py index eb7c8d2305e..69b53578018 100644 --- a/addons/stock_picking_wave/wizard/stock_picking_to_wave.py +++ b/addons/stock_picking_wave/wizard/stock_picking_to_wave.py @@ -12,7 +12,7 @@ class StockPickingToWave(models.TransientModel): @api.multi def attach_pickings(self): - #use active_ids to add picking line to the selected wave + # use active_ids to add picking line to the selected wave self.ensure_one() picking_ids = self.env.context.get('active_ids') return self.env['stock.picking'].browse(picking_ids).write({'wave_id': self.wave_id.id}) From 6a824e4a5ef08125183060532393ff89cc63dc62 Mon Sep 17 00:00:00 2001 From: mdi-odoo Date: Thu, 26 May 2016 17:26:32 +0530 Subject: [PATCH 09/20] [MOV] stock_account: reorganize module --- addons/stock_account/__init__.py | 5 +- addons/stock_account/__openerp__.py | 19 +-- .../stock_account/{ => data}/product_data.xml | 0 .../{ => data}/stock_account_data.xml | 0 .../stock_account_data_post_install.yml} | 0 .../{ => data}/stock_account_demo.yml | 0 addons/stock_account/models/__init__.py | 8 + .../models/account_chart_template.py | 65 +++++++++ .../stock_account/models/account_invoice.py | 90 ++++++++++++ addons/stock_account/{ => models}/product.py | 0 .../{stock_account.py => models/stock.py} | 137 +----------------- .../stock_config_settings.py} | 0 addons/stock_account/report/__init__.py | 4 + .../stock_history.py} | 32 ---- .../stock_history_views.xml} | 32 ---- .../product_views.xml} | 0 .../stock_account_views.xml} | 0 .../stock_config_settings_views.xml} | 0 addons/stock_account/wizard/__init__.py | 2 +- ... => stock_change_standard_price_views.xml} | 0 .../wizard/wizard_valuation_history.py | 35 +++++ .../wizard/wizard_valuation_history_views.xml | 34 +++++ 22 files changed, 251 insertions(+), 212 deletions(-) rename addons/stock_account/{ => data}/product_data.xml (100%) rename addons/stock_account/{ => data}/stock_account_data.xml (100%) rename addons/stock_account/{stock_account_post_install.yml => data/stock_account_data_post_install.yml} (100%) rename addons/stock_account/{ => data}/stock_account_demo.yml (100%) create mode 100644 addons/stock_account/models/__init__.py create mode 100644 addons/stock_account/models/account_chart_template.py create mode 100644 addons/stock_account/models/account_invoice.py rename addons/stock_account/{ => models}/product.py (100%) rename addons/stock_account/{stock_account.py => models/stock.py} (79%) rename addons/stock_account/{res_config.py => models/stock_config_settings.py} (100%) create mode 100644 addons/stock_account/report/__init__.py rename addons/stock_account/{wizard/stock_valuation_history.py => report/stock_history.py} (89%) rename addons/stock_account/{wizard/stock_valuation_history_view.xml => report/stock_history_views.xml} (63%) rename addons/stock_account/{product_view.xml => views/product_views.xml} (100%) rename addons/stock_account/{stock_account_view.xml => views/stock_account_views.xml} (100%) rename addons/stock_account/{res_config_view.xml => views/stock_config_settings_views.xml} (100%) rename addons/stock_account/wizard/{stock_change_standard_price_view.xml => stock_change_standard_price_views.xml} (100%) create mode 100644 addons/stock_account/wizard/wizard_valuation_history.py create mode 100644 addons/stock_account/wizard/wizard_valuation_history_views.xml diff --git a/addons/stock_account/__init__.py b/addons/stock_account/__init__.py index dc44558835c..d84fb182e8f 100644 --- a/addons/stock_account/__init__.py +++ b/addons/stock_account/__init__.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import product -import stock_account +import models +import report import wizard -import res_config diff --git a/addons/stock_account/__openerp__.py b/addons/stock_account/__openerp__.py index a48ea757b6c..438adc04385 100644 --- a/addons/stock_account/__openerp__.py +++ b/addons/stock_account/__openerp__.py @@ -24,19 +24,20 @@ Dashboard / Reports for Warehouse Management includes: 'category': 'Hidden', 'sequence': 16, 'demo': [ - 'stock_account_demo.yml' + 'data/stock_account_demo.yml' ], 'data': [ 'security/stock_account_security.xml', 'security/ir.model.access.csv', - 'stock_account_data.xml', - 'wizard/stock_change_standard_price_view.xml', - 'wizard/stock_valuation_history_view.xml', - 'product_data.xml', - 'product_view.xml', - 'stock_account_view.xml', - 'res_config_view.xml', - 'stock_account_post_install.yml', + 'wizard/stock_change_standard_price_views.xml', + 'wizard/wizard_valuation_history_views.xml', + 'views/product_views.xml', + 'views/stock_account_views.xml', + 'views/stock_config_settings_views.xml', + 'report/stock_history_views.xml', + 'data/product_data.xml', + 'data/stock_account_data.xml', + 'data/stock_account_data_post_install.yml', ], 'test': [ diff --git a/addons/stock_account/product_data.xml b/addons/stock_account/data/product_data.xml similarity index 100% rename from addons/stock_account/product_data.xml rename to addons/stock_account/data/product_data.xml diff --git a/addons/stock_account/stock_account_data.xml b/addons/stock_account/data/stock_account_data.xml similarity index 100% rename from addons/stock_account/stock_account_data.xml rename to addons/stock_account/data/stock_account_data.xml diff --git a/addons/stock_account/stock_account_post_install.yml b/addons/stock_account/data/stock_account_data_post_install.yml similarity index 100% rename from addons/stock_account/stock_account_post_install.yml rename to addons/stock_account/data/stock_account_data_post_install.yml diff --git a/addons/stock_account/stock_account_demo.yml b/addons/stock_account/data/stock_account_demo.yml similarity index 100% rename from addons/stock_account/stock_account_demo.yml rename to addons/stock_account/data/stock_account_demo.yml diff --git a/addons/stock_account/models/__init__.py b/addons/stock_account/models/__init__.py new file mode 100644 index 00000000000..7564d3b99c9 --- /dev/null +++ b/addons/stock_account/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import account_chart_template +import account_invoice +import product +import stock +import stock_config_settings diff --git a/addons/stock_account/models/account_chart_template.py b/addons/stock_account/models/account_chart_template.py new file mode 100644 index 00000000000..dba5aa64f29 --- /dev/null +++ b/addons/stock_account/models/account_chart_template.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.tools.translate import _ +from openerp import api, models + +import logging + +_logger = logging.getLogger(__name__) + + +class AccountChartTemplate(models.Model): + _inherit = "account.chart.template" + + @api.model + def generate_journals(self, acc_template_ref, company, journals_dict=None): + journal_to_add = [{'name': _('Stock Journal'), 'type': 'general', 'code': 'STJ', 'favorite': False, 'sequence': 8}] + super(AccountChartTemplate, self).generate_journals(acc_template_ref=acc_template_ref, company=company, journals_dict=journal_to_add) + + @api.multi + def generate_properties(self, acc_template_ref, company, property_list=None): + super(AccountChartTemplate, self).generate_properties(acc_template_ref=acc_template_ref, company=company) + PropertyObj = self.env['ir.property'] # Property Stock Journal + value = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'STJ'), ('type', '=', 'general')], limit=1) + if value: + field = self.env['ir.model.fields'].search([('name', '=', 'property_stock_journal'), ('model', '=', 'product.category'), ('relation', '=', 'account.journal')], limit=1) + vals = { + 'name': 'property_stock_journal', + 'company_id': company.id, + 'fields_id': field.id, + 'value': 'account.journal,%s' % value.id, + } + properties = PropertyObj.search([('name', '=', 'property_stock_journal'), ('company_id', '=', company.id)]) + if properties: + #the property exist: modify it + properties.write(vals) + else: + #create the property + PropertyObj.create(vals) + + todo_list = [ # Property Stock Accounts + 'property_stock_account_input_categ_id', + 'property_stock_account_output_categ_id', + 'property_stock_valuation_account_id', + ] + for record in todo_list: + account = getattr(self, record) + value = account and 'account.account,' + str(acc_template_ref[account.id]) or False + if value: + field = self.env['ir.model.fields'].search([('name', '=', record), ('model', '=', 'product.category'), ('relation', '=', 'account.account')], limit=1) + vals = { + 'name': record, + 'company_id': company.id, + 'fields_id': field.id, + 'value': value, + } + properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)]) + if properties: + #the property exist: modify it + properties.write(vals) + else: + #create the property + PropertyObj.create(vals) + + return True \ No newline at end of file diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py new file mode 100644 index 00000000000..d312bc108e0 --- /dev/null +++ b/addons/stock_account/models/account_invoice.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import api +from openerp.osv import osv + +import logging + +_logger = logging.getLogger(__name__) + + +class account_invoice(osv.osv): + _inherit = "account.invoice" + + @api.model + def invoice_line_move_line_get(self): + res = super(account_invoice,self).invoice_line_move_line_get() + if self.company_id.anglo_saxon_accounting: + if self.type in ('out_invoice','out_refund'): + for i_line in self.invoice_line_ids: + res.extend(self._anglo_saxon_sale_move_lines(i_line)) + return res + + @api.model + def _anglo_saxon_sale_move_lines(self, i_line): + """Return the additional move lines for sales invoices and refunds. + + i_line: An account.invoice.line object. + res: The move line entries produced so far by the parent move_line_get. + """ + inv = i_line.invoice_id + company_currency = inv.company_id.currency_id.id + + if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time': + fpos = i_line.invoice_id.fiscal_position_id + accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) + # debit account dacc will be the output account + dacc = accounts['stock_output'].id + # credit account cacc will be the expense account + cacc = accounts['expense'].id + if dacc and cacc: + price_unit = i_line._get_anglo_saxon_price_unit() + return [ + { + 'type':'src', + 'name': i_line.name[:64], + 'price_unit': price_unit, + 'quantity': i_line.quantity, + 'price': self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit), + 'account_id':dacc, + 'product_id':i_line.product_id.id, + 'uom_id':i_line.uom_id.id, + 'account_analytic_id': False, + }, + + { + 'type':'src', + 'name': i_line.name[:64], + 'price_unit': price_unit, + 'quantity': i_line.quantity, + 'price': -1 * self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit), + 'account_id':cacc, + 'product_id':i_line.product_id.id, + 'uom_id':i_line.uom_id.id, + 'account_analytic_id': False, + }, + ] + return [] + + +class account_invoice_line(osv.osv): + _inherit = "account.invoice.line" + + def _get_anglo_saxon_price_unit(self): + self.ensure_one() + return self.product_id.standard_price + + def _get_price(self, cr, uid, inv, company_currency, i_line, price_unit): + cur_obj = self.pool.get('res.currency') + if inv.currency_id.id != company_currency: + price = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, price_unit * i_line.quantity, context={'date': inv.date_invoice}) + else: + price = price_unit * i_line.quantity + return round(price, inv.currency_id.decimal_places) + + def get_invoice_line_account(self, type, product, fpos, company): + if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product': + accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) + return accounts['stock_input'] + return super(account_invoice_line, self).get_invoice_line_account(type, product, fpos, company) \ No newline at end of file diff --git a/addons/stock_account/product.py b/addons/stock_account/models/product.py similarity index 100% rename from addons/stock_account/product.py rename to addons/stock_account/models/product.py diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/models/stock.py similarity index 79% rename from addons/stock_account/stock_account.py rename to addons/stock_account/models/stock.py index 798e8fdda5d..f300890a0b3 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/models/stock.py @@ -26,86 +26,6 @@ class stock_inventory(osv.osv): return super(stock_inventory, self).post_inventory(cr, uid, ids, context=ctx) -class account_invoice_line(osv.osv): - _inherit = "account.invoice.line" - - def _get_anglo_saxon_price_unit(self): - self.ensure_one() - return self.product_id.standard_price - - def _get_price(self, cr, uid, inv, company_currency, i_line, price_unit): - cur_obj = self.pool.get('res.currency') - if inv.currency_id.id != company_currency: - price = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, price_unit * i_line.quantity, context={'date': inv.date_invoice}) - else: - price = price_unit * i_line.quantity - return round(price, inv.currency_id.decimal_places) - - def get_invoice_line_account(self, type, product, fpos, company): - if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product': - accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) - return accounts['stock_input'] - return super(account_invoice_line, self).get_invoice_line_account(type, product, fpos, company) - -class account_invoice(osv.osv): - _inherit = "account.invoice" - - @api.model - def invoice_line_move_line_get(self): - res = super(account_invoice,self).invoice_line_move_line_get() - if self.company_id.anglo_saxon_accounting: - if self.type in ('out_invoice','out_refund'): - for i_line in self.invoice_line_ids: - res.extend(self._anglo_saxon_sale_move_lines(i_line)) - return res - - @api.model - def _anglo_saxon_sale_move_lines(self, i_line): - """Return the additional move lines for sales invoices and refunds. - - i_line: An account.invoice.line object. - res: The move line entries produced so far by the parent move_line_get. - """ - inv = i_line.invoice_id - company_currency = inv.company_id.currency_id.id - - if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time': - fpos = i_line.invoice_id.fiscal_position_id - accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) - # debit account dacc will be the output account - dacc = accounts['stock_output'].id - # credit account cacc will be the expense account - cacc = accounts['expense'].id - if dacc and cacc: - price_unit = i_line._get_anglo_saxon_price_unit() - return [ - { - 'type':'src', - 'name': i_line.name[:64], - 'price_unit': price_unit, - 'quantity': i_line.quantity, - 'price': self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit), - 'account_id':dacc, - 'product_id':i_line.product_id.id, - 'uom_id':i_line.uom_id.id, - 'account_analytic_id': False, - }, - - { - 'type':'src', - 'name': i_line.name[:64], - 'price_unit': price_unit, - 'quantity': i_line.quantity, - 'price': -1 * self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit), - 'account_id':cacc, - 'product_id':i_line.product_id.id, - 'uom_id':i_line.uom_id.id, - 'account_analytic_id': False, - }, - ] - return [] - - #---------------------------------------------------------- # Stock Location #---------------------------------------------------------- @@ -126,6 +46,7 @@ class stock_location(osv.osv): "This has no effect for internal locations."), } + #---------------------------------------------------------- # Quants #---------------------------------------------------------- @@ -399,6 +320,7 @@ class stock_quant(osv.osv): # self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context) # return remaining_solving_quant, remaining_to_solve_quant + class stock_move(osv.osv): _inherit = "stock.move" @@ -461,58 +383,3 @@ class stock_move(osv.osv): if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal': #store the average price of the move on the move and product form self._store_average_cost_price(cr, uid, move, context=context) - -class AccountChartTemplate(models.Model): - _inherit = "account.chart.template" - - @api.model - def generate_journals(self, acc_template_ref, company, journals_dict=None): - journal_to_add = [{'name': _('Stock Journal'), 'type': 'general', 'code': 'STJ', 'favorite': False, 'sequence': 8}] - super(AccountChartTemplate, self).generate_journals(acc_template_ref=acc_template_ref, company=company, journals_dict=journal_to_add) - - @api.multi - def generate_properties(self, acc_template_ref, company, property_list=None): - super(AccountChartTemplate, self).generate_properties(acc_template_ref=acc_template_ref, company=company) - PropertyObj = self.env['ir.property'] # Property Stock Journal - value = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'STJ'), ('type', '=', 'general')], limit=1) - if value: - field = self.env['ir.model.fields'].search([('name', '=', 'property_stock_journal'), ('model', '=', 'product.category'), ('relation', '=', 'account.journal')], limit=1) - vals = { - 'name': 'property_stock_journal', - 'company_id': company.id, - 'fields_id': field.id, - 'value': 'account.journal,%s' % value.id, - } - properties = PropertyObj.search([('name', '=', 'property_stock_journal'), ('company_id', '=', company.id)]) - if properties: - #the property exist: modify it - properties.write(vals) - else: - #create the property - PropertyObj.create(vals) - - todo_list = [ # Property Stock Accounts - 'property_stock_account_input_categ_id', - 'property_stock_account_output_categ_id', - 'property_stock_valuation_account_id', - ] - for record in todo_list: - account = getattr(self, record) - value = account and 'account.account,' + str(acc_template_ref[account.id]) or False - if value: - field = self.env['ir.model.fields'].search([('name', '=', record), ('model', '=', 'product.category'), ('relation', '=', 'account.account')], limit=1) - vals = { - 'name': record, - 'company_id': company.id, - 'fields_id': field.id, - 'value': value, - } - properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)]) - if properties: - #the property exist: modify it - properties.write(vals) - else: - #create the property - PropertyObj.create(vals) - - return True diff --git a/addons/stock_account/res_config.py b/addons/stock_account/models/stock_config_settings.py similarity index 100% rename from addons/stock_account/res_config.py rename to addons/stock_account/models/stock_config_settings.py diff --git a/addons/stock_account/report/__init__.py b/addons/stock_account/report/__init__.py new file mode 100644 index 00000000000..6435c7f35ed --- /dev/null +++ b/addons/stock_account/report/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import stock_history diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/report/stock_history.py similarity index 89% rename from addons/stock_account/wizard/stock_valuation_history.py rename to addons/stock_account/report/stock_history.py index 3096e635bdd..26cf3c69d6c 100644 --- a/addons/stock_account/wizard/stock_valuation_history.py +++ b/addons/stock_account/report/stock_history.py @@ -4,38 +4,6 @@ from openerp import tools from openerp.osv import fields, osv from openerp.tools.translate import _ -class wizard_valuation_history(osv.osv_memory): - - _name = 'wizard.valuation.history' - _description = 'Wizard that opens the stock valuation history table' - _columns = { - 'choose_date': fields.boolean('Inventory at Date'), - 'date': fields.datetime('Date', required=True), - } - - _defaults = { - 'choose_date': False, - 'date': fields.datetime.now, - } - - def open_table(self, cr, uid, ids, context=None): - if context is None: - context = {} - data = self.read(cr, uid, ids, context=context)[0] - ctx = context.copy() - ctx['history_date'] = data['date'] - ctx['search_default_group_by_product'] = True - ctx['search_default_group_by_location'] = True - return { - 'domain': "[('date', '<=', '" + data['date'] + "')]", - 'name': _('Stock Value At Date'), - 'view_type': 'form', - 'view_mode': 'tree', - 'res_model': 'stock.history', - 'type': 'ir.actions.act_window', - 'context': ctx, - } - class stock_history(osv.osv): _name = 'stock.history' diff --git a/addons/stock_account/wizard/stock_valuation_history_view.xml b/addons/stock_account/report/stock_history_views.xml similarity index 63% rename from addons/stock_account/wizard/stock_valuation_history_view.xml rename to addons/stock_account/report/stock_history_views.xml index 7c0955daed4..b4902c1759b 100644 --- a/addons/stock_account/wizard/stock_valuation_history_view.xml +++ b/addons/stock_account/report/stock_history_views.xml @@ -2,38 +2,6 @@ - - Inventory at Date - wizard.valuation.history - -
-

- Choose a date in the past to get the inventory at that date. -

-

- Retrieve the curent stock valuation. -

- - - - -
-
-
-
-
- - Inventory at Date - wizard.valuation.history - form - form - - new - - - Stock Value At Date stock.history diff --git a/addons/stock_account/product_view.xml b/addons/stock_account/views/product_views.xml similarity index 100% rename from addons/stock_account/product_view.xml rename to addons/stock_account/views/product_views.xml diff --git a/addons/stock_account/stock_account_view.xml b/addons/stock_account/views/stock_account_views.xml similarity index 100% rename from addons/stock_account/stock_account_view.xml rename to addons/stock_account/views/stock_account_views.xml diff --git a/addons/stock_account/res_config_view.xml b/addons/stock_account/views/stock_config_settings_views.xml similarity index 100% rename from addons/stock_account/res_config_view.xml rename to addons/stock_account/views/stock_config_settings_views.xml diff --git a/addons/stock_account/wizard/__init__.py b/addons/stock_account/wizard/__init__.py index 860c206efa0..81b976cd812 100644 --- a/addons/stock_account/wizard/__init__.py +++ b/addons/stock_account/wizard/__init__.py @@ -2,4 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import stock_change_standard_price -import stock_valuation_history +import wizard_valuation_history diff --git a/addons/stock_account/wizard/stock_change_standard_price_view.xml b/addons/stock_account/wizard/stock_change_standard_price_views.xml similarity index 100% rename from addons/stock_account/wizard/stock_change_standard_price_view.xml rename to addons/stock_account/wizard/stock_change_standard_price_views.xml diff --git a/addons/stock_account/wizard/wizard_valuation_history.py b/addons/stock_account/wizard/wizard_valuation_history.py new file mode 100644 index 00000000000..7d192a2998e --- /dev/null +++ b/addons/stock_account/wizard/wizard_valuation_history.py @@ -0,0 +1,35 @@ +from openerp.osv import fields, osv +from openerp.tools.translate import _ + + +class wizard_valuation_history(osv.osv_memory): + + _name = 'wizard.valuation.history' + _description = 'Wizard that opens the stock valuation history table' + _columns = { + 'choose_date': fields.boolean('Inventory at Date'), + 'date': fields.datetime('Date', required=True), + } + + _defaults = { + 'choose_date': False, + 'date': fields.datetime.now, + } + + def open_table(self, cr, uid, ids, context=None): + if context is None: + context = {} + data = self.read(cr, uid, ids, context=context)[0] + ctx = context.copy() + ctx['history_date'] = data['date'] + ctx['search_default_group_by_product'] = True + ctx['search_default_group_by_location'] = True + return { + 'domain': "[('date', '<=', '" + data['date'] + "')]", + 'name': _('Stock Value At Date'), + 'view_type': 'form', + 'view_mode': 'tree', + 'res_model': 'stock.history', + 'type': 'ir.actions.act_window', + 'context': ctx, + } diff --git a/addons/stock_account/wizard/wizard_valuation_history_views.xml b/addons/stock_account/wizard/wizard_valuation_history_views.xml new file mode 100644 index 00000000000..5f35b8fcb35 --- /dev/null +++ b/addons/stock_account/wizard/wizard_valuation_history_views.xml @@ -0,0 +1,34 @@ + + + + Inventory at Date + wizard.valuation.history + +
+

+ Choose a date in the past to get the inventory at that date. +

+

+ Retrieve the curent stock valuation. +

+ + + + +
+
+
+
+
+ + Inventory at Date + wizard.valuation.history + form + form + + new + + +
\ No newline at end of file From 288e0acca10b3d4136494b11cd14e60f66924a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 7 Jul 2016 10:41:20 +0200 Subject: [PATCH 10/20] [REF] stock_account: pre migration cleaning * All methods are converted into an api.multi style (using ids instead of a browse record or an id) * Dead code on product.template model: existing method on product.product has to be used * ``_get_accounting_data_for_valuation`` and ``_prepare_account_move_line`` methods moved on the stock.move model as their primary object is a stock move * Simplified implementation and call parameters of ``_get_price`` of account.invoice.line to ease code understanding --- .../stock_account/models/account_invoice.py | 15 +- addons/stock_account/models/product.py | 54 ----- addons/stock_account/models/stock.py | 198 +++++++++--------- 3 files changed, 103 insertions(+), 164 deletions(-) diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index d312bc108e0..a75267107e8 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -46,7 +46,7 @@ class account_invoice(osv.osv): 'name': i_line.name[:64], 'price_unit': price_unit, 'quantity': i_line.quantity, - 'price': self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit), + 'price': i_line._get_price(company_currency, price_unit), 'account_id':dacc, 'product_id':i_line.product_id.id, 'uom_id':i_line.uom_id.id, @@ -58,7 +58,7 @@ class account_invoice(osv.osv): 'name': i_line.name[:64], 'price_unit': price_unit, 'quantity': i_line.quantity, - 'price': -1 * self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit), + 'price': -1 * i_line._get_price(company_currency, price_unit), 'account_id':cacc, 'product_id':i_line.product_id.id, 'uom_id':i_line.uom_id.id, @@ -75,13 +75,14 @@ class account_invoice_line(osv.osv): self.ensure_one() return self.product_id.standard_price - def _get_price(self, cr, uid, inv, company_currency, i_line, price_unit): + def _get_price(self, cr, uid, ids, company_currency, price_unit, context=None): + line = self.browse(cr, uid, ids, context=context)[0] cur_obj = self.pool.get('res.currency') - if inv.currency_id.id != company_currency: - price = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, price_unit * i_line.quantity, context={'date': inv.date_invoice}) + if line.invoice_id.currency_id.id != company_currency: + price = cur_obj.compute(cr, uid, company_currency, line.invoice_id.currency_id.id, price_unit * line.quantity, context={'date': line.invoice_id.date_invoice}) else: - price = price_unit * i_line.quantity - return round(price, inv.currency_id.decimal_places) + price = price_unit * line.quantity + return round(price, line.invoice_id.currency_id.decimal_places) def get_invoice_line_account(self, type, product, fpos, company): if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product': diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 8572d670438..12a68ea287e 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -104,60 +104,6 @@ class product_template(osv.osv): accounts.update({'stock_journal': self.categ_id.property_stock_journal or False}) return accounts - # To remove in master because this function is now used on "product.product" model. - def do_change_standard_price(self, cr, uid, ids, new_price, account_id, context=None): - """ Changes the Standard Price of Product and creates an account move accordingly.""" - location_obj = self.pool.get('stock.location') - move_obj = self.pool.get('account.move') - if context is None: - context = {} - user_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - loc_ids = location_obj.search(cr, uid, [('usage', '=', 'internal'), ('company_id', '=', user_company_id)]) - for rec_id in ids: - datas = self.get_product_accounts(cr, uid, rec_id, context=context) - for location in location_obj.browse(cr, uid, loc_ids, context=context): - c = context.copy() - c.update({'location': location.id, 'compute_child': False}) - product = self.browse(cr, uid, rec_id, context=c) - - diff = product.standard_price - new_price - if not diff: - raise UserError(_("No difference between standard price and new price!")) - for prod_variant in product.product_variant_ids: - qty = prod_variant.qty_available - if qty: - # Accounting Entries - amount_diff = abs(diff * qty) - if diff * qty > 0: - debit_account_id = account_id - credit_account_id = datas['stock_valuation'].id - else: - debit_account_id = datas['stock_valuation'].id - credit_account_id = account_id - - lines = [(0, 0, {'name': _('Standard Price changed'), - 'account_id': debit_account_id, - 'debit': amount_diff, - 'credit': 0, - 'product_id': product.id, - }), - (0, 0, { - 'name': _('Standard Price changed'), - 'account_id': credit_account_id, - 'debit': 0, - 'credit': amount_diff, - 'product_id': product.id, - })] - move_vals = { - 'journal_id': datas['stock_journal'].id, - 'company_id': location.company_id.id, - 'line_ids': lines, - } - move_id = move_obj.create(cr, uid, move_vals, context=context) - move_obj.post(cr, uid, [move_id], context=context) - self.write(cr, uid, rec_id, {'standard_price': new_price}) - return True - class product_product(osv.osv): _inherit = 'product.product' diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index f300890a0b3..c3696213c0a 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -84,7 +84,7 @@ class stock_quant(osv.osv): #1) the product cost's method is 'real' #2) we just fixed a negative quant caused by an outgoing shipment if quant.product_id.cost_method == 'real' and quant.location_id.usage != 'internal': - self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context) + self.pool.get('stock.move')._store_average_cost_price(cr, uid, [move.id], context=context) def _account_entry_move(self, cr, uid, ids, move, context=None): """ @@ -122,23 +122,23 @@ class stock_quant(osv.osv): if company_to and (move.location_id.usage not in ('internal', 'transit') and move.location_dest_id.usage == 'internal' or company_from != company_to): ctx = context.copy() ctx['force_company'] = company_to.id - journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx) + journal_id, acc_src, acc_dest, acc_valuation = self.pool['stock.move']._get_accounting_data_for_valuation(cr, uid, [move.id], context=ctx) if location_from and location_from.usage == 'customer': #goods returned from customer - self._create_account_move_line(cr, uid, quants, move, acc_dest, acc_valuation, journal_id, context=ctx) + self._create_account_move_line(cr, uid, quants.ids, move, acc_dest, acc_valuation, journal_id, context=ctx) else: - self._create_account_move_line(cr, uid, quants, move, acc_src, acc_valuation, journal_id, context=ctx) + self._create_account_move_line(cr, uid, quants.ids, move, acc_src, acc_valuation, journal_id, context=ctx) # Create Journal Entry for products leaving the company if company_from and (move.location_id.usage == 'internal' and move.location_dest_id.usage not in ('internal', 'transit') or company_from != company_to): ctx = context.copy() ctx['force_company'] = company_from.id - journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx) + journal_id, acc_src, acc_dest, acc_valuation = self.pool['stock.move']._get_accounting_data_for_valuation(cr, uid, [move.id], context=ctx) if location_to and location_to.usage == 'supplier': #goods returned to supplier - self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_src, journal_id, context=ctx) + self._create_account_move_line(cr, uid, quants.ids, move, acc_valuation, acc_src, journal_id, context=ctx) else: - self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_dest, journal_id, context=ctx) + self._create_account_move_line(cr, uid, quants.ids, move, acc_valuation, acc_dest, journal_id, context=ctx) def _quant_create_from_move(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): quant_obj = self.pool.get('stock.quant') @@ -175,7 +175,91 @@ class stock_quant(osv.osv): self._account_entry_move(move) return res - def _get_accounting_data_for_valuation(self, cr, uid, move, context=None): + def _create_account_move_line(self, cr, uid, ids, move, credit_account_id, debit_account_id, journal_id, context=None): + quants = self.browse(cr, uid, ids, context=context) + # group quants by cost + quant_cost_qty = {} + for quant in quants: + if quant_cost_qty.get(quant.cost): + quant_cost_qty[quant.cost] += quant.qty + else: + quant_cost_qty[quant.cost] = quant.qty + move_obj = self.pool.get('account.move') + for cost, qty in quant_cost_qty.items(): + move_lines = self.pool['stock.move']._prepare_account_move_line(cr, uid, [move.id], qty, cost, credit_account_id, debit_account_id, context=context) + date = context.get('force_period_date', fields.date.context_today(self, cr, uid, context=context)) + new_move = move_obj.create(cr, uid, {'journal_id': journal_id, + 'line_ids': move_lines, + 'date': date, + 'ref': move.picking_id.name}, context=context) + move_obj.post(cr, uid, [new_move], context=context) + + +class stock_move(osv.osv): + _inherit = "stock.move" + + def action_done(self, cr, uid, ids, context=None): + self.product_price_update_before_done(cr, uid, ids, context=context) + res = super(stock_move, self).action_done(cr, uid, ids, context=context) + self.product_price_update_after_done(cr, uid, ids, context=context) + return res + + def _store_average_cost_price(self, cr, uid, ids, context=None): + ''' move is a browe record ''' + move = self.browse(cr, uid, ids, context=context)[0] + product_obj = self.pool.get('product.product') + if any([q.qty <= 0 for q in move.quant_ids]) or move.product_qty == 0: + #if there is a negative quant, the standard price shouldn't be updated + return + #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 valuate it at 6€ + average_valuation_price = 0.0 + for q in move.quant_ids: + average_valuation_price += q.qty * q.cost + average_valuation_price = average_valuation_price / move.product_qty + # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products + ctx = dict(context or {}, force_company=move.company_id.id) + product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=ctx) + self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context) + + def product_price_update_before_done(self, cr, uid, ids, context=None): + product_obj = self.pool.get('product.product') + tmpl_dict = {} + for move in self.browse(cr, uid, ids, context=context): + #adapt standard price on incomming moves if the product cost_method is 'average' + if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'): + product = move.product_id + product_id = move.product_id.id + qty_available = move.product_id.qty_available + if tmpl_dict.get(product_id): + product_avail = qty_available + tmpl_dict[product_id] + else: + tmpl_dict[product_id] = 0 + product_avail = qty_available + # if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use. + price_unit = move.get_price_unit() + if product_avail <= 0: + new_std_price = price_unit + else: + # Get the standard price + amount_unit = product.standard_price + new_std_price = ((amount_unit * product_avail) + (price_unit * move.product_qty)) / (product_avail + move.product_qty) + tmpl_dict[product_id] += move.product_qty + # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products + ctx = dict(context or {}, force_company=move.company_id.id) + product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=ctx) + + def product_price_update_after_done(self, cr, uid, ids, context=None): + ''' + This method adapts the price on the product when necessary + ''' + for move in self.browse(cr, uid, ids, context=context): + #adapt standard price on outgoing moves if the product cost_method is 'real', so that a return + #or an inventory loss is made using the last value used for an outgoing valuation. + if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal': + #store the average price of the move on the move and product form + self._store_average_cost_price(cr, uid, [move.id], context=context) + + def _get_accounting_data_for_valuation(self, cr, uid, ids, context=None): """ Return the accounts and journal to use to post Journal Entries for the real-time valuation of the quant. @@ -184,6 +268,7 @@ class stock_quant(osv.osv): :returns: journal_id, source account, destination account, valuation account :raise: openerp.exceptions.UserError if any mandatory account or journal is not defined. """ + move = self.browse(cr, uid, ids, context=context)[0] product_obj = self.pool.get('product.template') accounts = product_obj.browse(cr, uid, move.product_id.product_tmpl_id.id, context).get_product_accounts() if move.location_id.valuation_out_account_id: @@ -210,11 +295,12 @@ class stock_quant(osv.osv): journal_id = accounts['stock_journal'].id return journal_id, acc_src, acc_dest, acc_valuation - def _prepare_account_move_line(self, cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=None): + def _prepare_account_move_line(self, cr, uid, ids, qty, cost, credit_account_id, debit_account_id, context=None): """ Generate the account.move.line values to post to track the stock valuation difference due to the processing of the given quant. """ + move = self.browse(cr, uid, ids, context=context)[0] if context is None: context = {} currency_obj = self.pool.get('res.currency') @@ -289,97 +375,3 @@ class stock_quant(osv.osv): } res.append((0, 0, price_diff_line)) return res - - def _create_account_move_line(self, cr, uid, quants, move, credit_account_id, debit_account_id, journal_id, context=None): - #group quants by cost - quant_cost_qty = {} - for quant in quants: - if quant_cost_qty.get(quant.cost): - quant_cost_qty[quant.cost] += quant.qty - else: - quant_cost_qty[quant.cost] = quant.qty - move_obj = self.pool.get('account.move') - for cost, qty in quant_cost_qty.items(): - move_lines = self._prepare_account_move_line(cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=context) - date = context.get('force_period_date', fields.date.context_today(self, cr, uid, context=context)) - new_move = move_obj.create(cr, uid, {'journal_id': journal_id, - 'line_ids': move_lines, - 'date': date, - 'ref': move.picking_id.name}, context=context) - move_obj.post(cr, uid, [new_move], context=context) - - #def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None): - # move = self._get_latest_move(cr, uid, to_solve_quant, context=context) - # quant_neg_position = quant_neg.negative_dest_location_id.usage - # 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) - # #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 - # #1) there isn't any negative quant anymore - # #2) the product cost's method is 'real' - # #3) we just fixed a negative quant caused by an outgoing shipment - # if not remaining_to_solve_quant and move.product_id.cost_method == 'real' and quant_neg_position != 'internal': - # self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context) - # return remaining_solving_quant, remaining_to_solve_quant - - -class stock_move(osv.osv): - _inherit = "stock.move" - - def action_done(self, cr, uid, ids, context=None): - self.product_price_update_before_done(cr, uid, ids, context=context) - res = super(stock_move, self).action_done(cr, uid, ids, context=context) - self.product_price_update_after_done(cr, uid, ids, context=context) - return res - - def _store_average_cost_price(self, cr, uid, move, context=None): - ''' move is a browe record ''' - product_obj = self.pool.get('product.product') - if any([q.qty <= 0 for q in move.quant_ids]) or move.product_qty == 0: - #if there is a negative quant, the standard price shouldn't be updated - return - #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 valuate it at 6€ - average_valuation_price = 0.0 - for q in move.quant_ids: - average_valuation_price += q.qty * q.cost - average_valuation_price = average_valuation_price / move.product_qty - # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products - ctx = dict(context or {}, force_company=move.company_id.id) - product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=ctx) - self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context) - - def product_price_update_before_done(self, cr, uid, ids, context=None): - product_obj = self.pool.get('product.product') - tmpl_dict = {} - for move in self.browse(cr, uid, ids, context=context): - #adapt standard price on incomming moves if the product cost_method is 'average' - if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'): - product = move.product_id - product_id = move.product_id.id - qty_available = move.product_id.qty_available - if tmpl_dict.get(product_id): - product_avail = qty_available + tmpl_dict[product_id] - else: - tmpl_dict[product_id] = 0 - product_avail = qty_available - # if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use. - price_unit = move.get_price_unit() - if product_avail <= 0: - new_std_price = price_unit - else: - # Get the standard price - amount_unit = product.standard_price - new_std_price = ((amount_unit * product_avail) + (price_unit * move.product_qty)) / (product_avail + move.product_qty) - tmpl_dict[product_id] += move.product_qty - # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products - ctx = dict(context or {}, force_company=move.company_id.id) - product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=ctx) - - def product_price_update_after_done(self, cr, uid, ids, context=None): - ''' - This method adapts the price on the product when necessary - ''' - for move in self.browse(cr, uid, ids, context=context): - #adapt standard price on outgoing moves if the product cost_method is 'real', so that a return - #or an inventory loss is made using the last value used for an outgoing valuation. - if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal': - #store the average price of the move on the move and product form - self._store_average_cost_price(cr, uid, move, context=context) From 1919945770402fc15c823bf0cbd76c04ccb6fc03 Mon Sep 17 00:00:00 2001 From: Divyesh Makwana Date: Thu, 7 Jul 2016 10:22:57 +0200 Subject: [PATCH 11/20] [MIG] stock_account: new API --- addons/stock_account/__openerp__.py | 1 - addons/stock_account/data/product_data.xml | 6 +- .../stock_account/data/stock_account_data.xml | 6 +- .../models/account_chart_template.py | 18 +- .../stock_account/models/account_invoice.py | 38 +- addons/stock_account/models/product.py | 283 +++++----- addons/stock_account/models/stock.py | 486 ++++++++---------- .../models/stock_config_settings.py | 36 +- addons/stock_account/report/stock_history.py | 122 ++--- .../report/stock_history_views.xml | 8 +- .../security/stock_account_security.xml | 4 +- .../test/stock_valuation_account.xml | 4 +- addons/stock_account/views/product_views.xml | 4 +- .../views/stock_account_views.xml | 4 +- .../views/stock_config_settings_views.xml | 6 +- .../wizard/stock_change_standard_price.py | 87 ++-- .../stock_change_standard_price_views.xml | 5 +- .../wizard/wizard_valuation_history.py | 37 +- .../wizard/wizard_valuation_history_views.xml | 4 +- 19 files changed, 501 insertions(+), 658 deletions(-) diff --git a/addons/stock_account/__openerp__.py b/addons/stock_account/__openerp__.py index 438adc04385..8d5c0464fcd 100644 --- a/addons/stock_account/__openerp__.py +++ b/addons/stock_account/__openerp__.py @@ -40,7 +40,6 @@ Dashboard / Reports for Warehouse Management includes: 'data/stock_account_data_post_install.yml', ], 'test': [ - ], 'installable': True, 'auto_install': True, diff --git a/addons/stock_account/data/product_data.xml b/addons/stock_account/data/product_data.xml index 83de85fd67e..f1c3044dced 100644 --- a/addons/stock_account/data/product_data.xml +++ b/addons/stock_account/data/product_data.xml @@ -1,7 +1,6 @@ - + - property_stock_account_output @@ -26,6 +25,5 @@ - - + diff --git a/addons/stock_account/data/stock_account_data.xml b/addons/stock_account/data/stock_account_data.xml index 1db707e2ec2..f3bffa3a6d3 100644 --- a/addons/stock_account/data/stock_account_data.xml +++ b/addons/stock_account/data/stock_account_data.xml @@ -1,7 +1,6 @@ - + - Cost Method Property @@ -27,6 +26,5 @@ manual_periodic selection - - \ No newline at end of file + \ No newline at end of file diff --git a/addons/stock_account/models/account_chart_template.py b/addons/stock_account/models/account_chart_template.py index dba5aa64f29..fdf75e5978e 100644 --- a/addons/stock_account/models/account_chart_template.py +++ b/addons/stock_account/models/account_chart_template.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.tools.translate import _ -from openerp import api, models +from odoo import api, models, _ import logging - _logger = logging.getLogger(__name__) @@ -15,11 +13,11 @@ class AccountChartTemplate(models.Model): @api.model def generate_journals(self, acc_template_ref, company, journals_dict=None): journal_to_add = [{'name': _('Stock Journal'), 'type': 'general', 'code': 'STJ', 'favorite': False, 'sequence': 8}] - super(AccountChartTemplate, self).generate_journals(acc_template_ref=acc_template_ref, company=company, journals_dict=journal_to_add) + return super(AccountChartTemplate, self).generate_journals(acc_template_ref=acc_template_ref, company=company, journals_dict=journal_to_add) @api.multi def generate_properties(self, acc_template_ref, company, property_list=None): - super(AccountChartTemplate, self).generate_properties(acc_template_ref=acc_template_ref, company=company) + res = super(AccountChartTemplate, self).generate_properties(acc_template_ref=acc_template_ref, company=company) PropertyObj = self.env['ir.property'] # Property Stock Journal value = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'STJ'), ('type', '=', 'general')], limit=1) if value: @@ -32,10 +30,10 @@ class AccountChartTemplate(models.Model): } properties = PropertyObj.search([('name', '=', 'property_stock_journal'), ('company_id', '=', company.id)]) if properties: - #the property exist: modify it + # the property exist: modify it properties.write(vals) else: - #create the property + # create the property PropertyObj.create(vals) todo_list = [ # Property Stock Accounts @@ -56,10 +54,10 @@ class AccountChartTemplate(models.Model): } properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)]) if properties: - #the property exist: modify it + # the property exist: modify it properties.write(vals) else: - #create the property + # create the property PropertyObj.create(vals) - return True \ No newline at end of file + return res diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index a75267107e8..b4149ead98b 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -1,24 +1,22 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import api -from openerp.osv import osv +from odoo import api, models import logging _logger = logging.getLogger(__name__) -class account_invoice(osv.osv): +class AccountInvoice(models.Model): _inherit = "account.invoice" @api.model def invoice_line_move_line_get(self): - res = super(account_invoice,self).invoice_line_move_line_get() - if self.company_id.anglo_saxon_accounting: - if self.type in ('out_invoice','out_refund'): - for i_line in self.invoice_line_ids: - res.extend(self._anglo_saxon_sale_move_lines(i_line)) + res = super(AccountInvoice, self).invoice_line_move_line_get() + if self.company_id.anglo_saxon_accounting and self.type in ('out_invoice', 'out_refund'): + for i_line in self.invoice_line_ids: + res.extend(self._anglo_saxon_sale_move_lines(i_line)) return res @api.model @@ -29,9 +27,9 @@ class account_invoice(osv.osv): res: The move line entries produced so far by the parent move_line_get. """ inv = i_line.invoice_id - company_currency = inv.company_id.currency_id.id + company_currency = inv.company_id.currency_id - if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time': + if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time': fpos = i_line.invoice_id.fiscal_position_id accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) # debit account dacc will be the output account @@ -42,7 +40,7 @@ class account_invoice(osv.osv): price_unit = i_line._get_anglo_saxon_price_unit() return [ { - 'type':'src', + 'type': 'src', 'name': i_line.name[:64], 'price_unit': price_unit, 'quantity': i_line.quantity, @@ -54,7 +52,7 @@ class account_invoice(osv.osv): }, { - 'type':'src', + 'type': 'src', 'name': i_line.name[:64], 'price_unit': price_unit, 'quantity': i_line.quantity, @@ -68,24 +66,22 @@ class account_invoice(osv.osv): return [] -class account_invoice_line(osv.osv): +class AccountInvoiceLine(models.Model): _inherit = "account.invoice.line" def _get_anglo_saxon_price_unit(self): self.ensure_one() return self.product_id.standard_price - def _get_price(self, cr, uid, ids, company_currency, price_unit, context=None): - line = self.browse(cr, uid, ids, context=context)[0] - cur_obj = self.pool.get('res.currency') - if line.invoice_id.currency_id.id != company_currency: - price = cur_obj.compute(cr, uid, company_currency, line.invoice_id.currency_id.id, price_unit * line.quantity, context={'date': line.invoice_id.date_invoice}) + def _get_price(self, company_currency, price_unit): + if self.invoice_id.currency_id.id != company_currency: + price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id.id) else: - price = price_unit * line.quantity - return round(price, line.invoice_id.currency_id.decimal_places) + price = price_unit * self.quantity + return round(price, self.invoice_id.currency_id.decimal_places) def get_invoice_line_account(self, type, product, fpos, company): if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product': accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) return accounts['stock_input'] - return super(account_invoice_line, self).get_invoice_line_account(type, product, fpos, company) \ No newline at end of file + return super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company) diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 12a68ea287e..96d311416c1 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -1,79 +1,59 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp import api -from openerp.exceptions import UserError +from odoo import api, fields, models, tools, _ +from odoo.addons import decimal_precision as dp +from odoo.exceptions import UserError -class product_template(osv.osv): +class ProductTemplate(models.Model): _name = 'product.template' _inherit = 'product.template' - def _get_cost_method(self, cr, uid, ids, field, args, context=None): - res = {} - for product in self.browse(cr, uid, ids, context=context): - if product.property_cost_method: - res[product.id] = product.property_cost_method - else: - res[product.id] = product.categ_id.property_cost_method - return res + property_valuation = fields.Selection([ + ('manual_periodic', 'Periodic (manual)'), + ('real_time', 'Perpetual (automated)')], string='Inventory Valuation', + company_dependent=True, copy=True, default='manual_periodic', + help="If perpetual valuation is enabled for a product, the system will automatically create journal entries corresponding to stock moves, with product price as specified by the 'Costing Method'" \ + "The inventory variation account set on the product category will represent the current inventory value, and the stock input and stock output account will hold the counterpart moves for incoming and outgoing products.") + valuation = fields.Char(compute='_compute_valuation_type', inverse='_set_valuation_type') + property_cost_method = fields.Selection([ + ('standard', 'Standard Price'), + ('average', 'Average Price'), + ('real', 'Real Price')], string='Costing Method', + company_dependent=True, copy=True, + help="""Standard Price: The cost price is manually updated at the end of a specific period (usually once a year). + Average Price: The cost price is recomputed at each incoming shipment and used for the product valuation. + Real Price: The cost price displayed is the price of the last outgoing product (will be use in case of inventory loss for example).""") + cost_method = fields.Char(compute='_compute_cost_method', inverse='_set_cost_method') + property_stock_account_input = fields.Many2one( + 'account.account', 'Stock Input Account', + company_dependent=True, domain=[('deprecated', '=', False)], + help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless " + "there is a specific valuation account set on the source location. When not set on the product, the one from the product category is used.") + property_stock_account_output = fields.Many2one( + 'account.account', 'Stock Output Account', + company_dependent=True, domain=[('deprecated', '=', False)], + help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless " + "there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used.") - def _get_valuation_type(self, cr, uid, ids, field, args, context=None): - res = {} - for product in self.browse(cr, uid, ids, context=context): - if product.property_valuation: - res[product.id] = product.property_valuation - else: - res[product.id] = product.categ_id.property_valuation - return res + @api.one + @api.depends('property_valuation', 'categ_id.property_valuation') + def _compute_valuation_type(self): + self.valuation = self.property_valuation if self.property_valuation else self.categ_id.property_valuation - def _set_cost_method(self, cr, uid, ids, name, value, arg, context=None): - return self.write(cr, uid, ids, {'property_cost_method': value}, context=context) + @api.one + def _set_valuation_type(self): + return self.write({'property_valuation': self.valuation}) - def _set_valuation_type(self, cr, uid, ids, name, value, arg, context=None): - return self.write(cr, uid, ids, {'property_valuation': value}, context=context) + @api.one + @api.depends('property_cost_method', 'categ_id.property_cost_method') + def _compute_cost_method(self): + self.cost_method = self.property_cost_method if self.property_cost_method else self.categ_id.property_cost_method - _columns = { - 'property_valuation': fields.property(type='selection', selection=[('manual_periodic', 'Periodic (manual)'), - ('real_time', 'Perpetual (automated)')], string='Inventory Valuation', - help="If perpetual valuation is enabled for a product, the system will automatically create journal entries corresponding to stock moves, with product price as specified by the 'Costing Method'" \ - "The inventory variation account set on the product category will represent the current inventory value, and the stock input and stock output account will hold the counterpart moves for incoming and outgoing products." - , copy=True), - 'valuation': fields.function(_get_valuation_type, fnct_inv=_set_valuation_type, type='char'), # TDE FIXME: store it ? - 'property_cost_method': fields.property(type='selection', selection=[('standard', 'Standard Price'), ('average', 'Average Price'), ('real', 'Real Price')], - help="""Standard Price: The cost price is manually updated at the end of a specific period (usually once a year). - Average Price: The cost price is recomputed at each incoming shipment and used for the product valuation. - Real Price: The cost price displayed is the price of the last outgoing product (will be use in case of inventory loss for example).""", - string="Costing Method", copy=True), - 'cost_method': fields.function(_get_cost_method, fnct_inv=_set_cost_method, type='char'), # TDE FIXME: store it ? - 'property_stock_account_input': fields.property( - type='many2one', - relation='account.account', - string='Stock Input Account', - domain=[('deprecated', '=', False)], - help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless " - "there is a specific valuation account set on the source location. When not set on the product, the one from the product category is used."), - 'property_stock_account_output': fields.property( - type='many2one', - relation='account.account', - string='Stock Output Account', - domain=[('deprecated', '=', False)], - help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless " - "there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used."), - } - - _defaults = { - 'property_valuation': 'manual_periodic', - } - - def create(self, cr, uid, vals, context=None): - if vals.get('cost_method'): - vals['property_cost_method'] = vals.pop('cost_method') - if vals.get('valuation'): - vals['property_valuation'] = vals.pop('valuation') - return super(product_template, self).create(cr, uid, vals, context=context) + @api.one + def _set_cost_method(self): + return self.write({'property_cost_method': self.cost_method}) @api.onchange('type') def onchange_type_valuation(self): @@ -86,7 +66,7 @@ class product_template(osv.osv): """ Add the stock accounts related to product to the result of super() @return: dictionary which contains information regarding stock accounts and super (income+expense accounts) """ - accounts = super(product_template, self)._get_product_accounts() + accounts = super(ProductTemplate, self)._get_product_accounts() res = self._get_asset_accounts() accounts.update({ 'stock_input': res['stock_input'] or self.property_stock_account_input or self.categ_id.property_stock_account_input_categ_id, @@ -100,12 +80,12 @@ class product_template(osv.osv): """ Add the stock journal related to product to the result of super() @return: dictionary which contains all needed information regarding stock accounts and journal and super (income+expense accounts) """ - accounts = super(product_template, self).get_product_accounts(fiscal_pos=fiscal_pos) + accounts = super(ProductTemplate, self).get_product_accounts(fiscal_pos=fiscal_pos) accounts.update({'stock_journal': self.categ_id.property_stock_journal or False}) return accounts -class product_product(osv.osv): +class ProductProduct(models.Model): _inherit = 'product.product' @api.onchange('type') @@ -114,109 +94,94 @@ class product_product(osv.osv): self.valuation = 'manual_periodic' return {} - def do_change_standard_price(self, cr, uid, ids, new_price, account_id, context=None): + @api.multi + def do_change_standard_price(self, new_price, account_id): """ Changes the Standard Price of Product and creates an account move accordingly.""" - location_obj = self.pool.get('stock.location') - move_obj = self.pool.get('account.move') - if context is None: - context = {} - user_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - loc_ids = location_obj.search(cr, uid, [('usage', '=', 'internal'), ('company_id', '=', user_company_id)]) - for rec_id in ids: - for location in location_obj.browse(cr, uid, loc_ids, context=context): - c = context.copy() - c.update({'location': location.id, 'compute_child': False}) - product = self.browse(cr, uid, rec_id, context=c) - datas = self.pool['product.template'].get_product_accounts(cr, uid, product.product_tmpl_id.id, context=context) + AccountMove = self.env['account.move'] + + locations = self.env['stock.location'].search([('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id)]) + + product_accounts = {(product.id, product.product_tmpl_id.get_product_accounts()) for product in self} + price_precision = dp.get_precision('Product Price') + + for location in locations: + for product in self.with_context(location=location.id, compute_child=False): diff = product.standard_price - new_price - if not diff: + if tools.float_is_zero(diff, precision_digits=price_precision): raise UserError(_("No difference between standard price and new price!")) - qty = product.qty_available - if qty: + qty_available = product.qty_available + if qty_available: # Accounting Entries - amount_diff = abs(diff * qty) - if diff * qty > 0: + if diff * qty_available > 0: debit_account_id = account_id - credit_account_id = datas['stock_valuation'].id + credit_account_id = product_accounts[product.id]['stock_valuation'].id else: - debit_account_id = datas['stock_valuation'].id + debit_account_id = product_accounts[product.id]['stock_valuation'].id credit_account_id = account_id - lines = [(0, 0, {'name': _('Standard Price changed'), - 'account_id': debit_account_id, - 'debit': amount_diff, - 'credit': 0, - }), - (0, 0, { - 'name': _('Standard Price changed'), - 'account_id': credit_account_id, - 'debit': 0, - 'credit': amount_diff, - })] move_vals = { - 'journal_id': datas['stock_journal'].id, + 'journal_id': product_accounts[product.id]['stock_journal'].id, 'company_id': location.company_id.id, - 'line_ids': lines, + 'line_ids': [(0, 0, { + 'name': _('Standard Price changed'), + 'account_id': debit_account_id, + 'debit': abs(diff * qty_available), + 'credit': 0, + }), (0, 0, { + 'name': _('Standard Price changed'), + 'account_id': credit_account_id, + 'debit': 0, + 'credit': abs(diff * qty_available), + })], } - move_id = move_obj.create(cr, uid, move_vals, context=context) - move_obj.post(cr, uid, [move_id], context=context) - self.write(cr, uid, rec_id, {'standard_price': new_price}) + move = AccountMove.create(move_vals) + move.post() + + self.write({'standard_price': new_price}) return True -class product_category(osv.osv): + +class ProductCategory(models.Model): _inherit = 'product.category' - _columns = { - 'property_valuation': fields.property( - type='selection', - selection=[('manual_periodic', 'Periodic (manual)'), - ('real_time', 'Perpetual (automated)')], - string='Inventory Valuation', - required=True, copy=True, - help="If perpetual valuation is enabled for a product, the system " - "will automatically create journal entries corresponding to " - "stock moves, with product price as specified by the 'Costing " - "Method'. The inventory variation account set on the product " - "category will represent the current inventory value, and the " - "stock input and stock output account will hold the counterpart " - "moves for incoming and outgoing products."), - 'property_cost_method': fields.property( - type='selection', - selection=[('standard', 'Standard Price'), - ('average', 'Average Price'), - ('real', 'Real Price')], - string="Costing Method", - required=True, copy=True, - help="Standard Price: The cost price is manually updated at the end " - "of a specific period (usually once a year).\nAverage Price: " - "The cost price is recomputed at each incoming shipment and " - "used for the product valuation.\nReal Price: The cost price " - "displayed is the price of the last outgoing product (will be " - "used in case of inventory loss for example)."""), - 'property_stock_journal': fields.property( - relation='account.journal', - type='many2one', - string='Stock Journal', - help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."), - 'property_stock_account_input_categ_id': fields.property( - type='many2one', - relation='account.account', - string='Stock Input Account', - domain=[('deprecated', '=', False)], oldname="property_stock_account_input_categ", - help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless " - "there is a specific valuation account set on the source location. This is the default value for all products in this category. It " - "can also directly be set on each product"), - 'property_stock_account_output_categ_id': fields.property( - type='many2one', - relation='account.account', - domain=[('deprecated', '=', False)], - string='Stock Output Account', oldname="property_stock_account_output_categ", - help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless " - "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It " - "can also directly be set on each product"), - 'property_stock_valuation_account_id': fields.property( - type='many2one', - relation='account.account', - string="Stock Valuation Account", - domain=[('deprecated', '=', False)], - help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",), - } + + property_valuation = fields.Selection([ + ('manual_periodic', 'Periodic (manual)'), + ('real_time', 'Perpetual (automated)')], string='Inventory Valuation', + company_dependent=True, copy=True, required=True, + help="If perpetual valuation is enabled for a product, the system " + "will automatically create journal entries corresponding to " + "stock moves, with product price as specified by the 'Costing " + "Method'. The inventory variation account set on the product " + "category will represent the current inventory value, and the " + "stock input and stock output account will hold the counterpart " + "moves for incoming and outgoing products.") + property_cost_method = fields.Selection([ + ('standard', 'Standard Price'), + ('average', 'Average Price'), + ('real', 'Real Price')], string="Costing Method", + company_dependent=True, copy=True, required=True, + help="Standard Price: The cost price is manually updated at the end " + "of a specific period (usually once a year).\nAverage Price: " + "The cost price is recomputed at each incoming shipment and " + "used for the product valuation.\nReal Price: The cost price " + "displayed is the price of the last outgoing product (will be " + "used in case of inventory loss for example).""") + property_stock_journal = fields.Many2one( + 'account.journal', 'Stock Journal', company_dependent=True, + help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed.") + property_stock_account_input_categ_id = fields.Many2one( + 'account.account', 'Stock Input Account', company_dependent=True, + domain=[('deprecated', '=', False)], oldname="property_stock_account_input_categ", + help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless " + "there is a specific valuation account set on the source location. This is the default value for all products in this category. It " + "can also directly be set on each product") + property_stock_account_output_categ_id = fields.Many2one( + 'account.account', 'Stock Output Account', company_dependent=True, + domain=[('deprecated', '=', False)], oldname="property_stock_account_output_categ", + help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless " + "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It " + "can also directly be set on each product") + property_stock_valuation_account_id = fields.Many2one( + 'account.account', 'Stock Valuation Account', company_dependent=True, + domain=[('deprecated', '=', False)], + help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",) diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index c3696213c0a..a9c9d430837 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -1,57 +1,56 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp.tools import float_compare, float_round -from openerp.tools.translate import _ -from openerp import SUPERUSER_ID, api, models -from openerp.exceptions import UserError +from collections import defaultdict + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +from odoo.tools import float_compare, float_round + import logging _logger = logging.getLogger(__name__) -class stock_inventory(osv.osv): +class StockInventory(models.Model): _inherit = "stock.inventory" - _columns = { - 'accounting_date': fields.date('Force Accounting Date', help="Choose the accounting date at which you want to value the stock moves created by the inventory instead of the default one (the inventory end date)"), - } - def post_inventory(self, cr, uid, ids, context=None): - if context is None: - context = {} - ctx = context.copy() - inv = self.browse(cr, uid, ids, context=context)[0] - if inv.accounting_date: - ctx['force_period_date'] = inv.accounting_date - return super(stock_inventory, self).post_inventory(cr, uid, ids, context=ctx) + accounting_date = fields.Date( + 'Force Accounting Date', + help="Choose the accounting date at which you want to value the stock " + "moves created by the inventory instead of the default one (the " + "inventory end date)") + + @api.multi + def post_inventory(self): + acc_inventories = self.filtered(lambda inventory: inventory.accounting_date) + for inventory in acc_inventories: + res = super(StockInventory, inventory.with_context(force_period_date=inventory.accounting_date)).post_inventory() + other_inventories = self - acc_inventories + if other_inventories: + res = super(StockInventory, other_inventories).post_inventory() + return res -#---------------------------------------------------------- -# Stock Location -#---------------------------------------------------------- - -class stock_location(osv.osv): +class StockLocation(models.Model): _inherit = "stock.location" - _columns = { - 'valuation_in_account_id': fields.many2one('account.account', 'Stock Valuation Account (Incoming)', domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)], - help="Used for real-time inventory valuation. When set on a virtual location (non internal type), " - "this account will be used to hold the value of products being moved from an internal location " - "into this location, instead of the generic Stock Output Account set on the product. " - "This has no effect for internal locations."), - 'valuation_out_account_id': fields.many2one('account.account', 'Stock Valuation Account (Outgoing)', domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)], - help="Used for real-time inventory valuation. When set on a virtual location (non internal type), " - "this account will be used to hold the value of products being moved out of this location " - "and into an internal location, instead of the generic Stock Output Account set on the product. " - "This has no effect for internal locations."), - } + valuation_in_account_id = fields.Many2one( + 'account.account', 'Stock Valuation Account (Incoming)', + domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)], + help="Used for real-time inventory valuation. When set on a virtual location (non internal type), " + "this account will be used to hold the value of products being moved from an internal location " + "into this location, instead of the generic Stock Output Account set on the product. " + "This has no effect for internal locations.") + valuation_out_account_id = fields.Many2one( + 'account.account', 'Stock Valuation Account (Outgoing)', + domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)], + help="Used for real-time inventory valuation. When set on a virtual location (non internal type), " + "this account will be used to hold the value of products being moved out of this location " + "and into an internal location, instead of the generic Stock Output Account set on the product. " + "This has no effect for internal locations.") -#---------------------------------------------------------- -# Quants -#---------------------------------------------------------- - -class stock_quant(osv.osv): +class StockQuant(models.Model): _inherit = "stock.quant" @api.multi @@ -59,90 +58,82 @@ class stock_quant(osv.osv): real_value_quants = self.filtered(lambda quant: quant.product_id.cost_method == 'real') for quant in real_value_quants: quant.inventory_value = quant.cost * quant.qty - other = self - real_value_quants - return super(stock_quant, other)._compute_inventory_value() + return super(StockQuant, self - real_value_quants)._compute_inventory_value() - @api.cr_uid_ids_context - def _price_update(self, cr, uid, quant_ids, newprice, context=None): - ''' 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 - ''' - if context is None: - context = {} - account_move_obj = self.pool['account.move'] - super(stock_quant, self)._price_update(cr, uid, quant_ids, newprice, context=context) - for quant in self.browse(cr, uid, quant_ids, context=context): + @api.multi + def _price_update(self, newprice): + ''' 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 ''' + super(StockQuant, self)._price_update(newprice) + for quant in self: move = quant._get_latest_move() valuation_update = newprice - quant.cost # this is where we post accounting entries for adjustment, if needed - if not quant.company_id.currency_id.is_zero(valuation_update): - # If neg quant period already closed (likely with manual valuation), skip update - if account_move_obj._check_lock_date(cr, uid, [move.id], context=context): - ctx = dict(context, force_valuation_amount=valuation_update) - self._account_entry_move(cr, uid, [quant], move, context=ctx) + # If neg quant period already closed (likely with manual valuation), skip update + if not quant.company_id.currency_id.is_zero(valuation_update) and move._check_lock_date(): + quant.with_context(force_valuation_amount=valuation_update)._account_entry_move(move) - #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 - #1) the product cost's method is 'real' - #2) we just fixed a negative quant caused by an outgoing shipment + # 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 + # 1) the product cost's method is 'real' + # 2) we just fixed a negative quant caused by an outgoing shipment if quant.product_id.cost_method == 'real' and quant.location_id.usage != 'internal': - self.pool.get('stock.move')._store_average_cost_price(cr, uid, [move.id], context=context) + move._store_average_cost_price() - def _account_entry_move(self, cr, uid, ids, move, context=None): - """ - Accounting Valuation Entries + def _account_entry_move(self, move): + """ Accounting Valuation Entries """ + if move.product_id.type != 'product' or move.product_id.valuation != 'real_time': + # no stock valuation for consumable products + return False + if any(quant.owner_id or quant.qty <= 0 for quant in self): + # if the quant isn't owned by the company, we don't make any valuation en + # we don't make any stock valuation for negative quants because the valuation is already made for the counterpart. + # At that time the valuation will be made at the product cost price and afterward there will be new accounting entries + # to make the adjustments when we know the real cost price. + return False - 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) - move: Move to use. browse record - """ - quants = self.browse(cr, uid, ids, context=context) - if context is None: - context = {} - location_obj = self.pool.get('stock.location') location_from = move.location_id - location_to = quants[0].location_id - company_from = location_from and (location_from.usage == 'internal') and location_from.company_id or False + location_to = self[0].location_id # TDE FIXME: as the accounting is based on this value, should probably check all location_to to be the same + company_from = location_from.usage == 'internal' and location_from.company_id or False company_to = location_to and (location_to.usage == 'internal') and location_to.company_id or False - if move.product_id.valuation != 'real_time': - return False - if move.product_id.type != 'product': - #No stock valuation for consumable products - return False - for q in quants: - if q.owner_id: - #if the quant isn't owned by the company, we don't make any valuation entry - return False - if q.qty <= 0: - #we don't make any stock valuation for negative quants because the valuation is already made for the counterpart. - #At that time the valuation will be made at the product cost price and afterward there will be new accounting entries - #to make the adjustments when we know the real cost price. - return False - - #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 - # Create Journal Entry for products arriving in the company + # Create Journal Entry for products arriving in the company; 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 if company_to and (move.location_id.usage not in ('internal', 'transit') and move.location_dest_id.usage == 'internal' or company_from != company_to): - ctx = context.copy() - ctx['force_company'] = company_to.id - journal_id, acc_src, acc_dest, acc_valuation = self.pool['stock.move']._get_accounting_data_for_valuation(cr, uid, [move.id], context=ctx) - if location_from and location_from.usage == 'customer': - #goods returned from customer - self._create_account_move_line(cr, uid, quants.ids, move, acc_dest, acc_valuation, journal_id, context=ctx) + journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation() + if location_from and location_from.usage == 'customer': # goods returned from customer + self.with_context(force_company=company_to.id)._create_account_move_line(move, acc_dest, acc_valuation, journal_id) else: - self._create_account_move_line(cr, uid, quants.ids, move, acc_src, acc_valuation, journal_id, context=ctx) + self.with_context(force_company=company_to.id)._create_account_move_line(move, acc_src, acc_valuation, journal_id) # Create Journal Entry for products leaving the company if company_from and (move.location_id.usage == 'internal' and move.location_dest_id.usage not in ('internal', 'transit') or company_from != company_to): - ctx = context.copy() - ctx['force_company'] = company_from.id - journal_id, acc_src, acc_dest, acc_valuation = self.pool['stock.move']._get_accounting_data_for_valuation(cr, uid, [move.id], context=ctx) - if location_to and location_to.usage == 'supplier': - #goods returned to supplier - self._create_account_move_line(cr, uid, quants.ids, move, acc_valuation, acc_src, journal_id, context=ctx) + journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation() + if location_to and location_to.usage == 'supplier': # goods returned to supplier + self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_src, journal_id) else: - self._create_account_move_line(cr, uid, quants.ids, move, acc_valuation, acc_dest, journal_id, context=ctx) + self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_dest, journal_id) - def _quant_create_from_move(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): - quant_obj = self.pool.get('stock.quant') - quant = super(stock_quant, self)._quant_create_from_move(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) + def _create_account_move_line(self, move, credit_account_id, debit_account_id, journal_id): + # group quants by cost + quant_cost_qty = defaultdict(lambda: 0.0) + for quant in self: + quant_cost_qty[quant.cost] += quant.qty + + AccountMove = self.env['account.move'] + for cost, qty in quant_cost_qty.iteritems(): + move_lines = move._prepare_account_move_line(qty, cost, credit_account_id, debit_account_id) + date = self._context.get('force_period_date', fields.Date.context_today(self)) + new_account_move = AccountMove.create({ + 'journal_id': journal_id, + 'line_ids': move_lines, + 'date': date, + 'ref': move.picking_id.name}) + new_account_move.post() + + def _quant_create_from_move(self, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False): + quant = super(StockQuant, self)._quant_create_from_move(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) quant._account_entry_move(move) if move.product_id.valuation == 'real_time': # If the precision required for the variable quant cost is larger than the accounting @@ -161,217 +152,178 @@ class stock_quant(osv.osv): if float_compare(quant.product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\ and float_compare(quant.qty * quant.cost, quant.qty * cost_rounded, precision_rounding=curr_rounding) != 0\ and float_compare(quant.qty, 2.0, precision_rounding=quant.product_id.uom_id.rounding) >= 0: - qty = quant.qty - cost = quant.cost quant_correct = quant._quant_split(quant.qty - 1.0) - cost_correct += (qty * cost) - (qty * cost_rounded) - quant_obj.write(cr, SUPERUSER_ID, [quant.id], {'cost': cost_rounded}, context=context) - quant_obj.write(cr, SUPERUSER_ID, [quant_correct.id], {'cost': cost_correct}, context=context) + cost_correct += (quant.qty * quant.cost) - (quant.qty * cost_rounded) + quant.sudo().write({'cost': cost_rounded}) + quant_correct.sudo().write({'cost': cost_correct}) return quant - @api.multi def _quant_update_from_move(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False): - res = super(stock_quant, self)._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=lot_id, entire_pack=entire_pack) + res = super(StockQuant, self)._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=lot_id, entire_pack=entire_pack) self._account_entry_move(move) return res - def _create_account_move_line(self, cr, uid, ids, move, credit_account_id, debit_account_id, journal_id, context=None): - quants = self.browse(cr, uid, ids, context=context) - # group quants by cost - quant_cost_qty = {} - for quant in quants: - if quant_cost_qty.get(quant.cost): - quant_cost_qty[quant.cost] += quant.qty - else: - quant_cost_qty[quant.cost] = quant.qty - move_obj = self.pool.get('account.move') - for cost, qty in quant_cost_qty.items(): - move_lines = self.pool['stock.move']._prepare_account_move_line(cr, uid, [move.id], qty, cost, credit_account_id, debit_account_id, context=context) - date = context.get('force_period_date', fields.date.context_today(self, cr, uid, context=context)) - new_move = move_obj.create(cr, uid, {'journal_id': journal_id, - 'line_ids': move_lines, - 'date': date, - 'ref': move.picking_id.name}, context=context) - move_obj.post(cr, uid, [new_move], context=context) - -class stock_move(osv.osv): +class StockMove(models.Model): _inherit = "stock.move" - def action_done(self, cr, uid, ids, context=None): - self.product_price_update_before_done(cr, uid, ids, context=context) - res = super(stock_move, self).action_done(cr, uid, ids, context=context) - self.product_price_update_after_done(cr, uid, ids, context=context) + @api.multi + def action_done(self): + self.product_price_update_before_done() + res = super(StockMove, self).action_done() + self.product_price_update_after_done() return res - def _store_average_cost_price(self, cr, uid, ids, context=None): - ''' move is a browe record ''' - move = self.browse(cr, uid, ids, context=context)[0] - product_obj = self.pool.get('product.product') - if any([q.qty <= 0 for q in move.quant_ids]) or move.product_qty == 0: - #if there is a negative quant, the standard price shouldn't be updated - return - #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 valuate it at 6€ - average_valuation_price = 0.0 - for q in move.quant_ids: - average_valuation_price += q.qty * q.cost - average_valuation_price = average_valuation_price / move.product_qty - # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products - ctx = dict(context or {}, force_company=move.company_id.id) - product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=ctx) - self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context) + @api.multi + def product_price_update_before_done(self): + tmpl_dict = defaultdict(lambda: 0.0) + # adapt standard price on incomming moves if the product cost_method is 'average' + for move in self.filtered(lambda move: move.location_id.usage == 'supplier' and move.product_id.cost_method == 'average'): + product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id] - def product_price_update_before_done(self, cr, uid, ids, context=None): - product_obj = self.pool.get('product.product') - tmpl_dict = {} - for move in self.browse(cr, uid, ids, context=context): - #adapt standard price on incomming moves if the product cost_method is 'average' - if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'): - product = move.product_id - product_id = move.product_id.id - qty_available = move.product_id.qty_available - if tmpl_dict.get(product_id): - product_avail = qty_available + tmpl_dict[product_id] - else: - tmpl_dict[product_id] = 0 - product_avail = qty_available - # if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use. - price_unit = move.get_price_unit() - if product_avail <= 0: - new_std_price = price_unit - else: - # Get the standard price - amount_unit = product.standard_price - new_std_price = ((amount_unit * product_avail) + (price_unit * move.product_qty)) / (product_avail + move.product_qty) - tmpl_dict[product_id] += move.product_qty - # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products - ctx = dict(context or {}, force_company=move.company_id.id) - product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=ctx) + # if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use. + if product_tot_qty_available <= 0: + new_std_price = move.get_price_unit() + else: + # Get the standard price + amount_unit = move.product_id.standard_price + new_std_price = ((amount_unit * product_tot_qty_available) + (move.get_price_unit() * move.product_qty)) / (product_tot_qty_available + move.product_qty) - def product_price_update_after_done(self, cr, uid, ids, context=None): - ''' - This method adapts the price on the product when necessary - ''' - for move in self.browse(cr, uid, ids, context=context): - #adapt standard price on outgoing moves if the product cost_method is 'real', so that a return - #or an inventory loss is made using the last value used for an outgoing valuation. - if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal': - #store the average price of the move on the move and product form - self._store_average_cost_price(cr, uid, [move.id], context=context) + tmpl_dict[move.product_id.id] += move.product_qty + # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products + move.product_id.with_context(force_company=move.company_id.id).write({'standard_price': new_std_price}) - def _get_accounting_data_for_valuation(self, cr, uid, ids, context=None): - """ - Return the accounts and journal to use to post Journal Entries for the real-time - valuation of the quant. + @api.multi + def product_price_update_after_done(self): + ''' Adapt standard price on outgoing moves if the product cost_method is 'real', so that a + return or an inventory loss is made using the last value used for an outgoing valuation. ''' + to_update_moves = self.filtered(lambda move: move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal') + to_update_moves._store_average_cost_price() - :param context: context dictionary that can explicitly mention the company to consider via the 'force_company' key - :returns: journal_id, source account, destination account, valuation account - :raise: openerp.exceptions.UserError if any mandatory account or journal is not defined. - """ - move = self.browse(cr, uid, ids, context=context)[0] - product_obj = self.pool.get('product.template') - accounts = product_obj.browse(cr, uid, move.product_id.product_tmpl_id.id, context).get_product_accounts() - if move.location_id.valuation_out_account_id: - acc_src = move.location_id.valuation_out_account_id.id + def _store_average_cost_price(self): + """ Store the average price of the move on the move and product form """ + for move in self: + # product_obj = self.pool.get('product.product') + if any(q.qty <= 0 for q in move.quant_ids) or move.product_qty == 0: + # if there is a negative quant, the standard price shouldn't be updated + return + # 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 valuate it at 6€ + valuation_price = sum(q.qty * q.cost for q in move.quant_ids) + average_valuation_price = valuation_price / move.product_qty + + move.product_id.with_context(force_company=move.company_id.id).sudo().write({'standard_price': average_valuation_price}) + move.write({'price_unit': average_valuation_price}) + + @api.multi + def _get_accounting_data_for_valuation(self): + """ Return the accounts and journal to use to post Journal Entries for + the real-time valuation of the quant. """ + self.ensure_one() + accounts_data = self.product_id.product_tmpl_id.get_product_accounts() + + if self.location_id.valuation_out_account_id: + acc_src = self.location_id.valuation_out_account_id.id else: - acc_src = accounts['stock_input'].id + acc_src = accounts_data['stock_input'].id - if move.location_dest_id.valuation_in_account_id: - acc_dest = move.location_dest_id.valuation_in_account_id.id + if self.location_dest_id.valuation_in_account_id: + acc_dest = self.location_dest_id.valuation_in_account_id.id else: - acc_dest = accounts['stock_output'].id + acc_dest = accounts_data['stock_output'].id - acc_valuation = accounts.get('stock_valuation', False) + acc_valuation = accounts_data.get('stock_valuation', False) if acc_valuation: acc_valuation = acc_valuation.id - if not accounts.get('stock_journal', False): + if not accounts_data.get('stock_journal', False): raise UserError(_('You don\'t have any stock journal defined on your product category, check if you have installed a chart of accounts')) if not acc_src: - raise UserError(_('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (move.product_id.name)) + raise UserError(_('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.name)) if not acc_dest: - raise UserError(_('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (move.product_id.name)) + raise UserError(_('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.name)) if not acc_valuation: raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.')) - journal_id = accounts['stock_journal'].id + journal_id = accounts_data['stock_journal'].id return journal_id, acc_src, acc_dest, acc_valuation - def _prepare_account_move_line(self, cr, uid, ids, qty, cost, credit_account_id, debit_account_id, context=None): + def _prepare_account_move_line(self, qty, cost, credit_account_id, debit_account_id): """ Generate the account.move.line values to post to track the stock valuation difference due to the processing of the given quant. """ - move = self.browse(cr, uid, ids, context=context)[0] - if context is None: - context = {} - currency_obj = self.pool.get('res.currency') - if context.get('force_valuation_amount'): - valuation_amount = context.get('force_valuation_amount') + self.ensure_one() + + if self._context.get('force_valuation_amount'): + valuation_amount = self._context.get('force_valuation_amount') else: - if move.product_id.cost_method == 'average': - valuation_amount = cost if move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'internal' else move.product_id.standard_price + if self.product_id.cost_method == 'average': + valuation_amount = cost if self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'internal' else self.product_id.standard_price else: - valuation_amount = cost if move.product_id.cost_method == 'real' else move.product_id.standard_price - #the standard_price of the product may be in another decimal precision, or not compatible with the coinage of - #the company currency... so we need to use round() before creating the accounting entries. - debit_value = currency_obj.round(cr, uid, move.company_id.currency_id, valuation_amount * qty) - #check that all data is correct - if move.company_id.currency_id.is_zero(debit_value): - raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (move.product_id.name,)) + valuation_amount = cost if self.product_id.cost_method == 'real' else self.product_id.standard_price + # the standard_price of the product may be in another decimal precision, or not compatible with the coinage of + # the company currency... so we need to use round() before creating the accounting entries. + debit_value = self.company_id.currency_id.round(valuation_amount * qty) + + # check that all data is correct + if self.company_id.currency_id.is_zero(debit_value): + raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (self.product_id.name,)) credit_value = debit_value - if move.product_id.cost_method == 'average' and move.company_id.anglo_saxon_accounting: - #in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input - #account books the real purchase price, while the stock account books the average price. The difference is - #booked in the dedicated price difference account. - if move.location_dest_id.usage == 'supplier' and move.origin_returned_move_id and move.origin_returned_move_id.purchase_line_id: - debit_value = move.origin_returned_move_id.price_unit * qty - #in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation - #is made using the original average price to negate the delivery effect. - if move.location_id.usage == 'customer' and move.origin_returned_move_id: - debit_value = move.origin_returned_move_id.price_unit * qty + if self.product_id.cost_method == 'average' and self.company_id.anglo_saxon_accounting: + # in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input + # account books the real purchase price, while the stock account books the average price. The difference is + # booked in the dedicated price difference account. + if self.location_dest_id.usage == 'supplier' and self.origin_returned_move_id and self.origin_returned_move_id.purchase_line_id: + debit_value = self.origin_returned_move_id.price_unit * qty + # in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation + # is made using the original average price to negate the delivery effect. + if self.location_id.usage == 'customer' and self.origin_returned_move_id: + debit_value = self.origin_returned_move_id.price_unit * qty credit_value = debit_value - partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False + partner_id = (self.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(self.picking_id.partner_id).id) or False debit_line_vals = { - 'name': move.name, - 'product_id': move.product_id.id, - 'quantity': qty, - 'product_uom_id': move.product_id.uom_id.id, - 'ref': move.picking_id and move.picking_id.name or False, - 'partner_id': partner_id, - 'debit': debit_value, - 'credit': 0, - 'account_id': debit_account_id, + 'name': self.name, + 'product_id': self.product_id.id, + 'quantity': qty, + 'product_uom_id': self.product_id.uom_id.id, + 'ref': self.picking_id.name, + 'partner_id': partner_id, + 'debit': debit_value, + 'credit': 0, + 'account_id': debit_account_id, } credit_line_vals = { - 'name': move.name, - 'product_id': move.product_id.id, - 'quantity': qty, - 'product_uom_id': move.product_id.uom_id.id, - 'ref': move.picking_id and move.picking_id.name or False, - 'partner_id': partner_id, - 'credit': credit_value, - 'debit': 0, - 'account_id': credit_account_id, + 'name': self.name, + 'product_id': self.product_id.id, + 'quantity': qty, + 'product_uom_id': self.product_id.uom_id.id, + 'ref': self.picking_id.name, + 'partner_id': partner_id, + 'credit': credit_value, + 'debit': 0, + 'account_id': credit_account_id, } res = [(0, 0, debit_line_vals), (0, 0, credit_line_vals)] if credit_value != debit_value: - #for supplier returns of product in average costing method, in anglo saxon mode + # for supplier returns of product in average costing method, in anglo saxon mode diff_amount = debit_value - credit_value - price_diff_account = move.product_id.property_account_creditor_price_difference + price_diff_account = self.product_id.property_account_creditor_price_difference if not price_diff_account: - price_diff_account = move.product_id.categ_id.property_account_creditor_price_difference_categ + price_diff_account = self.product_id.categ_id.property_account_creditor_price_difference_categ if not price_diff_account: raise UserError(_('Configuration error. Please configure the price difference account on the product or its category to process this operation.')) price_diff_line = { - 'name': move.name, - 'product_id': move.product_id.id, - 'quantity': qty, - 'product_uom_id': move.product_id.uom_id.id, - 'ref': move.picking_id and move.picking_id.name or False, - 'partner_id': partner_id, - 'credit': diff_amount > 0 and diff_amount or 0, - 'debit': diff_amount < 0 and -diff_amount or 0, - 'account_id': price_diff_account.id, + 'name': self.name, + 'product_id': self.product_id.id, + 'quantity': qty, + 'product_uom_id': self.product_id.uom_id.id, + 'ref': self.picking_id.name, + 'partner_id': partner_id, + 'credit': diff_amount > 0 and diff_amount or 0, + 'debit': diff_amount < 0 and -diff_amount or 0, + 'account_id': price_diff_account.id, } res.append((0, 0, price_diff_line)) return res diff --git a/addons/stock_account/models/stock_config_settings.py b/addons/stock_account/models/stock_config_settings.py index 1a52143257c..d782cce0615 100644 --- a/addons/stock_account/models/stock_config_settings.py +++ b/addons/stock_account/models/stock_config_settings.py @@ -1,29 +1,23 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv +from odoo import api, fields, models - -class stock_config_settings(osv.osv_memory): +class StockConfigSettings(models.TransientModel): _inherit = 'stock.config.settings' - _columns = { - 'group_stock_inventory_valuation': fields.selection([ - (0, "Periodic inventory valuation (recommended)"), - (1, 'Perpetual inventory valuation (stock move generates accounting entries)') - ], "Inventory Valuation", - implied_group='stock_account.group_inventory_valuation', - help="""Allows to configure inventory valuations on products and product categories."""), - 'module_stock_landed_costs': fields.selection([ - (0, 'No landed costs'), - (1, 'Include landed costs in product costing computation') - ], "Landed Costs", - help="""Install the module that allows to affect landed costs on pickings, and split them onto the different products."""), - } + group_stock_inventory_valuation = fields.Selection([ + (0, "Periodic inventory valuation (recommended)"), + (1, 'Perpetual inventory valuation (stock move generates accounting entries)')], + "Inventory Valuation", implied_group='stock_account.group_inventory_valuation', + help="""Allows to configure inventory valuations on products and product categories.""") + module_stock_landed_costs = fields.Selection([ + (0, 'No landed costs'), + (1, 'Include landed costs in product costing computation')], "Landed Costs", + help="""Install the module that allows to affect landed costs on pickings, and split them onto the different products.""") - - def onchange_landed_costs(self, cr, uid, ids, module_landed_costs, context=None): - if module_landed_costs: - return {'value': {'group_stock_inventory_valuation': True}} - return {} + @api.onchange('module_stock_landed_costs') + def onchange_landed_costs(self): + if self.module_stock_landed_costs: + self.group_stock_inventory_valuation = 1 diff --git a/addons/stock_account/report/stock_history.py b/addons/stock_account/report/stock_history.py index 26cf3c69d6c..716ec6248cc 100644 --- a/addons/stock_account/report/stock_history.py +++ b/addons/stock_account/report/stock_history.py @@ -1,92 +1,74 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime -from openerp import tools -from openerp.osv import fields, osv -from openerp.tools.translate import _ +from odoo import api, fields, models, tools -class stock_history(osv.osv): +class StockHistory(models.Model): _name = 'stock.history' _auto = False _order = 'date asc' - def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True): - res = super(stock_history, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy) - if context is None: - context = {} - date = context.get('history_date', datetime.now()) + move_id = fields.Many2one('stock.move', 'Stock Move', required=True) + location_id = fields.Many2one('stock.location', 'Location', required=True) + company_id = fields.Many2one('res.company', 'Company') + product_id = fields.Many2one('product.product', 'Product', required=True) + product_categ_id = fields.Many2one('product.category', 'Product Category', required=True) + quantity = fields.Float('Product Quantity') + date = fields.Datetime('Operation Date') + price_unit_on_quant = fields.Float('Value') + inventory_value = fields.Float('Inventory Value', compute='_compute_inventory_value', readonly=True) + source = fields.Char('Source') + product_template_id = fields.Many2one('product.template', 'Product Template', required=True) + serial_number = fields.Char('Lot/Serial Number', required=True) + + @api.model + def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + res = super(StockHistory, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) if 'inventory_value' in fields: + date = self._context.get('history_date', fields.Datetime.now()) + stock_history = self.env['stock.history'] group_lines = {} for line in res: domain = line.get('__domain', domain) - group_lines.setdefault(str(domain), self.search(cr, uid, domain, context=context)) - line_ids = set() - for ids in group_lines.values(): - for product_id in ids: - line_ids.add(product_id) - line_ids = list(line_ids) - lines_rec = {} - if line_ids: - cr.execute('SELECT id, product_id, price_unit_on_quant, company_id, quantity FROM stock_history WHERE id in %s', (tuple(line_ids),)) - lines_rec = cr.dictfetchall() - lines_dict = dict((line['id'], line) for line in lines_rec) - product_ids = list(set(line_rec['product_id'] for line_rec in lines_rec)) - products_rec = self.pool['product.product'].read(cr, uid, product_ids, ['cost_method', 'id'], context=context) - products_dict = dict((product['id'], product) for product in products_rec) - cost_method_product_ids = list(set(product['id'] for product in products_rec if product['cost_method'] != 'real')) - histories = [] - if cost_method_product_ids: - cr.execute('SELECT DISTINCT ON (product_id, company_id) product_id, company_id, cost FROM product_price_history WHERE product_id in %s AND datetime <= %s ORDER BY product_id, company_id, datetime DESC', (tuple(cost_method_product_ids), date)) - histories = cr.dictfetchall() + group_lines.setdefault(str(domain), self.search(domain)) + stock_history |= group_lines[str(domain)] + histories_dict = {} - for history in histories: - histories_dict[(history['product_id'], history['company_id'])] = history['cost'] + not_real_cost_method_products = stock_history.mapped('product_id').filtered(lambda product: product.cost_method != 'real') + if not_real_cost_method_products: + self._cr.execute("""SELECT DISTINCT ON (product_id, company_id) product_id, company_id, cost + FROM product_price_history + WHERE product_id in %s AND datetime <= %s + ORDER BY product_id, company_id, datetime DESC""", (not_real_cost_method_products.ids, date)) + for history in self._cr.dictfetchall(): + histories_dict[(history['product_id'], history['company_id'])] = history['cost'] + for line in res: inv_value = 0.0 - lines = group_lines.get(str(line.get('__domain', domain))) - for line_id in lines: - line_rec = lines_dict[line_id] - product = products_dict[line_rec['product_id']] - if product['cost_method'] == 'real': - price = line_rec['price_unit_on_quant'] + for stock_history in group_lines.get(str(line.get('__domain', domain))): + product = stock_history.product_id + if product.cost_method == 'real': + price = stock_history.price_unit_on_quant else: - price = histories_dict.get((product['id'], line_rec['company_id']), 0.0) - inv_value += price * line_rec['quantity'] + price = histories_dict.get((product.id, stock_history.company_id.id), 0.0) + inv_value += price * stock_history.quantity line['inventory_value'] = inv_value + return res - def _get_inventory_value(self, cr, uid, ids, name, attr, context=None): - if context is None: - context = {} - date = context.get('history_date') - product_obj = self.pool.get("product.product") - res = {} - for line in self.browse(cr, uid, ids, context=context): - if line.product_id.cost_method == 'real': - res[line.id] = line.quantity * line.price_unit_on_quant - else: - res[line.id] = line.quantity * product_obj.get_history_price(cr, uid, [line.product_id.id], line.company_id.id, date=date, context=context) - return res + @api.one + def _compute_inventory_value(self): + if self.product_id.cost_method == 'real': + self.inventory_value = self.quantity * self.price_unit_on_quant + else: + self.inventory_value = self.quantity * self.product_id.get_history_price(self.company_id.id, date=self._context.get('history_date', fields.Datetime.now())) - _columns = { - 'move_id': fields.many2one('stock.move', 'Stock Move', required=True), - 'location_id': fields.many2one('stock.location', 'Location', required=True), - 'company_id': fields.many2one('res.company', 'Company'), - 'product_id': fields.many2one('product.product', 'Product', required=True), - 'product_categ_id': fields.many2one('product.category', 'Product Category', required=True), - 'quantity': fields.float('Product Quantity'), - 'date': fields.datetime('Operation Date'), - 'price_unit_on_quant': fields.float('Value'), - 'inventory_value': fields.function(_get_inventory_value, string="Inventory Value", type='float', readonly=True), - 'source': fields.char('Source'), - 'product_template_id': fields.many2one('product.template', 'Product Template', required=True), - 'serial_number': fields.char('Lot/Serial Number', required=True), - } - - def init(self, cr): - tools.drop_view_if_exists(cr, 'stock_history') - cr.execute(""" - CREATE OR REPLACE VIEW stock_history AS ( + @api.model_cr + def init(self): + tools.drop_view_if_exists(self._cr, 'stock_history') + self._cr.execute(""" + CREATE VIEW stock_history AS ( SELECT MIN(id) as id, move_id, location_id, diff --git a/addons/stock_account/report/stock_history_views.xml b/addons/stock_account/report/stock_history_views.xml index b4902c1759b..888ac577e26 100644 --- a/addons/stock_account/report/stock_history_views.xml +++ b/addons/stock_account/report/stock_history_views.xml @@ -1,7 +1,5 @@ - - - + Stock Value At Date stock.history @@ -60,6 +58,4 @@
- - - + diff --git a/addons/stock_account/security/stock_account_security.xml b/addons/stock_account/security/stock_account_security.xml index 7c30c4ab08d..838ffdf5fb2 100644 --- a/addons/stock_account/security/stock_account_security.xml +++ b/addons/stock_account/security/stock_account_security.xml @@ -1,5 +1,5 @@ - + @@ -18,5 +18,5 @@ - + diff --git a/addons/stock_account/test/stock_valuation_account.xml b/addons/stock_account/test/stock_valuation_account.xml index 4b9513fd916..40e61a96be1 100644 --- a/addons/stock_account/test/stock_valuation_account.xml +++ b/addons/stock_account/test/stock_valuation_account.xml @@ -1,5 +1,5 @@ - + @@ -18,4 +18,4 @@ - + diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml index 3c6b8072702..fe1ad3d92fc 100644 --- a/addons/stock_account/views/product_views.xml +++ b/addons/stock_account/views/product_views.xml @@ -1,5 +1,5 @@ - + @@ -99,4 +99,4 @@ - + diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml index d581eddcda9..bd817ab8bd3 100644 --- a/addons/stock_account/views/stock_account_views.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -1,5 +1,5 @@ - + @@ -39,4 +39,4 @@ - + diff --git a/addons/stock_account/views/stock_config_settings_views.xml b/addons/stock_account/views/stock_config_settings_views.xml index c106fd30081..f2e45662a48 100644 --- a/addons/stock_account/views/stock_config_settings_views.xml +++ b/addons/stock_account/views/stock_config_settings_views.xml @@ -1,5 +1,5 @@ - + stock.config.settings.inherit @@ -8,11 +8,11 @@ - + - + diff --git a/addons/stock_account/wizard/stock_change_standard_price.py b/addons/stock_account/wizard/stock_change_standard_price.py index 5c0b1ce9197..ccda1ee7ee5 100644 --- a/addons/stock_account/wizard/stock_change_standard_price.py +++ b/addons/stock_account/wizard/stock_change_standard_price.py @@ -1,71 +1,42 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp.tools.translate import _ -import openerp.addons.decimal_precision as dp +from odoo import api, fields, models +from odoo.addons import decimal_precision as dp -class change_standard_price(osv.osv_memory): + +class StockChangeStandardPrice(models.TransientModel): _name = "stock.change.standard.price" _description = "Change Standard Price" - _columns = { - 'new_price': fields.float('Price', required=True, digits_compute=dp.get_precision('Product Price'), - help="If cost price is increased, stock variation account will be debited " - "and stock output account will be credited with the value = (difference of amount * quantity available).\n" - "If cost price is decreased, stock variation account will be creadited and stock input account will be debited."), - 'counterpart_account_id': fields.many2one('account.account', string="Counter-Part Account", required=True, domain=[('deprecated', '=', False)]), - } + new_price = fields.Float( + 'Price', digits_compute=dp.get_precision('Product Price'), required=True, + help="If cost price is increased, stock variation account will be debited " + "and stock output account will be credited with the value = (difference of amount * quantity available).\n" + "If cost price is decreased, stock variation account will be creadited and stock input account will be debited.") + counterpart_account_id = fields.Many2one( + 'account.account', string="Counter-Part Account", + domain=[('deprecated', '=', False)], required=True) - def default_get(self, cr, uid, fields, context=None): - """ To get default values for the object. - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param fields: List of fields for which we want default values - @param context: A standard dictionary - @return: A dictionary which of fields with values. - """ - if context is None: - context = {} - if context.get("active_model") == 'product.product': - product_pool = self.pool.get('product.product') - else: - product_pool = self.pool.get('product.template') - product_obj = product_pool.browse(cr, uid, context.get('active_id', False)) + @api.model + def default_get(self, fields): + res = super(StockChangeStandardPrice, self).default_get(fields) - res = super(change_standard_price, self).default_get(cr, uid, fields, context=context) - - price = context.get('default_new_price') or product_obj.standard_price - - if 'new_price' in fields: - res.update({'new_price': price}) - if 'counterpart_account_id' in fields: - default_account = product_obj.property_account_expense_id or product_obj.categ_id.property_account_expense_categ_id - if default_account: - res.update({'counterpart_account_id': default_account.id}) + product_or_template = self.env[self._context['active_model']].browse(self._context['active_id']) + if 'new_price' in fields and 'default_new_price' not in res: + res['new_price'] = product_or_template.standard_price + if 'counterpart_account_id' in fields and 'counterpart_account_id' not in res: + res['counterpart_account_id'] = product_or_template.property_account_expense_id.id or product_or_template.categ_id.property_account_expense_categ_id.id return res - def change_price(self, cr, uid, ids, context=None): - """ Changes the Standard Price of Product. - And creates an account move accordingly. - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param ids: List of IDs selected - @param context: A standard dictionary - @return: - """ - if context is None: - context = {} - rec_id = context.get('active_id', False) - assert rec_id, _('Active ID is not set in Context.') - new_price = self.browse(cr, uid, ids, context=context)[0].new_price - if context.get("active_model") == 'product.template': - prod_obj = self.pool.get('product.template') - rec_ids = prod_obj.browse(cr, uid, rec_id, context=context).product_variant_ids.mapped('id') + @api.multi + def change_price(self): + """ Changes the Standard Price of Product and creates an account move accordingly. """ + self.ensure_one() + if self._context['active_model'] == 'product.template': + products = self.env['product.template'].browse(self._context['active_id']).product_variant_ids else: - rec_ids = [rec_id] - res = self.browse(cr, uid, ids, context=context)[0] - self.pool.get('product.product').do_change_standard_price(cr, uid, rec_ids, new_price, res.counterpart_account_id.id, context=context) + products = self.env['product.product'].browse(self._context['active_id']) + + products.do_change_standard_price(self.new_price, self.counterpart_account_id.id) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/stock_account/wizard/stock_change_standard_price_views.xml b/addons/stock_account/wizard/stock_change_standard_price_views.xml index ef1dd334fa1..806f6883b72 100644 --- a/addons/stock_account/wizard/stock_change_standard_price_views.xml +++ b/addons/stock_account/wizard/stock_change_standard_price_views.xml @@ -1,5 +1,5 @@ - + Change Standard Price @@ -34,5 +34,4 @@ - - + \ No newline at end of file diff --git a/addons/stock_account/wizard/wizard_valuation_history.py b/addons/stock_account/wizard/wizard_valuation_history.py index 7d192a2998e..889db533a83 100644 --- a/addons/stock_account/wizard/wizard_valuation_history.py +++ b/addons/stock_account/wizard/wizard_valuation_history.py @@ -1,31 +1,26 @@ -from openerp.osv import fields, osv -from openerp.tools.translate import _ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields, _ -class wizard_valuation_history(osv.osv_memory): - +class WizardValuationHistory(models.TransientModel): _name = 'wizard.valuation.history' _description = 'Wizard that opens the stock valuation history table' - _columns = { - 'choose_date': fields.boolean('Inventory at Date'), - 'date': fields.datetime('Date', required=True), - } - _defaults = { - 'choose_date': False, - 'date': fields.datetime.now, - } + choose_date = fields.Boolean('Inventory at Date') + date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - def open_table(self, cr, uid, ids, context=None): - if context is None: - context = {} - data = self.read(cr, uid, ids, context=context)[0] - ctx = context.copy() - ctx['history_date'] = data['date'] - ctx['search_default_group_by_product'] = True - ctx['search_default_group_by_location'] = True + @api.multi + def open_table(self): + self.ensure_one() + ctx = dict( + self._context, + history_date=self.date, + search_default_group_by_product=True, + search_default_group_by_location=True) return { - 'domain': "[('date', '<=', '" + data['date'] + "')]", + 'domain': "[('date', '<=', '" + self.date + "')]", 'name': _('Stock Value At Date'), 'view_type': 'form', 'view_mode': 'tree', diff --git a/addons/stock_account/wizard/wizard_valuation_history_views.xml b/addons/stock_account/wizard/wizard_valuation_history_views.xml index 5f35b8fcb35..bf65b78f0f6 100644 --- a/addons/stock_account/wizard/wizard_valuation_history_views.xml +++ b/addons/stock_account/wizard/wizard_valuation_history_views.xml @@ -1,5 +1,5 @@ - + Inventory at Date wizard.valuation.history @@ -31,4 +31,4 @@ new - \ No newline at end of file + From 61039df4b9d0e5e8b5bfc0ab818d7c934c3f1a2b Mon Sep 17 00:00:00 2001 From: Mansi Trivedi Date: Tue, 3 Nov 2015 12:46:23 +0530 Subject: [PATCH 12/20] [MOV] stock_calendar: reorganize module --- addons/stock_calendar/__init__.py | 4 +- addons/stock_calendar/__openerp__.py | 4 +- addons/stock_calendar/models/__init__.py | 6 +++ .../{ => models}/procurement.py | 0 .../stock_calendar/{ => models}/resource.py | 0 addons/stock_calendar/{ => models}/stock.py | 0 addons/stock_calendar/stock_calendar_view.xml | 40 ------------------- .../views/procurement_views.xml | 14 +++++++ .../stock_calendar/views/resource_views.xml | 14 +++++++ addons/stock_calendar/views/stock_views.xml | 16 ++++++++ 10 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 addons/stock_calendar/models/__init__.py rename addons/stock_calendar/{ => models}/procurement.py (100%) rename addons/stock_calendar/{ => models}/resource.py (100%) rename addons/stock_calendar/{ => models}/stock.py (100%) delete mode 100644 addons/stock_calendar/stock_calendar_view.xml create mode 100644 addons/stock_calendar/views/procurement_views.xml create mode 100644 addons/stock_calendar/views/resource_views.xml create mode 100644 addons/stock_calendar/views/stock_views.xml diff --git a/addons/stock_calendar/__init__.py b/addons/stock_calendar/__init__.py index 7a3be6818a8..81fc81d29c9 100644 --- a/addons/stock_calendar/__init__.py +++ b/addons/stock_calendar/__init__.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import stock -import procurement -import resource +import models diff --git a/addons/stock_calendar/__openerp__.py b/addons/stock_calendar/__openerp__.py index d6c763073ee..9d96e5c14e5 100644 --- a/addons/stock_calendar/__openerp__.py +++ b/addons/stock_calendar/__openerp__.py @@ -45,7 +45,9 @@ ], 'data': [ - 'stock_calendar_view.xml' + 'views/stock_views.xml', + 'views/procurement_views.xml', + 'views/resource_views.xml', ], 'test': [ 'test/orderpoint_calendar.yml' diff --git a/addons/stock_calendar/models/__init__.py b/addons/stock_calendar/models/__init__.py new file mode 100644 index 00000000000..03dad687433 --- /dev/null +++ b/addons/stock_calendar/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import procurement +import resource +import stock diff --git a/addons/stock_calendar/procurement.py b/addons/stock_calendar/models/procurement.py similarity index 100% rename from addons/stock_calendar/procurement.py rename to addons/stock_calendar/models/procurement.py diff --git a/addons/stock_calendar/resource.py b/addons/stock_calendar/models/resource.py similarity index 100% rename from addons/stock_calendar/resource.py rename to addons/stock_calendar/models/resource.py diff --git a/addons/stock_calendar/stock.py b/addons/stock_calendar/models/stock.py similarity index 100% rename from addons/stock_calendar/stock.py rename to addons/stock_calendar/models/stock.py diff --git a/addons/stock_calendar/stock_calendar_view.xml b/addons/stock_calendar/stock_calendar_view.xml deleted file mode 100644 index 86d3dcd5e29..00000000000 --- a/addons/stock_calendar/stock_calendar_view.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - stock.warehouse.orderpoint.form.inherit - stock.warehouse.orderpoint - - - - - - - - - - - - resource.calendar.attendance.tree.procurement - - resource.calendar.attendance - - - - - - - - - - procurement.order.form.stock.inherit - procurement.order - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -49,6 +46,5 @@ - From 7ad15c3827bda1d47314af0143f535cf5a6ca349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 12 Jul 2016 16:44:37 +0200 Subject: [PATCH 20/20] [IMP] sale_stock: small code improvements This module is already migrated. However some code could easily be improved or cleaned. --- addons/sale_stock/models/sale_order.py | 34 +++++++------------------- addons/sale_stock/models/stock.py | 26 ++++++++------------ 2 files changed, 19 insertions(+), 41 deletions(-) diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 88accc4c053..57ba77d43b5 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -51,34 +51,20 @@ class SaleOrder(models.Model): of given sales order ids. It can either be a in a list or in a form view, if there is only one delivery order to show. ''' - action = self.env.ref('stock.action_picking_tree_all') + action = self.env.ref('stock.action_picking_tree_all').read()[0] - result = { - 'name': action.name, - 'help': action.help, - 'type': action.type, - 'view_type': action.view_type, - 'view_mode': action.view_mode, - 'target': action.target, - 'context': action.context, - 'res_model': action.res_model, - } - - pick_ids = sum([order.picking_ids.ids for order in self], []) - - if len(pick_ids) > 1: - result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]" - elif len(pick_ids) == 1: - form = self.env.ref('stock.view_picking_form', False) - form_id = form.id if form else False - result['views'] = [(form_id, 'form')] - result['res_id'] = pick_ids[0] - return result + pickings = self.mapped('picking_ids') + if len(pickings) > 1: + action['domain'] = [('id', 'in', pickings.ids)] + elif pickings: + action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] + action['res_id'] = pickings.id + return action @api.multi def action_cancel(self): self.order_line.mapped('procurement_ids').cancel() - super(SaleOrder, self).action_cancel() + return super(SaleOrder, self).action_cancel() @api.multi def _prepare_invoice(self): @@ -132,13 +118,11 @@ class SaleOrderLine(models.Model): @api.onchange('product_id') def _onchange_product_id_set_customer_lead(self): self.customer_lead = self.product_id.sale_delay - return {} @api.onchange('product_packaging') def _onchange_product_packaging(self): if self.product_packaging: return self._check_package() - return {} @api.onchange('product_id') def _onchange_product_id_uom_check_availability(self): diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index e3097132860..94693801023 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -22,11 +22,8 @@ class StockMove(models.Model): result = super(StockMove, self).action_done() # Update delivered quantities on sale order lines - todo = self.env['sale.order.line'] - for move in self: - if (move.procurement_id.sale_line_id) and (move.product_id.expense_policy=='no'): - todo |= move.procurement_id.sale_line_id - for line in todo: + sale_order_lines = self.filtered(lambda move: move.procurement_id.sale_line_id and move.product_id.expense_policy == 'no').mapped('procurement_id.sale_line_id') + for line in sale_order_lines: line.qty_delivered = line._get_delivered_qty() return result @@ -47,15 +44,15 @@ class StockMove(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' - @api.depends('move_lines') + sale_id = fields.Many2one('sale.order', "Sale Order", compute='_compute_sale_id', search='_search_sale_id') + + @api.one + @api.depends('move_lines.procurement_id.sale_line_id.order_id') def _compute_sale_id(self): - for picking in self: - sale_order = False - for move in picking.move_lines: - if move.procurement_id.sale_line_id: - sale_order = move.procurement_id.sale_line_id.order_id - break - picking.sale_id = sale_order.id if sale_order else False + for move in self.move_lines: + if move.procurement_id.sale_line_id: + self.sale_id = move.procurement_id.sale_line_id.order_id + return def _search_sale_id(self, operator, value): moves = self.env['stock.move'].search( @@ -63,9 +60,6 @@ class StockPicking(models.Model): ) return [('id', 'in', moves.mapped('picking_id').ids)] - sale_id = fields.Many2one(comodel_name='sale.order', string="Sale Order", - compute='_compute_sale_id', search='_search_sale_id') - @api.multi def _create_backorder(self, backorder_moves=[]): res = super(StockPicking, self)._create_backorder(backorder_moves)