Joël Grand-Guillaume @ camptocamp has proposed merging lp:~camptocamp/openerp-product-attributes/port-add-product_multi_company_7.0-bis-jge into lp:openerp-product-attributes.
Requested reviews: Maxime Chambreuil (http://www.savoirfairelinux.com) (max3903): code review For more details, see: https://code.launchpad.net/~camptocamp/openerp-product-attributes/port-add-product_multi_company_7.0-bis-jge/+merge/192872 Hi, I propose here a brand new version of product_multi_company to manage prices (standard_price and list_price) by company. It uses another table to store and historize the prices instead of using ir.property. You can also easily override the field to historize in other module to add your own. I provided here a whole set of tests and demo data to ensure no regression and show how to setup OpenERP in a complex multi-company, multi-currency context. With this module, you can have a same shared product between company, with each of them their own average price computed. You can even have the standard_price recorded in USD for company 1 and CHF for company 2 for example. See description of the module for more details. Note that this module replace the old one. I prefered to update this module rather than making another one (with another name). So, If you think it's better to call it differently, please just say it ! Thanks for the review, Joël -- https://code.launchpad.net/~camptocamp/openerp-product-attributes/port-add-product_multi_company_7.0-bis-jge/+merge/192872 Your team OpenERP Community is subscribed to branch lp:openerp-product-attributes.
=== modified file 'product_multi_company/__init__.py' --- product_multi_company/__init__.py 2010-12-29 07:42:35 +0000 +++ product_multi_company/__init__.py 2013-10-28 13:34:35 +0000 @@ -2,7 +2,8 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# Copyright 2013 Camptocamp SA +# Author: Joel Grand-Guillaume # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -19,6 +20,5 @@ # ############################################################################## -import product_multi_company +from . import product_multi_company -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file === modified file 'product_multi_company/__openerp__.py' --- product_multi_company/__openerp__.py 2013-01-21 06:49:06 +0000 +++ product_multi_company/__openerp__.py 2013-10-28 13:34:35 +0000 @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# Copyright 2013 Camptocamp SA +# Author: Joel Grand-Guillaume # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -15,25 +16,56 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. +# along with this program. If not, see <http://www.gnu.org/licenses/> # ############################################################################## { "name" : "Product multi company ", - "version" : "1.1", - "author" : "OpenERP SA", + "version" : "1.2", + "author" : "Camptocamp", "category" : "Generic Modules/Inventory Control", - "depends" : [ "product"], - "init_xml" : [], - "demo_xml" : [], + "depends" : [ "product","purchase"], "description": """ - This module updates the definitions of standard price, public price and seller price with property fields. + This module allow you to record various prices of a same product for different + companies. This way, every company can have his own cost (average or standard) + and sale price. Moreover, it historize the prices in a way that you'll then + be able to retrieve the cost (or sale) price at a given date. + + Note that to benefit those values in stock report (or any other view that is based on SQL), + you'll have to adapt it to include this new historized table. Especially true for stock + valuation. + + This module also contain demo data and various tests to ensure it work well. It show + how to configure OpenERP properly when you have various company, each of them having + their product setup in average price and using different currency. The goal is to share + the products between all company, keeping the right price for each of them. + + Technically, this module updates the definition of field standard_price, list_price + of the product and will make them stored in an external table. We override the read, + write and create methods to achieve that and don't used ir.property for performance + and historization purpose. + + You may want to also use the module analytic_multicurrency from bzr branch lp:account-analytic + in order to have a proper computation in analytic line as well (standard_price will be converted + in company currency with this module when computing cost of analytic line). + + """, - 'update_xml': [], - 'test':[], - 'installable': False, + 'demo': [ + 'product_multi_company_purchase_demo.yml', + ], + 'data': [ + 'security/ir.model.access.csv', + 'security/product_multicompany_security.xml', + ], + 'test': [ + 'test/price_controlling_multicompany.yml', + 'test/avg_price_computation_mutlicompanies_multicurrencies.yml', + 'test/price_historization.yml', + ], + 'installable': True, 'active': False, } -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: === removed directory 'product_multi_company/i18n' === removed file 'product_multi_company/i18n/en_US.po' --- product_multi_company/i18n/en_US.po 2010-12-29 07:42:35 +0000 +++ product_multi_company/i18n/en_US.po 1970-01-01 00:00:00 +0000 @@ -1,32 +0,0 @@ -# Translation of OpenERP Server. -# This file contains the translation of the following modules: -# * product_multi_company -# -msgid "" -msgstr "" -"Project-Id-Version: OpenERP Server 6.0.0-rc1\n" -"Report-Msgid-Bugs-To: supp...@openerp.com\n" -"POT-Creation-Date: 2010-12-28 12:10:13+0000\n" -"PO-Revision-Date: 2010-12-28 12:10:13+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: product_multi_company -#: model:ir.model,name:product_multi_company.model_product_template -msgid "Product Template" -msgstr "" - -#. module: product_multi_company -#: constraint:product.template:0 -msgid "Error: The default UOM and the purchase UOM must be in the same category." -msgstr "" - -#. module: product_multi_company -#: model:ir.model,name:product_multi_company.model_pricelist_partnerinfo -msgid "pricelist.partnerinfo" -msgstr "" - === removed file 'product_multi_company/i18n/es.po' --- product_multi_company/i18n/es.po 2012-12-05 05:42:11 +0000 +++ product_multi_company/i18n/es.po 1970-01-01 00:00:00 +0000 @@ -1,36 +0,0 @@ -# Spanish translation for openobject-addons -# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2011. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2010-12-28 12:10+0000\n" -"PO-Revision-Date: 2011-08-27 14:01+0000\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: Spanish <e...@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2012-12-05 05:41+0000\n" -"X-Generator: Launchpad (build 16335)\n" - -#. module: product_multi_company -#: model:ir.model,name:product_multi_company.model_product_template -msgid "Product Template" -msgstr "Plantilla de producto" - -#. module: product_multi_company -#: constraint:product.template:0 -msgid "" -"Error: The default UOM and the purchase UOM must be in the same category." -msgstr "" -"Error: La UdM por defecto y la UdM de compra deben estar en la misma " -"categoría." - -#. module: product_multi_company -#: model:ir.model,name:product_multi_company.model_pricelist_partnerinfo -msgid "pricelist.partnerinfo" -msgstr "listaprecios.infoempresa" === removed file 'product_multi_company/i18n/fr.po' --- product_multi_company/i18n/fr.po 2012-12-05 05:42:11 +0000 +++ product_multi_company/i18n/fr.po 1970-01-01 00:00:00 +0000 @@ -1,33 +0,0 @@ -# Translation of OpenERP Server. -# This file contains the translation of the following modules: -# * product_multi_company -# -msgid "" -msgstr "" -"Project-Id-Version: OpenERP Server 6.0.0-rc1\n" -"Report-Msgid-Bugs-To: supp...@openerp.com\n" -"POT-Creation-Date: 2010-12-28 12:10+0000\n" -"PO-Revision-Date: 2011-02-15 17:25+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2012-12-05 05:41+0000\n" -"X-Generator: Launchpad (build 16335)\n" - -#. module: product_multi_company -#: model:ir.model,name:product_multi_company.model_product_template -msgid "Product Template" -msgstr "" - -#. module: product_multi_company -#: constraint:product.template:0 -msgid "" -"Error: The default UOM and the purchase UOM must be in the same category." -msgstr "" - -#. module: product_multi_company -#: model:ir.model,name:product_multi_company.model_pricelist_partnerinfo -msgid "pricelist.partnerinfo" -msgstr "" === removed file 'product_multi_company/i18n/product_multi_company.pot' --- product_multi_company/i18n/product_multi_company.pot 2010-12-29 07:42:35 +0000 +++ product_multi_company/i18n/product_multi_company.pot 1970-01-01 00:00:00 +0000 @@ -1,32 +0,0 @@ -# Translation of OpenERP Server. -# This file contains the translation of the following modules: -# * product_multi_company -# -msgid "" -msgstr "" -"Project-Id-Version: OpenERP Server 6.0.0-rc1\n" -"Report-Msgid-Bugs-To: supp...@openerp.com\n" -"POT-Creation-Date: 2010-12-28 12:10:13+0000\n" -"PO-Revision-Date: 2010-12-28 12:10:13+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: product_multi_company -#: model:ir.model,name:product_multi_company.model_product_template -msgid "Product Template" -msgstr "" - -#. module: product_multi_company -#: constraint:product.template:0 -msgid "Error: The default UOM and the purchase UOM must be in the same category." -msgstr "" - -#. module: product_multi_company -#: model:ir.model,name:product_multi_company.model_pricelist_partnerinfo -msgid "pricelist.partnerinfo" -msgstr "" - === modified file 'product_multi_company/product_multi_company.py' --- product_multi_company/product_multi_company.py 2010-12-29 08:08:51 +0000 +++ product_multi_company/product_multi_company.py 2013-10-28 13:34:35 +0000 @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# Copyright 2013 Camptocamp SA +# Author: Joel Grand-Guillaume # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -15,45 +16,181 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. +# along with this program. If not, see <http://www.gnu.org/licenses/> # ############################################################################## -from osv import osv, fields - -class product_template(osv.osv): +from openerp.osv import orm, fields +import time +import openerp.addons.decimal_precision as dp +import logging + +# All field name of product that will be historize +PRODUCT_FIELD_HISTORIZE = ['standard_price', 'list_price'] + + +class price_history(orm.Model): + # TODO : Create good index for select + + _name = 'price.history' + _order = 'datetime,company_id asc' + _logger = logging.getLogger(__name__) + + _columns = { + 'name': fields.char('Field name', size=32, required=True), + 'company_id': fields.many2one('res.company', 'Company', + required=True), + 'product_id': fields.many2one('product.template', 'Product', + required=True), + 'datetime': fields.datetime('Date'), + 'amount': fields.float('Amount', + digits_compute=dp.get_precision('Product Price')), + } + + def _get_default_company(self, cr, uid, context=None): + company = self.pool.get('res.company') + return company._company_default_get(cr, uid, + 'product.template', + context=context) + + def _get_default_date(self, cr, uid, context=None): + if context.get('date_for_history'): + result = context.get('date_for_history') + else: + result = time.strftime('%Y-%m-%d %H:%M:%S') + return result + + _defaults = { + 'company_id': _get_default_company, + 'datetime': _get_default_date, + } + + def _get_historic_price(self, cr, uid, ids, company_id, + datetime=False, field_name=PRODUCT_FIELD_HISTORIZE, + context=None): + """ Use SQL for performance. Return a dict like: + {product_id:{'standard_price': Value, 'list_price': Value}} + If no value found, return 0.0 for each field and products. + """ + res = {} + if not ids: + return res + if not datetime: + datetime = time.strftime('%Y-%m-%d %H:%M:%S') + sql_wh_clause = """SELECT DISTINCT ON (product_id, name) + datetime, product_id, name, amount + FROM price_history + WHERE product_id IN %s + AND datetime <= %s + AND company_id = %s + AND name IN %s + ORDER BY product_id, name, datetime DESC""" + cr.execute(sql_wh_clause, (tuple(ids), datetime, + company_id, tuple(field_name))) + for id in ids: + res[id] = dict.fromkeys(field_name, 0.0) + result = cr.dictfetchall() + for line in result: + data = {line['name']: line['amount']} + res[line['product_id']].update(data) + self._logger.debug('Prices value `%s`', res) + return res + + +class product_template(orm.Model): + _inherit = "product.template" - _description = "Product Template" - _columns={ - 'list_price': fields.property('product.template', - type='float', - string='Public Price', - method=True, - view_load=True, - required=True, - help="Base price for computing the customer price. Sometimes called the catalog price."), - 'standard_price': fields.property('product.template', - type='float', - string='Standard Price', - method=True, - view_load=True, - required=True, - help="Product's cost for accounting stock valuation. It is the base price for the supplier price."), - } -product_template() - -class pricelist_partnerinfo(osv.osv): - _inherit = 'pricelist.partnerinfo' - _description = "Pricelist Partner" + _logger = logging.getLogger(__name__) + + def _log_price_change(self, cr, uid, product, values, context=None): + """ + On change of price create a price_history + :param product value of new product or product_id + """ + price_history = self.pool.get('price.history') + for field_name in PRODUCT_FIELD_HISTORIZE: + if values.get(field_name): + data = { + 'product_id': product, + 'amount': values[field_name], + 'name': field_name + } + price_history.create(cr, uid, data, context=context) + self._logger.debug('RECORD_DICT:`%s`', data) + + def create(self, cr, uid, values, context=None): + """Add the historization at product creation.""" + res = super(product_template, self).create(cr, uid, values, + context=context) + self._log_price_change(cr, uid, res, values, context=context) + return res + + def read(self, cr, uid, ids, fields=None, context=None, + load='_classic_read'): + """Override the read to take price values from the related + price history table.""" + if context is None: + context = {} + if fields: + fields.append('id') + results = super(product_template, + self).read(cr, uid, ids, + fields=fields, context=context, load=load) + # Note if fields is empty => read all, so look at history table + if not fields or any([f in PRODUCT_FIELD_HISTORIZE for f in fields]): + date_crit = False + price_history = self.pool.get('price.history') + user_obj = self.pool.get('res.users') + company_id = user_obj.browse(cr, uid, uid, context=context).company_id.id + if context.get('date_for_history'): + date_crit = context['date_for_history'] + # if fields is empty we read all price fields + if not fields: + price_fields = PRODUCT_FIELD_HISTORIZE + # Otherwise we filter on price fields asked in read + else: + price_fields = [f for f in PRODUCT_FIELD_HISTORIZE if f in fields] + prod_prices = price_history._get_historic_price(cr, uid, ids, + company_id, + datetime=date_crit, + field_name=price_fields, + context=context) + for result in results: + dict_value = prod_prices[result['id']] + result.update(dict_value) + return results + + def write(self, cr, uid, ids, values, context=None): + """Create an entry in the history table for every modified price + of every products with current datetime (or given one in context)""" + if any([f in PRODUCT_FIELD_HISTORIZE for f in values]): + for product in self.browse(cr, uid, ids, context=context): + self._log_price_change(cr, uid, product.id, values, + context=context) + return super(product_template, self).write(cr, uid, ids, values, + context=context) + + +class price_type(orm.Model): + """ + The price type is used to points which field in the product form + is a price and in which currency is this price expressed. + Here, we add the company field to allow having various price type for + various company, may be even in different currency. + """ + + _inherit = "product.price.type" + _columns = { - 'price': fields.property('pricelist.partnerinfo', - type='float', - string='Seller Price', - method=True, - view_load=True, - required=True, - help="This price will be considered as a price for the supplier UoM if any or the default Unit of Measure of the product otherwise"), + 'company_id': fields.many2one('res.company', 'Company', + required=True), } -pricelist_partnerinfo() -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + def _get_default_company(self, cr, uid, context=None): + company = self.pool.get('res.company') + return company._company_default_get(cr, uid, + 'product.price.type', + context=context) + _defaults = { + 'company_id': _get_default_company, + } === added file 'product_multi_company/product_multi_company_purchase_demo.yml' --- product_multi_company/product_multi_company_purchase_demo.yml 1970-01-01 00:00:00 +0000 +++ product_multi_company/product_multi_company_purchase_demo.yml 2013-10-28 13:34:35 +0000 @@ -0,0 +1,189 @@ +- + Create a Partner for second company and second company user +- + !record {model: res.partner, id: res_partner_company_01}: + name: Second Company Partner test +- + !record {model: res.partner, id: res_partner_user_second_company_01}: + name: Second Company User Partner test +- + Create a second company hanlded in CHF +- + !record {model: res.company, id: res_company_01}: + name: Second Company test + partner_id: res_partner_company_01 + currency_id: base.CHF +- + Create a user for second company +- + !record {model: res.users, id: res_users_second_company_01}: + login: user second company + password: user second company + partner_id: res_partner_user_second_company_01 + company_id: res_company_01 + company_ids: [res_company_01,base.main_company] + groups_id: + - base.group_user + - base.group_sale_manager + - purchase.group_purchase_manager + - stock.group_stock_user +- + Create a second set of price type in USD for the second company +- + !record {model: product.price.type, id: list_price_second_cmp}: + name: Public Price USD + field: list_price + currency_id: base.USD + company_id: res_company_01 +- + !record {model: product.price.type, id: standard_price_second_cmp}: + name: Cost Price USD + field: standard_price + currency_id: base.USD + company_id: res_company_01 +- + Ensure the price type of first company is in EUR +- + !record {model: product.price.type, id: product.list_price}: + name: Public Price EUR + field: list_price + currency_id: base.EUR + company_id: base.main_company +- + !record {model: product.price.type, id: product.standard_price}: + name: Cost Price EUR + field: standard_price + currency_id: base.EUR + company_id: base.main_company +- + Set the admin user to first company +- + !record {model: res.users, id: base.user_root}: + company_id: base.main_company +- + Set the currency rate of CHF, EUR and USD (reference EUR) +- + !record {model: res.currency.rate, id: base.rateCHF}: + rate: 1.3086 + currency_id: base.CHF + name: !eval time.strftime('%Y-%m-%d') +- + !record {model: res.currency.rate, id: base.rateUSD}: + rate: 1.2086 + currency_id: base.USD + name: !eval time.strftime('%Y-%m-%d') +- + !record {model: res.currency.rate, id: base.rateEUR}: + rate: 1.0 + currency_id: base.EUR + name: !eval time.strftime('%Y-%m-%d') +- + Ensure the first Company and pricelists are in EUR (rate 1.0) +- + !record {model: res.company, id: base.main_company}: + currency_id: base.EUR +- + !record {model: product.pricelist, id: purchase.list0}: + name: Purchase Pricelist EUR + currency_id: base.EUR + company_id: base.main_company +- + !record {model: product.pricelist, id: product.list0}: + name: Public Pricelist EUR + currency_id: base.EUR + company_id: base.main_company +- + Create a sale pricelist in CHF for second company (also in CHF) +- + !record {model: product.pricelist, id: product_pricelist_salechf}: + name: Public Pricelist CHF + type: sale + company_id: res_company_01 + currency_id: base.CHF +- + !record {model: product.pricelist.version, id: product_pricelist_version_salechf}: + pricelist_id: product_pricelist_salechf + name: Default Public Pricelist Version +- + !record {model: product.pricelist.item, id: product_pricelist_item_salechf}: + price_version_id: product_pricelist_version_salechf + base: !eval ref('list_price_second_cmp') + name: Default Public Pricelist Line +- + Create a purchase pricelist in CHF for second company (also in CHF) +- + !record {model: product.pricelist, id: product_pricelist_purchchf}: + name: Purchase Pricelist CHF + type: purchase + company_id: res_company_01 + currency_id: base.CHF +- + !record {model: product.pricelist.version, id: product_pricelist_version_purchchf}: + pricelist_id: product_pricelist_purchchf + name: Default Purchase Pricelist Version +- + !record {model: product.pricelist.item, id: product_pricelist_item_puchchf}: + price_version_id: product_pricelist_version_purchchf + base: !eval ref('standard_price_second_cmp') + name: Default Purchase Pricelist Line +- + Create a stock location for first and second company +- + !record {model: stock.location, id: location_stock_01}: + name: Stock first company EUR + usage: internal + company_id: base.main_company +- + !record {model: stock.location, id: location_stock_02}: + name: Stock second company CHF + usage: internal + company_id: res_company_01 +- + Create a warehouse for first and second company +- + !record {model: stock.warehouse, id: wh_stock_01}: + name: Warehouse for first company EUR + lot_output_id: location_stock_01 + lot_stock_id: location_stock_01 + lot_input_id: location_stock_01 + company_id: base.main_company +- + !record {model: stock.warehouse, id: wh_stock_02}: + name: Warehouse for second company CHF + lot_output_id: location_stock_02 + lot_stock_id: location_stock_02 + lot_input_id: location_stock_02 + company_id: res_company_01 +- + Create a Supplier for PO +- + !record {model: res.partner, id: res_partner_supplier_01}: + name: Supplier 1 + supplier: 1 +- + Create a wine product J owned by both company +- + !record {model: product.product, id: product_product_j_avg_01}: + categ_id: product.product_category_1 + cost_method: average + name: Wine J + type: product + uom_id: product.product_uom_unit + uom_po_id: product.product_uom_unit + company_id: False +- + Create a wine product K owned by both company +- + !record {model: product.product, id: product_product_k_avg_01}: + categ_id: product.product_category_1 + cost_method: average + name: Wine K + type: product + uom_id: product.product_uom_unit + uom_po_id: product.product_uom_unit + company_id: False +- + Setup the multi company rules for product, share them between company +- + !record {model: ir.rule, id: product.product_comp_rule}: + domain_force: "[(1,'=',1)]" === added directory 'product_multi_company/security' === added file 'product_multi_company/security/ir.model.access.csv' --- product_multi_company/security/ir.model.access.csv 1970-01-01 00:00:00 +0000 +++ product_multi_company/security/ir.model.access.csv 2013-10-28 13:34:35 +0000 @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_price_history_group_user","price_history","model_price_history","base.group_user",1,0,0,0 +"access_price_history_group_sale_manager","price_history","model_price_history","base.group_sale_manager",1,1,1,1 \ No newline at end of file === added file 'product_multi_company/security/product_multicompany_security.xml' --- product_multi_company/security/product_multicompany_security.xml 1970-01-01 00:00:00 +0000 +++ product_multi_company/security/product_multicompany_security.xml 2013-10-28 13:34:35 +0000 @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> +<data noupdate="1"> + + <record id="product_price_type_comp_rule" model="ir.rule"> + <field name="name" >Product Price type multi-company</field> + <field name="model_id" ref="model_product_price_type"/> + <field name="global" eval="True"/> + <field name="domain_force"> ['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field> + </record> + +</data> +</openerp> === added directory 'product_multi_company/test' === added file 'product_multi_company/test/avg_price_computation_mutlicompanies_multicurrencies.yml' --- product_multi_company/test/avg_price_computation_mutlicompanies_multicurrencies.yml 1970-01-01 00:00:00 +0000 +++ product_multi_company/test/avg_price_computation_mutlicompanies_multicurrencies.yml 2013-10-28 13:34:35 +0000 @@ -0,0 +1,186 @@ +- + Test the following with user admin from first company (EUR) +- + !context + uid: 'base.user_root' +- + Create a purchase order for first company EUR +- + !record {model: purchase.order, id: purchase_order_lcost_01}: + partner_id: res_partner_supplier_01 + invoice_method: order + location_id: location_stock_01 + pricelist_id: purchase.list0 + company_id: base.main_company + order_line: + - product_id: product_product_j_avg_01 + price_unit: 100 + product_qty: 15.0 +- + I confirm the order where invoice control is 'Bases on order'. +- + !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lcost_01} +- + Reception is ready to process, make it and check moves value +- + !python {model: stock.partial.picking}: | + pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_lcost_01")).picking_ids + partial_id = self.create(cr, uid, {},context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]}) + self.do_partial(cr, uid, [partial_id]) + picking = self.pool.get('stock.picking').browse(cr, uid, [pick_ids[0].id])[0] + for move in picking.move_lines: + if move.product_id.name == 'Wine J': + assert move.price_unit == 100.0,"Technical field price_unit of Wine J stock move should record the purchase price" +- + I check that purchase order is shipped. +- + !python {model: purchase.order}: | + assert self.browse(cr, uid, ref("purchase_order_lcost_01")).shipped == True,"Purchase order should be delivered" +- + I check that avg price of products is computed correctly +- + !python {model: product.product}: | + xchg_rate_chf = 1.0 + # computed as : (100 * 15 / 15) * Exchnge rate of 1.3086 + value_a = round(100.0 * xchg_rate_chf, 2) + assert self.browse(cr, uid, ref("product_product_j_avg_01")).standard_price == value_a,"Avg price for product Wine J for first company is wrongly computed" +- + Create a second purchase order for first company EUR +- + !record {model: purchase.order, id: purchase_order_lcost_01bis}: + partner_id: res_partner_supplier_01 + invoice_method: order + location_id: location_stock_01 + pricelist_id: purchase.list0 + company_id: base.main_company + order_line: + - product_id: product_product_j_avg_01 + price_unit: 200 + product_qty: 15.0 +- + I confirm the order where invoice control is 'Bases on order'. +- + !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lcost_01bis} +- + Reception is ready for process, make it and check moves value +- + !python {model: stock.partial.picking}: | + pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_lcost_01bis")).picking_ids + partial_id = self.create(cr, uid, {},context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]}) + self.do_partial(cr, uid, [partial_id]) + picking = self.pool.get('stock.picking').browse(cr, uid, [pick_ids[0].id])[0] + for move in picking.move_lines: + if move.product_id.name == 'Wine J': + assert move.price_unit == 200.0,"Technical field price_unit of Wine J stock move should record the purchase price" +- + I check that purchase order is shipped. +- + !python {model: purchase.order}: | + assert self.browse(cr, uid, ref("purchase_order_lcost_01bis")).shipped == True,"Purchase order should be delivered" +- + I check that avg price of products is computed correctly +- + !python {model: product.product}: | + xchg_rate_chf = 1.0 + # Value in stock in EUR + value_a = round(100.0 * xchg_rate_chf, 2) + # computed as : (value_a * 15 + (200 * xchg_rate_chf) * 15) / 30 + value_abis = round((value_a * 15 + (200 * xchg_rate_chf) * 15) / 30, 2) + assert self.browse(cr, uid, ref("product_product_j_avg_01")).standard_price == value_abis,"Avg price for product Wine J for first company is wrongly computed" +- + Test the following with user admin from second company (CHF) +- + !context + uid: 'res_users_second_company_01' +- + Create a purchase order for second company CHF +- + !record {model: purchase.order, id: purchase_order_lcost_02}: + partner_id: res_partner_supplier_01 + invoice_method: manual + location_id: location_stock_02 + pricelist_id: product_pricelist_purchchf + company_id: res_company_01 + order_line: + - product_id: product_product_j_avg_01 + price_unit: 50 + product_qty: 15.0 +- + I confirm the order where invoice control is 'Bases on order'. +- + !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lcost_02} +- + Reception is ready for process, make it and check moves value +- + !python {model: stock.partial.picking}: | + pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_lcost_02")).picking_ids + partial_id = self.create(cr, uid, {},context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]}) + self.do_partial(cr, uid, [partial_id]) + picking = self.pool.get('stock.picking').browse(cr, uid, [pick_ids[0].id])[0] + for move in picking.move_lines: + if move.product_id.name == 'Wine J': + assert move.price_unit == 50.0,"Technical field price_unit of Wine J stock move should record the purchase price" +- + I check that purchase order is shipped. +- + !python {model: purchase.order}: | + assert self.browse(cr, uid, ref("purchase_order_lcost_02")).shipped == True,"Purchase order should be delivered" +- + I check that avg price of products is computed correctly +- + !python {model: product.product}: | + # value in USD stored + value_a = round(50 * (1.2086 / 1.3086), 2) + assert self.browse(cr, uid, ref("product_product_j_avg_01")).standard_price == value_a,"Avg price for product Wine J for second company is wrongly computed" +- + Create a second purchase order for second company CHF +- + !record {model: purchase.order, id: purchase_order_lcost_02bis}: + partner_id: res_partner_supplier_01 + invoice_method: manual + location_id: location_stock_02 + pricelist_id: product_pricelist_purchchf + company_id: res_company_01 + order_line: + - product_id: product_product_j_avg_01 + price_unit: 100 + product_qty: 15.0 +- + I confirm the order where invoice control is 'Bases on order'. +- + !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lcost_02bis} +- + Reception is ready for process, make it and check moves value +- + !python {model: stock.partial.picking}: | + pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_lcost_02bis")).picking_ids + partial_id = self.create(cr, uid, {},context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]}) + self.do_partial(cr, uid, [partial_id]) + picking = self.pool.get('stock.picking').browse(cr, uid, [pick_ids[0].id])[0] + for move in picking.move_lines: + if move.product_id.name == 'Wine J': + assert move.price_unit == 100.0,"Technical field price_unit of Wine J stock move should record the purchase price" +- + I check that purchase order is shipped. +- + !python {model: purchase.order}: | + assert self.browse(cr, uid, ref("purchase_order_lcost_02bis")).shipped == True,"Purchase order should be delivered" +- + I check that avg price of products is computed correctly +- + !python {model: product.product}: | + # Value in stock in USD for first entry (compute as to_currency / from_currency) + value_a = round(50 * (1.2086 / 1.3086), 2) + # Value in stock in USD for second entry (compute as to_currency / from_currency) + value_b = round(100 * (1.2086 / 1.3086), 2) + # computed as : (value_a * 15 + value_b * 15) / 30 + value_abis = round((value_a * 15 + value_b * 15) / 30, 2) + assert self.browse(cr, uid, ref("product_product_j_avg_01")).standard_price == value_abis,"Avg price for product Wine J for second company is wrongly computed" +- + I Check that I get all entries in the price history table +- + !python {model: price.history}: | + num_j = self.search(cr, uid, [('product_id','=',ref("product_product_j_avg_01")),('name','=','standard_price')]) + # 4 PO, 4 updates of standard_price + right_number_j = 4 + assert len(num_j) == right_number_j,"The number of value in the price history table is correct for product J" === added file 'product_multi_company/test/price_controlling_multicompany.yml' --- product_multi_company/test/price_controlling_multicompany.yml 1970-01-01 00:00:00 +0000 +++ product_multi_company/test/price_controlling_multicompany.yml 2013-10-28 13:34:35 +0000 @@ -0,0 +1,55 @@ +- + Test the following with user admin from first company (EUR) +- + !context + uid: 'base.user_root' +- + Create a wine A product owned by both company and set price for first company (EUR) +- + !record {model: product.product, id: product_product_a_avg_01}: + categ_id: product.product_category_1 + name: Wine A + cost_method: standard + uom_id: product.product_uom_unit + uom_po_id: product.product_uom_unit + company_id: 0 + standard_price: 50.0 + list_price: 75.0 +- + Test the prices are those set for the first company (EUR) +- + !python {model: product.product}: | + product = self.browse(cr, uid, ref('product_product_a_avg_01')) + assert product.standard_price == 50.0, "The standard_price has not been recorded correctly for first company" + assert product.list_price == 75.0, "The list_price has not been recorded correctly for first company" +- + Test the following with user second_user from second company (CHF) +- + !context + uid: 'res_users_second_company_01' +- + Modify product A owned by both company and set price in USD for second company (CHF) +- + !python {model: product.product}: | + self.write(cr, uid, ref('product_product_a_avg_01'), {'standard_price':70,'list_price':90}) +- + Test the USD prices are those set for the second company (CHF) +- + !python {model: product.product}: | + product = self.browse(cr, uid, ref('product_product_a_avg_01')) + assert product.standard_price == 70.0, "The standard_price has not been recorded correctly for first company" + assert product.list_price == 90.0, "The list_price has not been recorded correctly for first company" +- + Test the following with user admin from first company (EUR) +- + !context + uid: 'base.user_root' +- + Test the prices are still the same for first company (EUR) +- + !python {model: product.product}: | + product = self.browse(cr, uid, ref('product_product_a_avg_01')) + assert product.standard_price == 50.0, "The standard_price has not been recorded correctly for first company" + assert product.list_price == 75.0, "The list_price has not been recorded correctly for first company" + + === added file 'product_multi_company/test/price_historization.yml' --- product_multi_company/test/price_historization.yml 1970-01-01 00:00:00 +0000 +++ product_multi_company/test/price_historization.yml 2013-10-28 13:34:35 +0000 @@ -0,0 +1,55 @@ +- + Test the following with user admin from first company (EUR) +- + !context + uid: 'base.user_root' +- + Modify product K at first day of year and set price in EUR for first company (EUR) +- + !python {model: product.product}: | + import time + ctx = context + ctx.update({'date_for_history':time.strftime('%Y-01-01 %H:%M:%S')}) + print ctx + self.write(cr, uid, ref('product_product_k_avg_01'), {'standard_price':70,'list_price':90}, context=ctx) +- + Test the EUR prices are those just set on the first day of year +- + !python {model: product.product}: | + import time + ctx = context + ctx.update({'date_for_history':time.strftime('%Y-01-01 %H:%M:%S')}) + product = self.browse(cr, uid, ref('product_product_k_avg_01'), context=ctx) + assert product.standard_price == 70.0, "The standard_price has not been recorded correcdate_for_historytly for first company" + assert product.list_price == 90.0, "The list_price has not been recorded correctly for first company" +- + Modify product K at 3rd day of year and set price in EUR for first company (EUR) +- + !python {model: product.product}: | + import time + ctx = context + ctx.update({'date_for_history':time.strftime('%Y-01-03 %H:%M:%S')}) + print ctx + self.write(cr, uid, ref('product_product_k_avg_01'), {'standard_price':80,'list_price':100}, context=ctx) +- + Test the EUR prices 2nd day of year +- + !python {model: product.product}: | + import time + ctx = context + ctx.update({'date_for_history':time.strftime('%Y-01-02 %H:%M:%S')}) + print ctx + product = self.browse(cr, uid, ref('product_product_k_avg_01'),context=ctx) + assert product.standard_price == 70.0, "The standard_price has not been retrieved correctly for first company" + assert product.list_price == 90.0, "The list_price has not been retrieved correctly for first company" +- + Test the EUR prices 3rd day of year +- + !python {model: product.product}: | + import time + ctx = context + ctx.update({'date_for_history':time.strftime('%Y-01-03 %H:%M:%S')}) + print ctx + product = self.browse(cr, uid, ref('product_product_k_avg_01'),context=ctx) + assert product.standard_price == 80.0, "The standard_price has not been retrieved correctly for first company" + assert product.list_price == 100.0, "The list_price has not been retrieved correctly for first company"
_______________________________________________ Mailing list: https://launchpad.net/~openerp-community Post to : openerp-community@lists.launchpad.net Unsubscribe : https://launchpad.net/~openerp-community More help : https://help.launchpad.net/ListHelp