diff --git a/sale_order_import/tests/test_order_import.py b/sale_order_import/tests/test_order_import.py index 344a1da96c..5b809d6bf8 100644 --- a/sale_order_import/tests/test_order_import.py +++ b/sale_order_import/tests/test_order_import.py @@ -7,7 +7,7 @@ from unittest import mock from odoo import exceptions -from odoo.tests import Form +from odoo.tests import Form, RecordCapturer from .common import TestCommon @@ -258,3 +258,114 @@ def test_order_import_log_errored_line(self): lambda m: messages[0] in m.body and messages[1] in m.body ) ) + + def test_create_missing_invoice_partner(self): + """Tests creation of missing invoice partner when ctx flag is True + + Expected behavior: the import workflow is not halted, and a new invoicing + partner is created on the fly. + """ + parsed_order = dict( + self.parsed_order, + invoice_to={ + "country_code": "FR", + "email": "test@invoice.partner", + "name": "Test Invoice Address", + }, + ) + wiz = self.wiz_model.with_context(create_missing_invoice_partner=True) + with RecordCapturer(self.env["res.partner"], []) as rc_partner: + order = wiz.create_order(parsed_order, "pricelist") + self.assertEqual(len(rc_partner.records), 1) + invoice_partner = rc_partner.records + self.assertEqual(invoice_partner.type, "invoice") + self.assertEqual(invoice_partner.parent_id, self.partner) + self.assertEqual(invoice_partner.country_id, self.env.ref("base.fr")) + self.assertEqual(invoice_partner.email, "test@invoice.partner") + self.assertEqual(invoice_partner.name, "Test Invoice Address") + self.assertEqual(order.partner_invoice_id, invoice_partner) + self.assertIn("Created invoice partner", order.message_ids[0].body) + + def test_create_missing_invoice_partner_disabled(self): + """Tests creation of missing invoice partner when ctx flag is False or not set + + Expected behavior: the import workflow is halted with an error. + """ + parsed_order = dict( + self.parsed_order, + invoice_to={ + "country_code": "FR", + "email": "test@invoice.partner", + "name": "Test Invoice Address", + }, + ) + wiz = self.wiz_model + + # No context flag + ctx = dict(wiz.env.context) + ctx.pop("create_missing_invoice_partner", None) + wiz = wiz.with_context(ctx) # pylint: disable=context-overridden + with self.assertRaises(exceptions.UserError): + wiz.create_order(parsed_order, "pricelist") + + # Context flag set to False + wiz = wiz.with_context(create_missing_invoice_partner=False) + with self.assertRaises(exceptions.UserError): + wiz.create_order(parsed_order, "pricelist") + + def test_create_missing_shipping_partner(self): + """Tests creation of missing shipping partner when ctx flag is True + + Expected behavior: the import workflow is not halted, and a new shipping + partner is created on the fly. + """ + parsed_order = dict( + self.parsed_order, + ship_to={ + "city": "Rome", + "country_code": "IT", + "street": "Via dei Platani", + "zip": "00100", + }, + ) + wiz = self.wiz_model.with_context(create_missing_shipping_partner=True, test=1) + with RecordCapturer(self.env["res.partner"], []) as rc_partner: + order = wiz.create_order(parsed_order, "pricelist") + self.assertEqual(len(rc_partner.records), 1) + shipping_partner = rc_partner.records + self.assertEqual(shipping_partner.type, "delivery") + self.assertEqual(shipping_partner.parent_id, self.partner) + self.assertEqual(shipping_partner.city, "Rome") + self.assertEqual(shipping_partner.country_id, self.env.ref("base.it")) + self.assertEqual(shipping_partner.street, "Via dei Platani") + self.assertEqual(shipping_partner.zip, "00100") + self.assertEqual(order.partner_shipping_id, shipping_partner) + self.assertIn("Created shipping partner", order.message_ids[0].body) + + def test_create_missing_shipping_partner_disabled(self): + """Tests creation of missing shipping partner when ctx flag is False or not set + + Expected behavior: the import workflow is halted with an error. + """ + parsed_order = dict( + self.parsed_order, + ship_to={ + "city": "Rome", + "country_code": "IT", + "street": "Via dei Platani", + "zip": "00100", + }, + ) + wiz = self.wiz_model + + # No context flag + ctx = dict(wiz.env.context) + ctx.pop("create_missing_shipping_partner", None) + wiz = wiz.with_context(ctx) # pylint: disable=context-overridden + with self.assertRaises(exceptions.UserError): + wiz.create_order(parsed_order, "pricelist") + + # Context flag set to False + wiz = wiz.with_context(create_missing_shipping_partner=False) + with self.assertRaises(exceptions.UserError): + wiz.create_order(parsed_order, "pricelist") diff --git a/sale_order_import/wizard/sale_order_import.py b/sale_order_import/wizard/sale_order_import.py index 2eca9ac60a..5930b25775 100644 --- a/sale_order_import/wizard/sale_order_import.py +++ b/sale_order_import/wizard/sale_order_import.py @@ -62,6 +62,9 @@ class SaleOrderImport(models.TransientModel): skip_error_lines = fields.Boolean( help="Ignore and push all error lines to the chatter when importing if enabled." ) + # Allow creating invoice/shipping partners on import + create_missing_invoice_partner = fields.Boolean(default=False) + create_missing_shipping_partner = fields.Boolean(default=False) @api.onchange("order_file") def order_file_change(self): @@ -196,6 +199,11 @@ def parse_pdf_order(self, order_file, detect_doc_type=False): # 'name': 'Camptocamp', # 'email': 'luc@camptocamp.com', # }, + # 'invoice_to': { # Same structure and fields as 'partner'; completely optional + # 'vat': 'FR25499247138', + # 'name': 'Camptocamp', + # 'email': 'luc@camptocamp.com', + # }, # 'ship_to': { # 'partner': partner_dict, # 'address': { @@ -260,18 +268,34 @@ def _prepare_order(self, parsed_order, price_source): so_vals = soo.play_onchanges(so_vals, ["partner_id"]) so_vals["order_line"] = [] if parsed_order.get("ship_to"): - shipping_partner = bdio._match_shipping_partner( - parsed_order["ship_to"], partner, parsed_order["chatter_msg"] - ) + try: + shipping_partner = bdio._match_shipping_partner( + parsed_order["ship_to"], partner, parsed_order["chatter_msg"] + ) + except UserError: + if not self._can_create_missing_shipping_partner(): + raise + shipping_partner = self._create_missing_shipping_partner( + parsed_order["ship_to"], partner, parsed_order["chatter_msg"] + ) so_vals["partner_shipping_id"] = shipping_partner.id if parsed_order.get("delivery_detail"): so_vals.update(parsed_order.get("delivery_detail")) if parsed_order.get("invoice_to"): - invoicing_partner = bdio._match_partner( - parsed_order["invoice_to"], parsed_order["chatter_msg"], partner_type="" - ) + try: + invoicing_partner = bdio._match_partner( + parsed_order["invoice_to"], + parsed_order["chatter_msg"], + partner_type="", + ) + except UserError: + if not self._can_create_missing_invoice_partner(): + raise + invoicing_partner = self._create_missing_invoice_partner( + parsed_order["invoice_to"], partner, parsed_order["chatter_msg"] + ) so_vals["partner_invoice_id"] = invoicing_partner.id if parsed_order.get("date"): so_vals["date_order"] = parsed_order["date"] @@ -399,9 +423,16 @@ def import_order_button(self): commercial_partner = partner.commercial_partner_id partner_shipping_id = False if parsed_order.get("ship_to"): - partner_shipping_id = bdio._match_shipping_partner( - parsed_order["ship_to"], partner, [] - ).id + try: + partner_shipping_id = bdio._match_shipping_partner( + parsed_order["ship_to"], partner, [] + ).id + except UserError: + if not self._can_create_missing_shipping_partner(): + raise + partner_shipping_id = self._create_missing_shipping_partner( + parsed_order["ship_to"], partner, [] + ).id existing_quotations = self.env["sale.order"].search( self._search_existing_order_domain( parsed_order, commercial_partner, [("state", "in", ("draft", "sent"))] @@ -465,9 +496,16 @@ def _prepare_update_order_vals(self, parsed_order, order, partner): ) vals = {"partner_id": partner.id} if parsed_order.get("ship_to"): - shipping_partner = bdio._match_shipping_partner( - parsed_order["ship_to"], partner, parsed_order["chatter_msg"] - ) + try: + shipping_partner = bdio._match_shipping_partner( + parsed_order["ship_to"], partner, parsed_order["chatter_msg"] + ) + except UserError: + if not self._can_create_missing_shipping_partner(): + raise + shipping_partner = self._create_missing_shipping_partner( + parsed_order["ship_to"], partner, parsed_order["chatter_msg"] + ) vals["partner_shipping_id"] = shipping_partner.id if parsed_order.get("order_ref"): vals["client_order_ref"] = parsed_order["order_ref"] @@ -712,3 +750,101 @@ def _post_error_lines_message(self, parsed_order, order): }, subtype_id=self.env.ref("mail.mt_note").id, ) + + def _can_create_missing_invoice_partner(self) -> bool: + """Checks if the current importer allows invoice address creation + + When called upon a record, checks field "create_missing_invoice_partner". + When called upon an empty recordset (eg: as a model method), checks context's + key "create_missing_invoice_partner". + + Hook method, can be overridden by inheriting modules. + """ + if self: + return self.create_missing_invoice_partner + return bool(self.env.context.get("create_missing_invoice_partner")) + + def _create_missing_invoice_partner(self, invoice_to_dict, partner, chatter_msg): + """Creates a new invoice partner for the current import""" + vals = self._create_missing_invoice_partner_values( + invoice_to_dict, partner, chatter_msg + ) + invoice_partner = self.env["res.partner"].create(vals) + chatter_msg.append( + self.env._("Created invoice partner '%s'", invoice_partner.display_name) + ) + return invoice_partner + + def _create_missing_invoice_partner_values( + self, invoice_to_dict, partner, chatter_msg + ): + """Prepares a new invoice partner's values for the current import""" + vals = invoice_to_dict.copy() + vals["type"] = "invoice" + vals["parent_id"] = partner.id + if country_code := vals.pop("country_code", None): + country = self.env["res.country"].search( + [("code", "=", country_code)], limit=1 + ) + if country: + vals["country_id"] = country.id + if state_code := vals.pop("state_code", None) and vals.get("country_id"): + state = self.env["res.country.state"].search( + [ + ("code", "=", state_code), + ("country_id", "=", vals["country_id"]), + ], + limit=1, + ) + if state: + vals["state_id"] = state.id + return vals + + def _can_create_missing_shipping_partner(self) -> bool: + """Checks if the current importer allows shipping address creation + + When called upon a record, checks field "create_missing_shipping_partner". + When called upon an empty recordset (eg: as a model method), checks context's + key "create_missing_shipping_partner". + + Hook method, can be overridden by inheriting modules. + """ + if self: + return self.create_missing_shipping_partner + return bool(self.env.context.get("create_missing_shipping_partner")) + + def _create_missing_shipping_partner(self, ship_to_dict, partner, chatter_msg): + """Creates a new shipping partner for the current import""" + vals = self._create_missing_shipping_partner_values( + ship_to_dict, partner, chatter_msg + ) + shipping_partner = self.env["res.partner"].create(vals) + chatter_msg.append( + self.env._("Created shipping partner '%s'", shipping_partner.display_name) + ) + return shipping_partner + + def _create_missing_shipping_partner_values( + self, ship_to_dict, partner, chatter_msg + ): + """Prepares a new shipping partner's values for the current import""" + vals = ship_to_dict.copy() + vals["type"] = "delivery" + vals["parent_id"] = partner.id + if country_code := vals.pop("country_code", None): + country = self.env["res.country"].search( + [("code", "=", country_code)], limit=1 + ) + if country: + vals["country_id"] = country.id + if state_code := vals.pop("state_code", None) and vals.get("country_id"): + state = self.env["res.country.state"].search( + [ + ("code", "=", state_code), + ("country_id", "=", vals["country_id"]), + ], + limit=1, + ) + if state: + vals["state_id"] = state.id + return vals diff --git a/sale_order_import/wizard/sale_order_import_view.xml b/sale_order_import/wizard/sale_order_import_view.xml index d09e74f16d..5f50b038e8 100644 --- a/sale_order_import/wizard/sale_order_import_view.xml +++ b/sale_order_import/wizard/sale_order_import_view.xml @@ -60,6 +60,8 @@ required="doc_type == 'order'" /> + +