diff options
| author | tpa-odoo <tpa@tinyerp.com> | 2015-02-05 10:28:49 +0100 |
|---|---|---|
| committer | Jérome Maes <jem@openerp.com> | 2015-02-05 13:25:44 +0100 |
| commit | 88200e42e936bebdc8d9f282827c26da0874af79 (patch) | |
| tree | fa19a45860afb060460b7694b7b3c1408112475a | |
| parent | 275234c06796ababf5bfb5356e8d216c8aea81c7 (diff) | |
[ADD] inter_company_rules : feature to create inter company sale, purchase and invoice (Example : automatically create PO or SO for supplier company of the same database).
| -rw-r--r-- | addons/base_setup/res_config.py | 2 | ||||
| -rw-r--r-- | addons/base_setup/res_config_view.xml | 9 | ||||
| -rw-r--r-- | addons/inter_company_rules/__init__.py | 2 | ||||
| -rw-r--r-- | addons/inter_company_rules/__openerp__.py | 50 | ||||
| -rw-r--r-- | addons/inter_company_rules/models/__init__.py | 6 | ||||
| -rw-r--r-- | addons/inter_company_rules/models/account_invoice.py | 133 | ||||
| -rw-r--r-- | addons/inter_company_rules/models/purchase_order.py | 118 | ||||
| -rw-r--r-- | addons/inter_company_rules/models/res_company.py | 35 | ||||
| -rw-r--r-- | addons/inter_company_rules/models/res_config.py | 58 | ||||
| -rw-r--r-- | addons/inter_company_rules/models/sale_order.py | 127 | ||||
| -rw-r--r-- | addons/inter_company_rules/test/inter_company_invoice.yml | 79 | ||||
| -rw-r--r-- | addons/inter_company_rules/test/inter_company_po_to_so.yml | 42 | ||||
| -rw-r--r-- | addons/inter_company_rules/test/inter_company_so_to_po.yml | 46 | ||||
| -rw-r--r-- | addons/inter_company_rules/test/test_intercompany_data.yml | 147 | ||||
| -rw-r--r-- | addons/inter_company_rules/views/inter_company_so_po_view.xml | 24 | ||||
| -rw-r--r-- | addons/inter_company_rules/views/res_config_view.xml | 34 |
16 files changed, 912 insertions, 0 deletions
diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py index 4a7614b0908..7ba914f0774 100644 --- a/addons/base_setup/res_config.py +++ b/addons/base_setup/res_config.py @@ -43,6 +43,8 @@ class base_config_settings(osv.osv_memory): help="""This installs the module google_calendar."""), 'font': fields.many2one('res.font', string="Report Font", domain=[('mode', 'in', ('Normal', 'Regular', 'all', 'Book'))], help="Set the font into the report header, it will be used as default font in the RML reports of the user company"), + 'module_inter_company_rules': fields.boolean('Manage Inter Company', + help="""This installs the module inter_company_rules.\n Configure company rules to automatically create SO/PO when one of your company sells/buys to another of your company."""), 'company_share_partner': fields.boolean('Share partners to all companies', help="Share your partners to all companies defined in your instance.\n" " * Checked : Partners are visible for every companies, even if a company is defined on the partner.\n" diff --git a/addons/base_setup/res_config_view.xml b/addons/base_setup/res_config_view.xml index 687558b2782..d5e0b50d479 100644 --- a/addons/base_setup/res_config_view.xml +++ b/addons/base_setup/res_config_view.xml @@ -104,6 +104,15 @@ </div> </div> </group> + <group> + <label for="module_inter_company_rules" string="Inter company"/> + <div name="inter_company"> + <div name="module_inter_company_rules"> + <field name="module_inter_company_rules" class="oe_inline"/> + <label for="module_inter_company_rules"/> + </div> + </div> + </group> <group string="Shared ressources" name="company_share_ressources"> <label for="company_share_partner" /> <div> diff --git a/addons/inter_company_rules/__init__.py b/addons/inter_company_rules/__init__.py new file mode 100644 index 00000000000..89d26e2f597 --- /dev/null +++ b/addons/inter_company_rules/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import models diff --git a/addons/inter_company_rules/__openerp__.py b/addons/inter_company_rules/__openerp__.py new file mode 100644 index 00000000000..0f3da7a26e8 --- /dev/null +++ b/addons/inter_company_rules/__openerp__.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/> +# +############################################################################## +{ + 'name': 'Inter Company Module for Sale/Purchase Orders and Invoices', + 'version': '1.1', + 'summary': 'Intercompany SO/PO/INV rules', + 'description': ''' Module for synchronization of Documents between several companies. For example, this allow you to have a Sale Order created automatically when a Purchase Order is validated with another company of the system as supplier, and inversely. + + Supported documents are SO, PO and invoices/refunds. +''', + 'author': 'Odoo SA', + 'website': 'http://www.odoo.com', + 'depends': [ + 'sale', + 'purchase', + 'sale_stock', + 'sale_order_dates' + ], + 'data': [ + 'views/inter_company_so_po_view.xml', + 'views/res_config_view.xml', + ], + 'test': [ + 'test/test_intercompany_data.yml', + 'test/inter_company_so_to_po.yml', + 'test/inter_company_po_to_so.yml', + 'test/inter_company_invoice.yml' + ], + 'demo': [], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/inter_company_rules/models/__init__.py b/addons/inter_company_rules/models/__init__.py new file mode 100644 index 00000000000..33c928ea8b2 --- /dev/null +++ b/addons/inter_company_rules/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +import res_config +import res_company +import purchase_order +import sale_order +import account_invoice
\ No newline at end of file diff --git a/addons/inter_company_rules/models/account_invoice.py b/addons/inter_company_rules/models/account_invoice.py new file mode 100644 index 00000000000..4740cc4e1be --- /dev/null +++ b/addons/inter_company_rules/models/account_invoice.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models, _ +from openerp.exceptions import Warning + + +class account_invoice(models.Model): + + _inherit = 'account.invoice' + + auto_generated = fields.Boolean(string='Auto Generated Document', copy=False, default=False) + auto_invoice_id = fields.Many2one('account.invoice', string='Source Invoice', readonly=True, copy=False) + + @api.multi + def invoice_validate(self): + """ Validated invoice generate cross invoice base on company rules """ + for invoice in self: + # do not consider invoices that have already been auto-generated, nor the invoices that were already validated in the past + company = self.env['res.company']._find_company_from_partner(invoice.partner_id.id) + if company and company.auto_generate_invoices and not invoice.auto_generated: + if invoice.type == 'out_invoice': + invoice.inter_company_create_invoice(company, 'in_invoice', 'purchase') + elif invoice.type == 'in_invoice': + invoice.inter_company_create_invoice(company, 'out_invoice', 'sale') + elif invoice.type == 'out_refund': + invoice.inter_company_create_invoice(company, 'in_refund', 'purchase_refund') + elif invoice.type == 'in_refund': + invoice.inter_company_create_invoice(company, 'out_refund', 'sale_refund') + return super(account_invoice, self).invoice_validate() + + @api.one + def inter_company_create_invoice(self, company, inv_type, journal_type): + """ create an invoice for the given company : it wil copy the invoice lines in the new + invoice. The intercompany user is the author of the new invoice. + :param company : the company of the created invoice + :rtype company : res.company record + :param inv_type : the type of the invoice ('in_refund', 'out_refund', 'in_invoice', ...) + :rtype inv_type : string + :param journal_type : the type of the journal to register the invoice + :rtype journal_type : string + """ + # find user for creating the invoice from company + intercompany_uid = company.intercompany_user_id and company.intercompany_user_id.id or False + if not intercompany_uid: + raise Warning(_('Provide one user for intercompany relation for % ') % company.name) + + context = self._context.copy() + context['force_company'] = company.id + origin_partner_id = self.company_id.partner_id + invoice_line_ids = [] + for line in self.invoice_line: + # get invoice line data from product onchange + product_uom_id = line.product_id.uom_id and line.product_id.uom_id.id or False + line_data = line.with_context(context).sudo().product_id_change(line.product_id.id, + product_uom_id, + qty=line.quantity, + name='', + type=inv_type, + partner_id=origin_partner_id.id, + fposition_id=origin_partner_id.property_account_position.id, + company_id=company.id) + # create invoice line, as the intercompany user + inv_line_data = self.sudo()._prepare_invoice_line_data(line_data, line) + inv_line_id = line.with_context(context).sudo(intercompany_uid).create(inv_line_data) + invoice_line_ids.append(inv_line_id.id) + # create invoice, as the intercompany user + invoice_vals = self.with_context(context).sudo()._prepare_invoice_data(invoice_line_ids, inv_type, journal_type, company)[0] + return self.with_context(context).sudo(intercompany_uid).create(invoice_vals) + + @api.one + def _prepare_invoice_data(self, invoice_line_ids, inv_type, journal_type, company): + """ Generate invoice values + :param invoice_line_ids : the ids of the invoice lines + :rtype invoice_line_ids : array of integer + :param inv_type : the type of the invoice to prepare the values + :param journal_type : type of the journal to register the invoice_line_ids + :rtype journal_type : string + :rtype company : res.company record + """ + # find the correct journal + journal = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company.id)], limit=1) + if not journal: + raise Warning(_('Please define %s journal for this company: "%s" (id:%d).') % (journal_type, company.name, company.id)) + + # find periods of supplier company + context = self._context.copy() + context['company_id'] = company.id + period_ids = self.env['account.period'].with_context(context).find(self.date_invoice) + + # find account, payment term, fiscal position, bank. + partner_data = self.onchange_partner_id(inv_type, self.company_id.partner_id.id, company_id=company.id) + return { + 'name': self.name, + #TODO : not sure !! + 'origin': self.company_id.name + _(' Invoice: ') + str(self.number), + 'type': inv_type, + 'date_invoice': self.date_invoice, + 'reference': self.reference, + 'account_id': partner_data['value'].get('account_id', False), + 'partner_id': self.company_id.partner_id.id, + 'journal_id': journal.id, + 'invoice_line': [(6, 0, invoice_line_ids)], + 'currency_id': self.currency_id and self.currency_id.id, + 'fiscal_position': partner_data['value'].get('fiscal_position', False), + 'payment_term': partner_data['value'].get('payment_term', False), + 'company_id': company.id, + 'period_id': period_ids and period_ids[0].id or False, + 'partner_bank_id': partner_data['value'].get('partner_bank_id', False), + 'auto_generated': True, + 'auto_invoice_id': self.id, + } + + @api.model + def _prepare_invoice_line_data(self, line_data, line): + """ Generate invoice line values + :param line_data : dict of invoice line data + :rtype line_data : dict + :param line : the invoice line object + :rtype line : account.invoice.line record + """ + vals = { + 'name': line.name, + 'price_unit': line.price_unit, + 'quantity': line.quantity, + 'discount': line.discount, + 'product_id': line.product_id.id or False, + 'uos_id': line.uos_id.id or False, + 'sequence': line.sequence, + 'invoice_line_tax_id': [(6, 0, line_data['value'].get('invoice_line_tax_id', []))], + 'account_analytic_id': line.account_analytic_id.id or False, + } + if line_data['value'].get('account_id'): + vals['account_id'] = line_data['value']['account_id'] + return vals diff --git a/addons/inter_company_rules/models/purchase_order.py b/addons/inter_company_rules/models/purchase_order.py new file mode 100644 index 00000000000..b59592a2a1d --- /dev/null +++ b/addons/inter_company_rules/models/purchase_order.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models, _ +from openerp.exceptions import Warning + + +class purchase_order(models.Model): + + _inherit = "purchase.order" + + auto_generated = fields.Boolean(string='Auto Generated Purchase Order', copy=False) + auto_sale_order_id = fields.Many2one('sale.order', string='Source Sale Order', readonly=True, copy=False) + + @api.multi + def wkf_confirm_order(self): + """ Generate inter company sale order base on conditions.""" + res = super(purchase_order, self).wkf_confirm_order() + for order in self: + # get the company from partner then trigger action of intercompany relation + company_rec = self.env['res.company']._find_company_from_partner(order.partner_id.id) + if company_rec and company_rec.so_from_po and (not order.auto_generated): + order.inter_company_create_sale_order(company_rec) + return res + + + @api.one + def inter_company_create_sale_order(self, company): + """ Create a Sale Order from the current PO (self) + Note : In this method, reading the current PO is done as sudo, and the creation of the derived + SO as intercompany_user, minimizing the access right required for the trigger user. + :param company : the company of the created PO + :rtype company : res.company record + """ + SaleOrder = self.env['sale.order'] + company_partner = self.company_id.partner_id + + # find user for creating and validation SO/PO from partner company + intercompany_uid = company.intercompany_user_id and company.intercompany_user_id.id or False + if not intercompany_uid: + raise Warning(_('Provide at least one user for inter company relation for % ') % company.name) + # check intercompany user access rights + if not SaleOrder.sudo(intercompany_uid).check_access_rights('create', raise_exception=False): + raise Warning(_("Inter company user of company %s doesn't have enough access rights") % company.name) + + # check pricelist currency should be same with SO/PO document + if self.pricelist_id.currency_id.id != company_partner.property_product_pricelist.currency_id.id: + raise Warning(_('You cannot create SO from PO because sale price list currency is different than purchase price list currency.')) + + # create the SO and generate its lines from the PO lines + SaleOrderLine = self.env['sale.order.line'] + # read it as sudo, because inter-compagny user can not have the access right on PO + sale_order_data = self.sudo()._prepare_sale_order_data(self.name, company_partner, company, self.dest_address_id and self.dest_address_id.id or False) + sale_order = SaleOrder.sudo(intercompany_uid).create(sale_order_data[0]) + for line in self.order_line: + so_line_vals = self.sudo()._prepare_sale_order_line_data(line, company, sale_order.id) + SaleOrderLine.sudo(intercompany_uid).create(so_line_vals) + + # write supplier reference field on PO + if not self.partner_ref: + self.partner_ref = sale_order.name + + #Validation of sale order + if company.auto_validation: + sale_order.sudo(intercompany_uid).signal_workflow('order_confirm') + + @api.one + def _prepare_sale_order_data(self, name, partner, company, direct_delivery_address): + """ Generate the Sale Order values from the PO + :param name : the origin client reference + :rtype name : string + :param partner : the partner reprenseting the company + :rtype partner : res.partner record + :param company : the company of the created SO + :rtype company : res.company record + :param direct_delivery_address : the address of the SO + :rtype direct_delivery_address : res.partner record + """ + partner_addr = partner.sudo().address_get(['default', 'invoice', 'delivery', 'contact']) + return { + 'name': self.env['ir.sequence'].sudo().next_by_code('sale.order') or '/', + 'company_id': company.id, + 'client_order_ref': name, + 'partner_id': partner.id, + 'pricelist_id': partner.property_product_pricelist.id, + 'partner_invoice_id': partner_addr['invoice'], + 'date_order': self.date_order, + 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False, + 'user_id': False, + 'auto_generated': True, + 'auto_purchase_order_id': self.id, + 'partner_shipping_id': direct_delivery_address or partner_addr['delivery'] + } + + @api.model + def _prepare_sale_order_line_data(self, line, company, sale_id): + """ Generate the Sale Order Line values from the PO line + :param line : the origin Purchase Order Line + :rtype line : purchase.order.line record + :param company : the company of the created SO + :rtype company : res.company record + :param sale_id : the id of the SO + """ + # it may not affected because of parallel company relation + price = line.price_unit or 0.0 + taxes = line.taxes_id + if line.product_id: + taxes = line.product_id.taxes_id + company_taxes = [tax_rec.id for tax_rec in taxes if tax_rec.company_id.id == company.id] + return { + 'name': line.product_id and line.product_id.name or line.name, + 'order_id': sale_id, + 'product_uom_qty': line.product_qty, + 'product_id': line.product_id and line.product_id.id or False, + 'product_uom': line.product_id and line.product_id.uom_id.id or line.product_uom.id, + 'price_unit': price, + 'delay': line.product_id and line.product_id.sale_delay or 0.0, + 'company_id': company.id, + 'tax_id': [(6, 0, company_taxes)], + } diff --git a/addons/inter_company_rules/models/res_company.py b/addons/inter_company_rules/models/res_company.py new file mode 100644 index 00000000000..2d7c4081a7a --- /dev/null +++ b/addons/inter_company_rules/models/res_company.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models, _, SUPERUSER_ID +from openerp.exceptions import Warning + + +class res_company(models.Model): + + _inherit = 'res.company' + + so_from_po = fields.Boolean(string="Create Sale Orders when buying to this company", + help="Generate a Sale Order when a Purchase Order with this company as supplier is created.\n The intercompany user must at least be Sale User.") + po_from_so = fields.Boolean(string="Create Purchase Orders when selling to this company", + help="Generate a Purchase Order when a Sale Order with this company as customer is created.\n The intercompany user must at least be Purchase User.") + auto_generate_invoices = fields.Boolean(string="Create Invoices/Refunds when encoding invoices/refunds made to this company", + help="Generate Customer/Supplier Invoices (and refunds) when encoding invoices (or refunds) made to this company.\n e.g: Generate a Customer Invoice when a Supplier Invoice with this company as supplier is created.") + auto_validation = fields.Boolean(string="Sale/Purchase Orders Auto Validation", + help="When a Sale Order or a Purchase Order is created by a multi company rule for this company, it will automatically validate it") + intercompany_user_id = fields.Many2one("res.users", string="Inter Company User", default=SUPERUSER_ID, + help="Responsible user for creation of documents triggered by intercompany rules.") + warehouse_id = fields.Many2one("stock.warehouse", string="Warehouse For Purchase Orders", + help="Default value to set on Purchase Orders that will be created based on Sale Orders made to this company") + + @api.model + def _find_company_from_partner(self, partner_id): + company = self.sudo().search([('partner_id', '=', partner_id)], limit=1) + return company or False + + @api.one + @api.constrains('po_from_so', 'so_from_po', 'auto_generate_invoices') + def _check_intercompany_missmatch_selection(self): + if (self.po_from_so or self.so_from_po) and self.auto_generate_invoices: + raise Warning(_('''You cannot select to create invoices based on other invoices + simultaneously with another option ('Create Sale Orders when buying to this + company' or 'Create Purchase Orders when selling to this company')!''')) + diff --git a/addons/inter_company_rules/models/res_config.py b/addons/inter_company_rules/models/res_config.py new file mode 100644 index 00000000000..dfc6aa8c681 --- /dev/null +++ b/addons/inter_company_rules/models/res_config.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from openerp import models, fields, api + +class inter_company_rules_configuration(models.TransientModel): + + _inherit = 'base.config.settings' + + company_id = fields.Many2one('res.company', string='Select Company', + help='Select company to setup Inter company rules.') + rule_type = fields.Selection([('so_and_po', 'SO and PO setting for inter company'), + ('invoice_and_refunds', 'Create Invoice/Refunds when encoding invoice/refunds')], + help='Select the type to setup inter company rules in selected company.') + so_from_po = fields.Boolean(string='Create Sale Orders when buying to this company', + help='Generate a Sale Order when a Purchase Order with this company as supplier is created.') + po_from_so = fields.Boolean(string='Create Purchase Orders when selling to this company', + help='Generate a Purchase Order when a Sale Order with this company as customer is created.') + auto_validation = fields.Boolean(string='Sale/Purchase Orders Auto Validation', + help='''When a Sale Order or a Purchase Order is created by a multi + company rule for this company, it will automatically validate it.''') + warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse For Purchase Orders', + help='Default value to set on Purchase Orders that will be created based on Sale Orders made to this company.') + + @api.onchange('rule_type') + def onchange_rule_type(self): + if self.rule_type == 'invoice_and_refunds': + self.so_from_po = False + self.po_from_so = False + self.auto_validation = False + + elif self.rule_type == 'so_and_po': + self.invoice_and_refunds = False + + @api.onchange('company_id') + def onchange_company_id(self): + if self.company_id: + rule_type = False + if self.company_id.so_from_po or self.company_id.po_from_so or self.company_id.auto_validation: + rule_type = 'so_and_po' + elif self.company_id.auto_generate_invoices: + rule_type = 'invoice_and_refunds' + + self.rule_type = rule_type + self.so_from_po = self.company_id.so_from_po + self.po_from_so = self.company_id.po_from_so + self.auto_validation = self.company_id.auto_validation + self.warehouse_id = self.company_id.warehouse_id.id + + @api.multi + def set_inter_company_configuration(self): + if self.company_id: + vals = { + 'so_from_po': self.so_from_po, + 'po_from_so': self.po_from_so, + 'auto_validation': self.auto_validation, + 'auto_generate_invoices': True if self.rule_type == 'invoice_and_refunds' else False, + 'warehouse_id': self.warehouse_id.id + } + self.company_id.write(vals) diff --git a/addons/inter_company_rules/models/sale_order.py b/addons/inter_company_rules/models/sale_order.py new file mode 100644 index 00000000000..a8070e270ad --- /dev/null +++ b/addons/inter_company_rules/models/sale_order.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models, _ +from openerp.exceptions import Warning + + +class sale_order(models.Model): + + _inherit = "sale.order" + + auto_generated = fields.Boolean(string='Auto Generated Sale Order', copy=False) + auto_purchase_order_id = fields.Many2one('purchase.order', string='Source Purchase Order', readonly=True, copy=False) + + @api.multi + def action_button_confirm(self): + """ Generate inter company purchase order based on conditions """ + res = super(sale_order, self).action_button_confirm() + for order in self: + if not order.company_id: # if company_id not found, return to normal behavior + continue + # if company allow to create a Purchase Order from Sale Order, then do it ! + company = self.env['res.company']._find_company_from_partner(order.partner_id.id) + if company and company.po_from_so and (not order.auto_generated): + order.inter_company_create_purchase_order(company) + return res + + @api.one + def inter_company_create_purchase_order(self, company): + """ Create a Purchase Order from the current SO (self) + Note : In this method, reading the current SO is done as sudo, and the creation of the derived + PO as intercompany_user, minimizing the access right required for the trigger user + :param company : the company of the created PO + :rtype company : res.company record + """ + PurchaseOrder = self.env['purchase.order'] + company_partner = self.company_id and self.company_id.partner_id or False + if not company or not company_partner.id: + return + + # find user for creating and validating SO/PO from company + intercompany_uid = company.intercompany_user_id and company.intercompany_user_id.id or False + if not intercompany_uid: + raise Warning(_('Provide one user for intercompany relation for % ') % company.name) + # check intercompany user access rights + if not PurchaseOrder.sudo(intercompany_uid).check_access_rights('create', raise_exception=False): + raise Warning(_("Inter company user of company %s doesn't have enough access rights") % company.name) + + # check pricelist currency should be same with SO/PO document + if self.pricelist_id.currency_id.id != company_partner.property_product_pricelist_purchase.currency_id.id: + raise Warning(_('You cannot create PO from SO because purchase pricelist currency is different than sale pricelist currency.')) + + # create the PO and generate its lines from the SO + PurchaseOrderLine = self.env['purchase.order.line'] + # read it as sudo, because inter-compagny user can not have the access right on PO + po_vals = self.sudo()._prepare_purchase_order_data(company, company_partner) + purchase_order = PurchaseOrder.sudo(intercompany_uid).create(po_vals[0]) + for line in self.order_line: + po_line_vals = self.sudo()._prepare_purchase_order_line_data(line, self.date_order, purchase_order.id, company) + PurchaseOrderLine.sudo(intercompany_uid).create(po_line_vals) + + # write customer reference field on SO + if not self.client_order_ref: + self.client_order_ref = purchase_order.name + + # auto-validate the purchase order if needed + if company.auto_validation: + purchase_order.sudo(intercompany_uid).signal_workflow('purchase_confirm') + + @api.one + def _prepare_purchase_order_data(self, company, company_partner): + """ Generate purchase order values, from the SO (self) + :param company_partner : the partner representing the company of the SO + :rtype company_partner : res.partner record + :param company : the company in which the PO line will be created + :rtype company : res.company record + """ + # find location and warehouse, pick warehouse from company object + warehouse = company.warehouse_id and company.warehouse_id.company_id.id == company.id and company.warehouse_id or False + if not warehouse: + raise Warning(_('Configure correct warehouse for company(%s) from Menu: Settings/companies/companies' % (company.name))) + + return { + 'name': self.env['ir.sequence'].sudo().next_by_code('purchase.order'), + 'origin': self.name, + 'partner_id': company_partner.id, + 'location_id': warehouse.lot_stock_id.id, + 'pricelist_id': company_partner.property_product_pricelist_purchase.id, + 'date_order': self.date_order, + 'company_id': company.id, + 'fiscal_position': company_partner.property_account_position or False, + 'payment_term_id': company_partner.property_supplier_payment_term.id or False, + 'auto_generated': True, + 'auto_sale_order_id': self.id, + 'partner_ref': self.name, + 'dest_address_id': self.partner_shipping_id and self.partner_shipping_id.id or False, + } + + @api.model + def _prepare_purchase_order_line_data(self, so_line, date_order, purchase_id, company): + """ Generate purchase order line values, from the SO line + :param so_line : origin SO line + :rtype so_line : sale.order.line record + :param date_order : the date of the orgin SO + :param purchase_id : the id of the purchase order + :param company : the company in which the PO line will be created + :rtype company : res.company record + """ + # price on PO so_line should be so_line - discount + price = so_line.price_unit - (so_line.price_unit * (so_line.discount / 100)) + + # computing Default taxes of so_line. It may not affect because of parallel company relation + taxes = so_line.tax_id + if so_line.product_id: + taxes = so_line.product_id.supplier_taxes_id + + # fetch taxes by company not by inter-company user + company_taxes = [tax_rec.id for tax_rec in taxes if tax_rec.company_id.id == company.id] + return { + 'name': so_line.name, + 'order_id': purchase_id, + 'product_qty': so_line.product_uom_qty, + 'product_id': so_line.product_id and so_line.product_id.id or False, + 'product_uom': so_line.product_id and so_line.product_id.uom_po_id.id or so_line.product_uom.id, + 'price_unit': price or 0.0, + 'company_id': so_line.order_id.company_id.id, + 'date_planned': so_line.order_id.commitment_date or date_order, + 'taxes_id': [(6, 0, company_taxes)], + } diff --git a/addons/inter_company_rules/test/inter_company_invoice.yml b/addons/inter_company_rules/test/inter_company_invoice.yml new file mode 100644 index 00000000000..65287bc04d0 --- /dev/null +++ b/addons/inter_company_rules/test/inter_company_invoice.yml @@ -0,0 +1,79 @@ +- + In order to check Inter-Company Rule of creating Supplier invoice from Customer invoice + make 'Create Invoices/Refunds when encoding invoices/refunds made' true. +- + !record {model: res.company, id: company_a}: + so_from_po: 0 + po_from_so: 0 + auto_generate_invoices: 1 +- + !record {model: res.company, id: company_b}: + so_from_po: 0 + po_from_so: 0 + auto_generate_invoices: 1 +- + I apply account user group to Company A user. +- + !record {model: res.users, id: res_users_company_a}: + groups_id: + - account.group_account_user +- + I will create customer invoice with account user. +- + !context + uid: 'res_users_company_a' +- + !record {model: account.account, id: account_expense_company_a}: + name: Expenses + code: X1000 + type: other + user_type: account.data_account_type_expense + company_id: company_a +- + Create an invoice for the partner of company B with amount 450.0 +- + !record {model: account.invoice, id: account_invoice_0}: + company_id: company_a + partner_id: partner_company_b + currency_id: base.EUR + invoice_line: + - product_id: product_consultant + price_unit: 450.0 + account_id: account_expense_company_a + quantity: 1.0 +- + I check that the customer invoice is in draft state. +- + !assert {model: account.invoice, id: account_invoice_0}: + - state == 'draft' +- + I validate the invoice. +- + !workflow {model: account.invoice, action: invoice_open, ref: account_invoice_0} +- + Now apply account user group to User B and test with that user. +- + !record {model: res.users, id: res_users_company_b}: + groups_id: + - account.group_account_user +- + !context + uid: 'res_users_company_b' +- + I check that the supplier invoice is created with proper data. +- + !python {model: account.invoice}: | + supplier_invoices = self.search(cr, uid, [('type','=','in_invoice')], context=context) + supplier_invoice = self.browse(cr, uid, supplier_invoices[0], context=context) + + #check invoice line is created with proper data + assert supplier_invoice.invoice_line[0].quantity == 1, "Quantity in invoice line is incorrect." + assert supplier_invoice.invoice_line[0].product_id.id == ref("product_consultant"), "Product in line is incorrect." + assert supplier_invoice.invoice_line[0].price_unit == 450, "Unit Price in invoice line is incorrect." + assert supplier_invoice.invoice_line[0].account_id.company_id.id == ref("company_b"), "Applied account in created invoice line is not relevant to company." + + assert supplier_invoice.state == "draft", "invoice should be in draft state." + assert supplier_invoice.amount_total == 450.0, "Total amount is incorrect." + assert supplier_invoice.company_id.id == ref("company_b"), "Applied company in created invoice is incorrect." + assert supplier_invoice.account_id.company_id.id == ref("company_b"), "Applied account in created invoice is not relevant to company." + diff --git a/addons/inter_company_rules/test/inter_company_po_to_so.yml b/addons/inter_company_rules/test/inter_company_po_to_so.yml new file mode 100644 index 00000000000..417d903d945 --- /dev/null +++ b/addons/inter_company_rules/test/inter_company_po_to_so.yml @@ -0,0 +1,42 @@ +- + In order to check Inter-Company Rule of creating SO from PO, I login with user company A +- + !context + uid: 'res_users_company_a' +- + Quotation of purchase order. +- + !record {model: purchase.order, id: purchase_order_cmpa}: + partner_id: partner_company_b + company_id: company_a + order_line: + - product_id: product_consultant + name: 'Service' + price_unit: 450.0 +- + I confirm the order. +- + !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_cmpa} +- + Login with user of company B and check sale order. +- + !context + uid: 'res_users_company_b' +- + I check that the Quotation of sale order is created with proper data. +- + !python {model: sale.order}: | + #NOTE: Browse PO with superuser id because of different company of user. + po_name = self.pool.get('purchase.order').browse(cr, 1, ref("purchase_order_cmpa"), context=context).name + sale_order_ids = self.search(cr, uid, [('client_order_ref', '=', po_name)], context=context) + sale_order= self.browse(cr, uid, sale_order_ids[0], context=context) + assert sale_order.state == "draft", "sale order should be in draft state." + assert sale_order.partner_id.id == ref("partner_company_a"), "Supplier is not correspond to Company A." + assert sale_order.company_id.id == ref("company_b"), "Applied company in created sale order is incorrect." + assert sale_order.amount_total == 450.0, "Total amount is incorrect." + + #check for order line + assert sale_order.order_line[0].product_id.id == ref("product_consultant"), "Product in line is incorrect." + assert sale_order.order_line[0].name == 'Service', "Product name is incorrect." + assert sale_order.order_line[0].product_uom_qty == 1 , "Product qty is incorrect." + assert sale_order.order_line[0].price_unit == 450, "Unit Price in line is incorrect." diff --git a/addons/inter_company_rules/test/inter_company_so_to_po.yml b/addons/inter_company_rules/test/inter_company_so_to_po.yml new file mode 100644 index 00000000000..95e7711397d --- /dev/null +++ b/addons/inter_company_rules/test/inter_company_so_to_po.yml @@ -0,0 +1,46 @@ +- + In order to check Inter-Company Rule of creating PO from SO, I login with user company A +- + !context + uid: 'res_users_company_a' +- + I create Quotation of sale order and confirm it. +- + !record {model: sale.order, id: sale_order_cmpa, view: False}: + company_id: company_a + warehouse_id: warehouse_company_a + user_id: res_users_company_a + pricelist_id: 1 + partner_id: partner_company_b + partner_invoice_id: partner_company_b + partner_shipping_id: partner_company_b + order_line: + - product_id: product_consultant + name: 'Service' + price_unit: 450.0 +- + I confirm the order. +- + !python {model: sale.order}: | + self.action_button_confirm(cr, uid, [ref('sale_order_cmpa')], context=context) +- + I login with user company B. +- + !context + uid: 'res_users_company_b' +- + I check that Quotation of purchase order and order line is same as sale order +- + !python {model: purchase.order}: | + order_ids = self.search(cr, uid, [('company_id','=',ref("company_b"))], context=context) + purchase_order = self.browse(cr, uid, order_ids[0], context=context) + assert purchase_order.state == "draft", "Invoice should be in draft state." + assert purchase_order.partner_id.id == ref("partner_company_a"),"Supplier is not correspond to Company A." + assert purchase_order.company_id.id == ref("company_b"),"Company is not correspond to purchase order." + assert purchase_order.amount_total == 450.0, "Total amount is incorrect." + + assert purchase_order.order_line[0].product_id.id == ref("product_consultant"), "Product in line is incorrect." + assert purchase_order.order_line[0].name == 'Service',"Product name is incorrect." + assert purchase_order.order_line[0].price_unit == 450 , "Price unit is incorrect." + assert purchase_order.order_line[0].product_qty == 1 , "Product qty is incorrect." + assert purchase_order.order_line[0].price_subtotal == 450, "line total is incorrect." diff --git a/addons/inter_company_rules/test/test_intercompany_data.yml b/addons/inter_company_rules/test/test_intercompany_data.yml new file mode 100644 index 00000000000..bc0259ef6ab --- /dev/null +++ b/addons/inter_company_rules/test/test_intercompany_data.yml @@ -0,0 +1,147 @@ +- + !record {model: res.partner, id: partner_company_a, view: False}: + name: Company A + supplier: 1 +- + !record {model: res.company, id: company_a, view: False}: + name: Company A + parent_id: base.main_company + currency_id: base.EUR + partner_id: partner_company_a + so_from_po: 1 + po_from_so: 1 +- + !record {model: stock.location, id: location_stock_company_a, view: False}: + name: Stock - A + usage: internal + company_id: company_a +- + !record {model: stock.location, id: location_output_company_a, view: False}: + name: Output - A + usage: internal + company_id: company_a +- + !record {model: stock.warehouse, id: warehouse_company_a, view: False}: + name: purchase warehouse - A + code: CMPA + wh_input_stock_loc_id: location_stock_company_a + lot_stock_id: location_stock_company_a + wh_output_stock_loc_id: location_output_company_a + company_id: company_a +- + !record {model: res.partner, id: partner_company_b, view: False}: + name: Company B + supplier: 1 +- + !record {model: res.company, id: company_b, view: False}: + name: Company B + parent_id: base.main_company + currency_id: base.EUR + partner_id: partner_company_b + so_from_po: 1 + po_from_so: 1 +- + !record {model: stock.location, id: location_stock_company_b, view: False}: + name: Stock - B + usage: internal + company_id: company_b +- + !record {model: stock.location, id: location_output_company_b, view: False}: + name: Output - B + usage: internal + company_id: company_b +- + !record {model: stock.warehouse, id: warehouse_company_b, view: False}: + name: purchase warehouse - B + code: CMPB + wh_input_stock_loc_id: location_stock_company_b + lot_stock_id: location_stock_company_b + wh_output_stock_loc_id: location_output_company_b + company_id: company_b +- + Apply warehouse in company. +- + !record {model: res.company, id: company_a}: + warehouse_id: warehouse_company_a +- + !record {model: res.company, id: company_b}: + warehouse_id: warehouse_company_b +- + !record {model: product.product, id: product_consultant, view: False}: + name: Service + uom_id: product.product_uom_hour + uom_po_id: product.product_uom_hour + categ_id: product.product_category_all + type: service + company_id: False +- + Create a user for each company +- + !record {model: res.users, id: res_users_company_a, view: False}: + name: User A + login: usera + password: usera + email: usera@yourcompany.com + company_id: company_a + company_ids: + - company_a + groups_id: + - base.group_sale_salesman + - purchase.group_purchase_user +- + !record {model: res.users, id: res_users_company_b, view: False}: + name: User B + login: userb + password: userb + email: userb@yourcompany.com + company_id: company_b + company_ids: + - company_b + groups_id: + - base.group_sale_salesman + - purchase.group_purchase_user +- + Install COA for new companies. +- + !python {model: account.installer}: | + wiz_comp_a = self.create(cr, uid, {'charts': 'configurable', 'company_id': ref("company_a")}) + self.execute(cr, uid, [wiz_comp_a]) + wiz_comp_b = self.create(cr, uid, {'charts': 'configurable', 'company_id': ref("company_b")}) + self.execute(cr, uid, [wiz_comp_b]) +- + !python {model: wizard.multi.charts.accounts}: | + wiz_comp_a = self.create(cr, uid, { + 'chart_template_id': '1', + 'company_id': ref("company_a"), + 'currency_id': ref("base.EUR"), + 'code_digits': 6, + 'purchase_tax': False, + 'sale_tax': False}) + self.execute(cr, uid, [wiz_comp_a]) + wiz_comp_b = self.create(cr, uid, { + 'chart_template_id': '1', + 'company_id': ref("company_b"), + 'currency_id': ref("base.EUR"), + 'code_digits': 6, + 'purchase_tax': False, + 'sale_tax': False}) + self.execute(cr, uid, [wiz_comp_b]) +- + Now remove company from related partners of company and apply default accounts. +- + !python {model: res.partner}: | + account_obj = self.pool.get("account.account") + account_receivable_a = account_obj.search(cr, uid, [('company_id', '=', ref("company_a")), ('type', '=', 'receivable')], context=context) + account_payable_a = account_obj.search(cr, uid, [('company_id', '=', ref("company_a")), ('type', '=', 'payable')], context=context) + self.write(cr, uid, [ref("partner_company_a")], { + 'company_id': False, + 'property_account_receivable': account_receivable_a[0], + 'property_account_payable': account_payable_a[0] + }, context=context) + account_receivable_b = account_obj.search(cr, uid, [('company_id', '=', ref("company_b")), ('type', '=', 'receivable')], context=context) + account_payable_b = account_obj.search(cr, uid, [('company_id', '=', ref("company_b")), ('type', '=', 'payable')], context=context) + self.write(cr, uid, [ref("partner_company_b")], { + 'company_id': False, + 'property_account_receivable': account_receivable_b[0], + 'property_account_payable': account_payable_b[0] + }, context=context) diff --git a/addons/inter_company_rules/views/inter_company_so_po_view.xml b/addons/inter_company_rules/views/inter_company_so_po_view.xml new file mode 100644 index 00000000000..92ab0bb1ee1 --- /dev/null +++ b/addons/inter_company_rules/views/inter_company_so_po_view.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<odoo> + <data> + <record model="ir.ui.view" id="view_company_inter_change_inherit_form"> + <field name="name">res.company.form.inherit</field> + <field name="inherit_id" ref="base.view_company_form"/> + <field name="model">res.company</field> + <field name="arch" type="xml"> + <xpath expr="//notebook" position="inside"> + <page string="Inter-Company Rules"> + <group string="Inter-Company Rules"> + <field name="so_from_po" attrs="{'invisible': [('auto_generate_invoices', '=', True)]}"/> + <field name="po_from_so" attrs="{'invisible': [('auto_generate_invoices', '=', True)]}"/> + <field name="warehouse_id" attrs="{'required': [('po_from_so', '=', True)], 'invisible': [('po_from_so', '=', False)]}" domain="[('company_id', '=', active_id)]"/> + <field name="auto_validation" attrs="{'invisible': [('auto_generate_invoices', '=', True)]}"/> + <field name="auto_generate_invoices" attrs="{'invisible': ['|', ('so_from_po', '=', True), ('po_from_so', '=', True)]}"/> + <field name="intercompany_user_id" colspan="2" attrs="{'required': ['|','|',('so_from_po', '=', True), ('po_from_so', '=', True), ('auto_generate_invoices', '=', True)]}"/> + </group> + </page> + </xpath> + </field> + </record> + </data> +</odoo> diff --git a/addons/inter_company_rules/views/res_config_view.xml b/addons/inter_company_rules/views/res_config_view.xml new file mode 100644 index 00000000000..88516f0cb99 --- /dev/null +++ b/addons/inter_company_rules/views/res_config_view.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <record id="view_general_configuration_inter_company_rules" model="ir.ui.view"> + <field name="name">base.config.settings.inter.company.rules</field> + <field name="model">base.config.settings</field> + <field name="inherit_id" ref="base_setup.view_general_configuration"/> + <field name="arch" type="xml"> + <xpath expr="//div[@name='module_inter_company_rules']" position='after'> + <div name="module_inter_company_rules_company_id" attrs="{'invisible': [('module_inter_company_rules', '=', False)]}"> + <field name="company_id" widget="selection" class="oe_inline"/> + <field name="rule_type" widget="radio"/> + </div> + <div name="module_inter_company_rules_set_so_po" attrs="{'invisible': ['|', ('rule_type', '!=', 'so_and_po'), ('module_inter_company_rules', '=', False)]}"> + <field name="so_from_po"/> + <label for="so_from_po"/> + </div> + <div name="module_inter_company_rules_set_po_so" attrs="{'invisible': ['|', ('rule_type', '!=', 'so_and_po'), ('module_inter_company_rules', '=', False)]}"> + <field name="po_from_so"/> + <label for="po_from_so"/> + </div> + <div name="module_inter_company_rules_warehouse_id" attrs="{'invisible': ['|', ('po_from_so', '=', False), ('module_inter_company_rules', '=', False)]}"> + <label for="warehouse_id"/> + <field name="warehouse_id" class="oe_inline" attrs="{'required': [('po_from_so', '=', True), ('module_inter_company_rules', '=', True)]}" domain="[('company_id', '=', company_id)]"/> + </div> + <div name="module_inter_company_rules_set_auto_validation" attrs="{'invisible': ['|', ('rule_type', '!=', 'so_and_po'), ('module_inter_company_rules', '=', False)]}"> + <field name="auto_validation"/> + <label for="auto_validation"/> + </div> + </xpath> + </field> + </record> + </data> +</odoo> |
