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'"
/>
+
+