openerp-community team mailing list archive
-
openerp-community team
-
Mailing list archive
-
Message #03455
lp:~camptocamp/openerp-product-attributes/port-add-product_multi_company_7.0-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-jge into lp:openerp-product-attributes.
Commit message:
[ADD] product_multi_company for version 7.0, completely rebuilt to allow managing a single product between various company. The prices on the product can be recorded in different currency for each company. It also add an historization of the prices to allow a stock valuation at a given date (not provided yet, but data will be there).
Requested reviews:
Product Core Editors (product-core-editors)
For more details, see:
https://code.launchpad.net/~camptocamp/openerp-product-attributes/port-add-product_multi_company_7.0-jge/+merge/191128
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-jge/+merge/191128
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-15 09:27: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-15 09:27: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/average_price_computation_mutlicompany_currency.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-15 09:27: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-15 09:27: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-15 09:27: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-15 09:27: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/average_price_computation_mutlicompany_currency.yml'
--- product_multi_company/test/average_price_computation_mutlicompany_currency.yml 1970-01-01 00:00:00 +0000
+++ product_multi_company/test/average_price_computation_mutlicompany_currency.yml 2013-10-15 09:27: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-15 09:27: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-15 09:27: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"
References