diff --git a/bemade_multiple_billing_contacts/__init__.py b/bemade_multiple_billing_contacts/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/bemade_multiple_billing_contacts/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/bemade_multiple_billing_contacts/__manifest__.py b/bemade_multiple_billing_contacts/__manifest__.py new file mode 100644 index 0000000..5e4c2c7 --- /dev/null +++ b/bemade_multiple_billing_contacts/__manifest__.py @@ -0,0 +1,38 @@ +# +# Bemade Inc. +# +# Copyright (C) June 2023 Bemade Inc. (). +# Author: mdurepos (Contact : it@bemade.org) +# +# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1) +# It is forbidden to publish, distribute, sublicense, or sell copies of the Software +# or modified copies of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +{ + 'name': 'bemade_multiple_billing_contacts', + 'version': '15.0.1.0.1', + 'summary': 'Send invoices to multiple contacts by default.', + 'description': """By default, newly created invoices add all invoice addresses for the given partner as + followers on the invoice. If billing contacts are set manually on the sales order, those billing + contacts are added as followers on the invoice instead.""", + 'category': 'Invoicing Management', + 'author': 'Bemade Inc.', + 'website': 'https://www.bemade.org', + 'license': 'OPL-1', + 'depends': ['sale', + 'account', + 'bemade_partner_root_ancestor', + ], + 'data': [], + 'demo': [], + 'installable': True, + 'auto_install': False, +} diff --git a/bemade_multiple_billing_contacts/models/__init__.py b/bemade_multiple_billing_contacts/models/__init__.py new file mode 100644 index 0000000..fdc1257 --- /dev/null +++ b/bemade_multiple_billing_contacts/models/__init__.py @@ -0,0 +1,3 @@ +from . import res_partner +from . import sale_order +from . import account_move diff --git a/bemade_multiple_billing_contacts/models/account_move.py b/bemade_multiple_billing_contacts/models/account_move.py new file mode 100644 index 0000000..a12ddda --- /dev/null +++ b/bemade_multiple_billing_contacts/models/account_move.py @@ -0,0 +1,32 @@ +from odoo import models, fields, api + + +class AccountMove(models.Model): + _inherit = 'account.move' + + billing_contacts = fields.Many2many(comodel_name='res.partner', + string="Billing Contacts", + compute='_compute_billing_contacts', + inverse='_inverse_billing_contacts', + store=True) + + @api.depends('line_ids.sale_line_ids.order_id', 'partner_id') + def _compute_billing_contacts(self): + for rec in self: + order_id = rec.line_ids and rec.line_ids.mapped('sale_line_ids').mapped('order_id') + if order_id and len(order_id) == 1 and order_id.billing_contacts: + rec.billing_contacts = order_id.billing_contacts + else: + rec.billing_contacts = rec.partner_id.billing_contacts + + def _inverse_billing_contacts(self): + pass + + def _post(self, soft=True): + # Override the original method to subscribe the partner's billing contacts instead of self.partner_id + initial_subscribers = self.message_partner_ids.ids + final_subscribers = initial_subscribers + self.billing_contacts.ids + posted = super()._post() + self.message_unsubscribe([s.id for s in self.message_partner_ids if s not in initial_subscribers]) + self.message_subscribe(final_subscribers) + return posted diff --git a/bemade_multiple_billing_contacts/models/res_partner.py b/bemade_multiple_billing_contacts/models/res_partner.py new file mode 100644 index 0000000..ae988ff --- /dev/null +++ b/bemade_multiple_billing_contacts/models/res_partner.py @@ -0,0 +1,20 @@ +from odoo import models, fields, api, _, Command + + +class Partner(models.Model): + _inherit = 'res.partner' + + billing_contacts = fields.Many2many(string='Default Billing Contacts', + comodel_name='res.partner', + compute='_compute_billing_contacts', + inverse='_inverse_billing_contacts') + + @api.depends('child_ids.type') + def _compute_billing_contacts(self): + for rec in self: + rec.billing_contacts = rec.child_ids.filtered(lambda r: r.type == 'invoice') + + @api.depends('billing_contacts') + def _inverse_billing_contacts(self): + for partner in self.mapped('billing_contacts'): + partner.type = 'invoice' diff --git a/bemade_multiple_billing_contacts/models/sale_order.py b/bemade_multiple_billing_contacts/models/sale_order.py new file mode 100644 index 0000000..877cc5d --- /dev/null +++ b/bemade_multiple_billing_contacts/models/sale_order.py @@ -0,0 +1,19 @@ +from odoo import models, fields, api, _, Command + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + billing_contacts = fields.Many2many(comodel_name='res.partner', + string='Billing Contacts', + compute='_compute_billing_contacts', + inverse='_inverse_billing_contacts', + store=True) + + @api.depends('partner_id') + def _compute_billing_contacts(self): + for rec in self: + rec.billing_contacts = rec.partner_id.billing_contacts + + def _inverse_billing_contacts(self): + pass diff --git a/bemade_multiple_billing_contacts/tests/__init__.py b/bemade_multiple_billing_contacts/tests/__init__.py new file mode 100644 index 0000000..219fd8e --- /dev/null +++ b/bemade_multiple_billing_contacts/tests/__init__.py @@ -0,0 +1 @@ +from . import test_billing_contacts diff --git a/bemade_multiple_billing_contacts/tests/test_billing_contacts.py b/bemade_multiple_billing_contacts/tests/test_billing_contacts.py new file mode 100644 index 0000000..c99d4d5 --- /dev/null +++ b/bemade_multiple_billing_contacts/tests/test_billing_contacts.py @@ -0,0 +1,107 @@ +from odoo.tests import TransactionCase, tagged +from odoo import Command +import datetime + + +@tagged('-at_install', 'post_install') +class TestBillingContacts(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + partner = cls.env['res.partner'].create + cls.parent_co = partner({ + 'name': 'Partner', + 'company_type': 'company', + }) + cls.billing_contact1 = partner({ + 'name': 'Billing Contact 1', + 'company_type': 'person', + 'email': 'billingcontact1@partner.co', + 'parent_id': cls.parent_co.id, + 'type': 'invoice', + }) + cls.billing_contact2 = partner({ + 'name': 'Billing Contact 2', + 'company_type': 'person', + 'email': 'billingcontact2@partner.co', + 'parent_id': cls.parent_co.id, + 'type': 'invoice', + }) + cls.non_billing_contact = partner({ + 'name': 'Non-billing contact', + 'company_type': 'person', + 'email': 'not_billing@partner.co', + 'parent_id': cls.parent_co.id, + 'type': 'contact', + }) + cls.product = cls.env['product.product'].with_company(cls.parent_co.company_id).create({ + 'name': 'Product', + 'categ_id': cls.env['product.category'].create({'name': 'Product Category'}).id, + 'list_price': 100.0, + 'type': 'service', + 'uom_id': cls.env.ref('uom.product_uom_unit').id, + 'uom_po_id': cls.env.ref('uom.product_uom_unit').id, + 'default_code': 'PRODUCT-X', + 'invoice_policy': 'order', + 'expense_policy': 'no', + 'taxes_id': [(6, 0, [])], + 'supplier_taxes_id': [(6, 0, [])], + }) + + cls.sale_order = cls.env['sale.order'].create({ + 'partner_id': cls.parent_co.id, + 'client_order_ref': 'abc123', + }) + + cls.env['sale.order.line'].create({ + 'product_id': cls.product.id, + 'name': cls.product.name, + 'product_uom_qty': 2, + 'product_uom': cls.product.uom_id.id, + 'price_unit': cls.product.list_price, + 'order_id': cls.sale_order.id, + 'tax_id': False, + 'qty_delivered_manual': 2, + }) + + def test_billing_contacts_structure(self): + self.assertTrue(self.billing_contact1 in self.parent_co.billing_contacts) + self.assertTrue(self.billing_contact2 in self.parent_co.billing_contacts) + self.assertTrue(self.non_billing_contact not in self.parent_co.billing_contacts) + + def test_sale_order_default_billing_contacts(self): + self.assertTrue(self.sale_order.billing_contacts == self.parent_co.billing_contacts) + + def test_sale_order_change_contacts(self): + # Test that changing the billing contacts on an SO doesn't change them on the partner + # Validate that changing them manually on the SO transfers to the invoice + self.sale_order.write({'billing_contacts': [Command.link(self.non_billing_contact.id)]}) + self.assertTrue(all([c in self.sale_order.billing_contacts for c in self.parent_co.billing_contacts])) + self.assertTrue(self.non_billing_contact not in self.parent_co.billing_contacts) + self.assertTrue(self.non_billing_contact in self.sale_order.billing_contacts) + + def test_sale_order_to_invoice_contacts(self): + # Test that the invoices created from sales orders take the billing contacts configured on the SO + + self.sale_order.write({'billing_contacts': [Command.link(self.non_billing_contact.id)]}) + self.sale_order.action_confirm() + + wiz = self.env['sale.advance.payment.inv'].create({}) + invoice = wiz._create_invoice(self.sale_order,self.sale_order.order_line[0],self.sale_order.order_line.price_total) + self.assertTrue(invoice) + self.assertTrue(invoice.billing_contacts == self.sale_order.billing_contacts) + + def test_direct_invoice_contacts(self): + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.parent_co, + }) + self.assertTrue(self.parent_co.billing_contacts == invoice.billing_contacts) + + def test_invoice_followers_on_validate(self): + self.sale_order.action_confirm() + wiz = self.env['sale.advance.payment.inv'].create({}) + invoice = wiz._create_invoice(self.sale_order,self.sale_order.order_line[0],self.sale_order.order_line.price_total) + invoice.write({'date': datetime.date.today()}) + invoice.action_post() + self.assertTrue(all([r in invoice.message_partner_ids for r in self.parent_co.billing_contacts])) diff --git a/bemade_multiple_billing_contacts/views/res_partner_views.xml b/bemade_multiple_billing_contacts/views/res_partner_views.xml new file mode 100644 index 0000000..6bec7f8 --- /dev/null +++ b/bemade_multiple_billing_contacts/views/res_partner_views.xml @@ -0,0 +1,27 @@ + + + + + bemade_multiple_billing_contacts.res_partner.defaults.form + res.partner + + + + + + + + + + + + + + + + + + + + \ No newline at end of file