← Back to team overview

openerp-community team mailing list archive

lp:~camptocamp/openerp-product-attributes/port-add-product_multi_company_7.0-bis-jge into lp:openerp-product-attributes

 

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: support@xxxxxxxxxxx\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 <es@xxxxxx>\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: support@xxxxxxxxxxx\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: support@xxxxxxxxxxx\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"


Follow ups