diff --git a/.gitignore b/.gitignore
index 574795e..3f5d3d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
#
+# Ignore Opened csv files
+.~lock.*
# Do not share pdf file. (grap-odoo-business-supplier-invoice)
*.pdf
#
diff --git a/grap_custom_import_account_product_fiscal_classification/README.rst b/grap_custom_import_account_product_fiscal_classification/README.rst
new file mode 100644
index 0000000..e69de29
diff --git a/grap_custom_import_account_product_fiscal_classification/__init__.py b/grap_custom_import_account_product_fiscal_classification/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/grap_custom_import_account_product_fiscal_classification/__manifest__.py b/grap_custom_import_account_product_fiscal_classification/__manifest__.py
new file mode 100644
index 0000000..3e11ffb
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/__manifest__.py
@@ -0,0 +1,17 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+{
+ "name": "GRAP - Custom Import Fiscal Classification",
+ "summary": "Extra GRAP Tools to import data for"
+ " Account Product Fiscal Classification",
+ "version": "16.0.1.0.0",
+ "category": "Tools",
+ "author": "GRAP",
+ "website": "https://github.com/grap/grap-odoo-import",
+ "license": "AGPL-3",
+ "depends": ["grap_custom_import_product", "account_product_fiscal_classification"],
+ "auto_install": True,
+ "installable": True,
+}
diff --git a/grap_custom_import_account_product_fiscal_classification/models/__init__.py b/grap_custom_import_account_product_fiscal_classification/models/__init__.py
new file mode 100644
index 0000000..5c74c8c
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/models/__init__.py
@@ -0,0 +1 @@
+from . import product_product
diff --git a/grap_custom_import_account_product_fiscal_classification/models/product_product.py b/grap_custom_import_account_product_fiscal_classification/models/product_product.py
new file mode 100644
index 0000000..fd0193f
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/models/product_product.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo import _, fields, models
+from odoo.exceptions import ValidationError
+from odoo.osv import expression
+
+
+class ProductProduct(models.Model):
+ _inherit = "product.product"
+
+ grap_import_vat_amount = fields.Float(string="VAT Amount (For import)", store=False)
+
+ # pylint: disable=missing-return
+ def _custom_import_hook_vals(self, old_vals, new_vals):
+ super()._custom_import_hook_vals(old_vals, new_vals)
+ self._custom_import_handle_fiscal_classification_id(old_vals, new_vals)
+
+ def _custom_import_get_fiscal_classifications(self, vat_amount):
+ domain = expression.OR(
+ [[("company_id", "=", self.env.company.id)], [("company_id", "=", False)]]
+ )
+ if vat_amount:
+ domain = expression.AND(
+ [domain, [("sale_tax_ids.amount", "=", 100 * vat_amount)]]
+ )
+ else:
+ domain = expression.AND([domain, [("sale_tax_ids", "=", False)]])
+
+ return (
+ self.env["account.product.fiscal.classification"]
+ .search(domain)
+ .filtered(lambda x: len(x.sale_tax_ids) < 2)
+ )
+
+ def _custom_import_handle_fiscal_classification_id(self, old_vals, new_vals):
+ vat_amount = old_vals.get("grap_import_vat_amount")
+ if not vat_amount and not self.env.context.get("install_mode"):
+ raise ValidationError(
+ _(
+ "No VAT Amount found for the product %(product_name)s",
+ product_name=old_vals.get("name"),
+ )
+ )
+ classifications = self._custom_import_get_fiscal_classifications(vat_amount)
+
+ if len(classifications) == 1:
+ new_vals["fiscal_classification_id"] = classifications.id
+ return
+
+ elif len(classifications) == 0:
+ raise ValidationError(
+ _(
+ "No Fiscal Classification Found for the product %(product_name)s."
+ " Vat Amount %(vat_amount)s",
+ product_name=old_vals.get("name"),
+ vat_amount=vat_amount,
+ )
+ )
+
+ raise ValidationError(
+ _(
+ "Many Fiscal Classifications Found for the product %(product_name)s."
+ " Vat Amount %(vat_amount)s. Fiscal Classifications : %(classification_names)s",
+ product_name=old_vals.get("name"),
+ vat_amount=vat_amount,
+ classification_names=",".join(classifications.mapped("name")),
+ )
+ )
diff --git a/grap_custom_import_account_product_fiscal_classification/readme/CONTRIBUTORS.rst b/grap_custom_import_account_product_fiscal_classification/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000..9f76a75
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Sylvain LE GAL
diff --git a/grap_custom_import_account_product_fiscal_classification/readme/DESCRIPTION.rst b/grap_custom_import_account_product_fiscal_classification/readme/DESCRIPTION.rst
new file mode 100644
index 0000000..6723531
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/readme/DESCRIPTION.rst
@@ -0,0 +1,5 @@
+This module improve the "import" features provided by Odoo.
+
+* ``product.product``:
+
+ * Allow to recover ``multiplier_qty`` field in the supplier info level.
diff --git a/grap_custom_import_account_product_fiscal_classification/readme/ROADMAP.rst b/grap_custom_import_account_product_fiscal_classification/readme/ROADMAP.rst
new file mode 100644
index 0000000..a09c923
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/readme/ROADMAP.rst
@@ -0,0 +1,2 @@
+* handle selection of classifications for ``recurring_consignment``,
+ once the module is ported in V16.
\ No newline at end of file
diff --git a/grap_custom_import_account_product_fiscal_classification/tests/__init__.py b/grap_custom_import_account_product_fiscal_classification/tests/__init__.py
new file mode 100644
index 0000000..d9b96c4
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_module
diff --git a/grap_custom_import_account_product_fiscal_classification/tests/templates/product.product/product.csv b/grap_custom_import_account_product_fiscal_classification/tests/templates/product.product/product.csv
new file mode 100644
index 0000000..b6b521e
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/tests/templates/product.product/product.csv
@@ -0,0 +1,2 @@
+name,uom_id,categ_id,barcode,list_price,grap_import_supplier_name,grap_import_supplier_product_code,grap_import_supplier_product_name,grap_import_supplier_gross_price,grap_import_vat_amount
+Mention Good (Late chocolate),Units,All / Saleable,3222472195092,2.29,Ready Mat,GOOD,MENTION GOOD,1.98,0.20
diff --git a/grap_custom_import_account_product_fiscal_classification/tests/test_module.py b/grap_custom_import_account_product_fiscal_classification/tests/test_module.py
new file mode 100644
index 0000000..8c707d0
--- /dev/null
+++ b/grap_custom_import_account_product_fiscal_classification/tests/test_module.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo.tests import tagged
+
+from odoo.addons.grap_custom_import_product.tests.test_module import TestModuleProduct
+
+
+@tagged("post_install", "-at_install")
+class TestModuleProductSupplierinfoQtyMultiplier(TestModuleProduct):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.ProductProduct = cls.env["product.product"]
+ cls.classification_20 = cls.env.ref(
+ "account_product_fiscal_classification.fiscal_classification_A_company_1"
+ )
+
+ def test_01_import_product_account_product_fiscal_classification(self):
+ products, messages = self._test_import_file(
+ "grap_custom_import_account_product_fiscal_classification",
+ "product.product",
+ "product.csv",
+ )
+ self.assertFalse(messages)
+ self.assertEqual(len(products), 1)
+ self.assertEqual(products.fiscal_classification_id, self.classification_20)
diff --git a/grap_custom_import_base/README.rst b/grap_custom_import_base/README.rst
new file mode 100644
index 0000000..e69de29
diff --git a/grap_custom_import_base/__init__.py b/grap_custom_import_base/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/grap_custom_import_base/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/grap_custom_import_base/__manifest__.py b/grap_custom_import_base/__manifest__.py
new file mode 100644
index 0000000..6d27624
--- /dev/null
+++ b/grap_custom_import_base/__manifest__.py
@@ -0,0 +1,15 @@
+# Copyright (C) 2019 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+{
+ "name": "GRAP - Custom Import Base Module",
+ "summary": "Extra GRAP Tools to import data for base module",
+ "version": "16.0.1.0.0",
+ "category": "Tools",
+ "author": "GRAP",
+ "website": "https://github.com/grap/grap-odoo-import",
+ "license": "AGPL-3",
+ "depends": ["base_import"],
+ "installable": True,
+}
diff --git a/grap_custom_import_base/models/__init__.py b/grap_custom_import_base/models/__init__.py
new file mode 100644
index 0000000..79c0518
--- /dev/null
+++ b/grap_custom_import_base/models/__init__.py
@@ -0,0 +1,2 @@
+from . import custom_import_mixin
+from . import res_partner
diff --git a/grap_custom_import_base/models/custom_import_mixin.py b/grap_custom_import_base/models/custom_import_mixin.py
new file mode 100644
index 0000000..8b155c7
--- /dev/null
+++ b/grap_custom_import_base/models/custom_import_mixin.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo import _, models
+from odoo.exceptions import ValidationError
+
+
+class CustomImportMixin(models.AbstractModel):
+ _name = "custom.import.mixin"
+ _description = "Mixin providing helper for imports"
+
+ def _custom_import_prevent_duplicate_fields(self):
+ """Define the fields that will be used to prevent
+ Duplicates in the import"""
+ return []
+
+ def _custom_import_hook_vals(self, old_vals, new_vals):
+ # Check if existing duplicates are present in the database
+ for field in self._custom_import_prevent_duplicate_fields():
+ if new_vals.get(field):
+ items = self.search([(field, "=", new_vals.get(field))])
+ if items:
+ raise ValidationError(
+ _(
+ "The following items still exist in the database"
+ " for the field %(field)s. Values: %(values)s",
+ field=field,
+ values=",".join(items.mapped(field)),
+ )
+ )
+ return new_vals
+
+ def _custom_import_check_duplicates_new_vals(self, vals_list):
+ for field in self._custom_import_prevent_duplicate_fields():
+ duplicates = []
+ for vals in vals_list:
+ if vals.get(field):
+ if vals[field] in duplicates:
+ raise ValidationError(
+ _(
+ "The file contain many item(s) with"
+ " the same value '%(value)s' for the"
+ " field '%(field)s'.",
+ field=field,
+ value=vals[field],
+ )
+ )
+ duplicates.append(vals[field])
+
+ def _custom_import_get_or_create(
+ self, model_name, search_field_name, vals, field_name
+ ):
+ ItemModel = self.env[model_name]
+ if not vals.get(field_name):
+ return False
+ items = ItemModel.search([(search_field_name, "=", vals[field_name])])
+ if len(items) == 0:
+ return ItemModel.create({"name": vals[field_name]})
+ elif len(items) == 1:
+ return items
+ elif len(items) >= 2:
+ raise ValidationError(
+ _(
+ "%(item_qty)d items found for the field %(field_name)s."
+ " Value: '%(value)s'."
+ ),
+ item_qty=len(items),
+ field_name=field_name,
+ value=vals["field_name"],
+ )
+
+ # Overload Section
+ def _load_records_create(self, vals_list):
+ new_vals_list = []
+ for vals in vals_list:
+ new_vals = vals.copy()
+ self._custom_import_hook_vals(vals, new_vals)
+ new_vals_list.append(new_vals)
+ # TODO, move this check in another part
+ self._custom_import_check_duplicates_new_vals(new_vals_list)
+ return super()._load_records_create(new_vals_list)
diff --git a/grap_custom_import_base/models/res_partner.py b/grap_custom_import_base/models/res_partner.py
new file mode 100644
index 0000000..4dcc46c
--- /dev/null
+++ b/grap_custom_import_base/models/res_partner.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+import logging
+
+from odoo import models
+
+logger = logging.getLogger(__name__)
+
+
+class ResPartner(models.Model):
+ _name = "res.partner"
+ _inherit = ["res.partner", "custom.import.mixin"]
+
+ def _custom_import_prevent_duplicate_fields(self):
+ res = super()._custom_import_prevent_duplicate_fields()
+ res += ["name", "vat"]
+ return res
diff --git a/grap_custom_import_base/readme/CONTRIBUTORS.rst b/grap_custom_import_base/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000..9f76a75
--- /dev/null
+++ b/grap_custom_import_base/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Sylvain LE GAL
diff --git a/grap_custom_import_base/readme/DESCRIPTION.rst b/grap_custom_import_base/readme/DESCRIPTION.rst
new file mode 100644
index 0000000..2a7a089
--- /dev/null
+++ b/grap_custom_import_base/readme/DESCRIPTION.rst
@@ -0,0 +1,7 @@
+This module improve the "import" features provided by Odoo.
+
+It provides generic tools for that purpose, and improve imports for some models.
+
+* ``res.partner``:
+
+ * Prevent to create duplicates regarding ``name`` and ``vat`` fields.
\ No newline at end of file
diff --git a/grap_custom_import_base/tests/__init__.py b/grap_custom_import_base/tests/__init__.py
new file mode 100644
index 0000000..d9b96c4
--- /dev/null
+++ b/grap_custom_import_base/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_module
diff --git a/grap_custom_import_base/tests/templates/res.partner/supplier.csv b/grap_custom_import_base/tests/templates/res.partner/supplier.csv
new file mode 100644
index 0000000..2d35ac1
--- /dev/null
+++ b/grap_custom_import_base/tests/templates/res.partner/supplier.csv
@@ -0,0 +1,2 @@
+name,email,phone,mobile,website,street,street2,zip,city,country_id,vat
+Relais Vert,contact84@relais-vert.com,04 90 67 23 72,06 12 34 56 78,https://www.relais-vert.com/,ZONE BELLECOUR 3,621 Allée BELLECOUR,84200,CARPENTRAS,France,FR72352867493
diff --git a/grap_custom_import_base/tests/templates/res.partner/supplier_new_duplicates_vat.csv b/grap_custom_import_base/tests/templates/res.partner/supplier_new_duplicates_vat.csv
new file mode 100644
index 0000000..9aeb3d0
--- /dev/null
+++ b/grap_custom_import_base/tests/templates/res.partner/supplier_new_duplicates_vat.csv
@@ -0,0 +1,3 @@
+name,email,phone,mobile,website,street,street2,zip,city,country_id,vat
+Relais Vert,contact84@relais-vert.com,04 90 67 23 72,06 12 34 56 78,https://www.relais-vert.com/,ZONE BELLECOUR 3,621 Allée BELLECOUR,84200,CARPENTRAS,France,FR72352867493
+Reloud Vert,contact84@relais-vert.com,04 90 67 23 72,06 12 34 56 78,https://www.relais-vert.com/,ZONE BELLECOUR 3,621 Allée BELLECOUR,84200,CARPENTRAS,France,FR72352867493
diff --git a/grap_custom_import_base/tests/test_module.py b/grap_custom_import_base/tests/test_module.py
new file mode 100644
index 0000000..1294863
--- /dev/null
+++ b/grap_custom_import_base/tests/test_module.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo.modules.module import get_module_resource
+from odoo.tests import tagged
+from odoo.tests.common import TransactionCase
+
+
+@tagged("post_install", "-at_install")
+class TestModuleBase(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.ResPartner = cls.env["res.partner"]
+ cls.Wizard = cls.env["base_import.import"]
+
+ def _test_import_file(self, module, model, file_name):
+ preview_options = {"headers": True, "quoting": '"'}
+ import_options = {"has_headers": True, "quoting": '"'}
+
+ # Read File
+ file_path = get_module_resource(module, "tests/templates/", model, file_name)
+ extension = file_path.split(".")[-1]
+ if extension == "csv":
+ file_type = "text/csv"
+ else:
+ file_type = "Unimplemented Extension"
+
+ file_content = open(file_path, "rb").read()
+
+ # Create Wizard
+ import_wizard = self.Wizard.create(
+ {"res_model": model, "file": file_content, "file_type": file_type}
+ )
+
+ # Run Preview
+ result_parse = import_wizard.parse_preview(preview_options)
+ column_list = [x[0] for x in result_parse["preview"]]
+
+ # Execute Import
+ results = import_wizard.execute_import(column_list, column_list, import_options)
+
+ items = self.env[model].browse(results.get("ids"))
+ return items, results["messages"]
+
+ def test_01_import_supplier(self):
+ partners, messages = self._test_import_file(
+ "grap_custom_import_base", "res.partner", "supplier.csv"
+ )
+ self.assertFalse(messages)
+ self.assertEqual(len(partners), 1)
+ self.assertEqual(partners.name, "Relais Vert")
+
+ def test_02_existing_duplicates_name(self):
+ partners, messages = self._test_import_file(
+ "grap_custom_import_base", "res.partner", "supplier.csv"
+ )
+ self.assertFalse(messages)
+ partners, messages = self._test_import_file(
+ "grap_custom_import_base", "res.partner", "supplier.csv"
+ )
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0].get("type"), "error")
+
+ def test_03_import_supplier_new_duplicates_vat(self):
+ partners, messages = self._test_import_file(
+ "grap_custom_import_base", "res.partner", "supplier_new_duplicates_vat.csv"
+ )
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0].get("type"), "error")
diff --git a/grap_custom_import_product/README.rst b/grap_custom_import_product/README.rst
new file mode 100644
index 0000000..e69de29
diff --git a/grap_custom_import_product/__init__.py b/grap_custom_import_product/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/grap_custom_import_product/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/grap_custom_import_product/__manifest__.py b/grap_custom_import_product/__manifest__.py
new file mode 100644
index 0000000..2604767
--- /dev/null
+++ b/grap_custom_import_product/__manifest__.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2019 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+{
+ "name": "GRAP - Custom Import Product Module",
+ "summary": "Extra GRAP Tools to import data for product module",
+ "version": "16.0.1.0.0",
+ "category": "Tools",
+ "author": "GRAP",
+ "website": "https://github.com/grap/grap-odoo-import",
+ "license": "AGPL-3",
+ "depends": ["grap_custom_import_base", "product"],
+ "auto_install": True,
+ "installable": True,
+}
diff --git a/grap_custom_import_product/models/__init__.py b/grap_custom_import_product/models/__init__.py
new file mode 100644
index 0000000..5c74c8c
--- /dev/null
+++ b/grap_custom_import_product/models/__init__.py
@@ -0,0 +1 @@
+from . import product_product
diff --git a/grap_custom_import_product/models/product_product.py b/grap_custom_import_product/models/product_product.py
new file mode 100644
index 0000000..fb0e04d
--- /dev/null
+++ b/grap_custom_import_product/models/product_product.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+ _name = "product.product"
+ _inherit = ["product.product", "custom.import.mixin"]
+
+ def _custom_import_prevent_duplicate_fields(self):
+ res = super()._custom_import_prevent_duplicate_fields()
+ res += ["name", "barcode"]
+ return res
+
+ grap_import_supplier_name = fields.Char(
+ string="Supplier Name (For import)", store=False
+ )
+ grap_import_supplier_product_code = fields.Char(
+ string="Product Code - Supplier (For import)", store=False
+ )
+ grap_import_supplier_product_name = fields.Char(
+ string="Product Name - Supplier (For import)", store=False
+ )
+ grap_import_supplier_min_qty = fields.Monetary(
+ string="Product Min Quantity - Supplier (For import)", store=False
+ )
+ grap_import_supplier_gross_price = fields.Monetary(
+ string="Product Gross Price - Supplier (For import)", store=False
+ )
+
+ # pylint: disable=missing-return
+ def _custom_import_hook_vals(self, old_vals, new_vals):
+ super()._custom_import_hook_vals(old_vals, new_vals)
+ self._custom_import_handle_supplierinfo_vals(old_vals, new_vals)
+
+ def _custom_import_handle_supplierinfo_vals(self, old_vals, new_vals):
+ supplier = self._custom_import_get_or_create(
+ "res.partner", "name", old_vals, "grap_import_supplier_name"
+ )
+ if supplier:
+ new_vals.update(
+ {
+ "seller_ids": [
+ (
+ 0,
+ False,
+ self._custom_import_prepare_supplierinfo_vals(
+ supplier, old_vals
+ ),
+ )
+ ],
+ "standard_price": self._custom_import_prepare_standard_price(
+ old_vals
+ ),
+ }
+ )
+
+ def _custom_import_prepare_supplierinfo_vals(self, partner, vals):
+ return {
+ "partner_id": partner.id,
+ "price": vals.get("grap_import_supplier_gross_price"),
+ "product_code": vals.get("grap_import_supplier_product_code"),
+ "product_name": vals.get("grap_import_supplier_product_name"),
+ "min_qty": vals.get("grap_import_supplier_min_qty"),
+ }
+
+ def _custom_import_prepare_standard_price(self, vals):
+ return vals.get("grap_import_supplier_gross_price")
diff --git a/grap_custom_import_product/readme/CONTRIBUTORS.rst b/grap_custom_import_product/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000..9f76a75
--- /dev/null
+++ b/grap_custom_import_product/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Sylvain LE GAL
diff --git a/grap_custom_import_product/readme/DESCRIPTION.rst b/grap_custom_import_product/readme/DESCRIPTION.rst
new file mode 100644
index 0000000..8bdabeb
--- /dev/null
+++ b/grap_custom_import_product/readme/DESCRIPTION.rst
@@ -0,0 +1,9 @@
+This module improve the "import" features provided by Odoo.
+
+It provides generic tools for that purpose, and improve imports for some models.
+
+* ``product.product``:
+
+ * Prevent to create duplicates regarding ``name`` and ``barcode`` fields.
+
+ * Allow to create main ``seller_ids``, based on supplier information fields.
diff --git a/grap_custom_import_product/tests/__init__.py b/grap_custom_import_product/tests/__init__.py
new file mode 100644
index 0000000..d9b96c4
--- /dev/null
+++ b/grap_custom_import_product/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_module
diff --git a/grap_custom_import_product/tests/templates/product.product/product.csv b/grap_custom_import_product/tests/templates/product.product/product.csv
new file mode 100644
index 0000000..c453af2
--- /dev/null
+++ b/grap_custom_import_product/tests/templates/product.product/product.csv
@@ -0,0 +1,4 @@
+name,uom_id,categ_id,barcode,list_price,grap_import_supplier_name,grap_import_supplier_product_code,grap_import_supplier_product_name,grap_import_supplier_gross_price
+Coca Cola (Import),Units,All / Saleable / Office Furniture,5000112602791,4.12,Coke Corp,CC,BOTTLE 33CL,3.33
+Produit B,Units,All / Saleable / Office Furniture,,8.00,Supplier From Product Import,PB,product_B,6.00
+Produit C,Units,All / Saleable,,2.30,,,,
diff --git a/grap_custom_import_product/tests/test_module.py b/grap_custom_import_product/tests/test_module.py
new file mode 100644
index 0000000..118aa66
--- /dev/null
+++ b/grap_custom_import_product/tests/test_module.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo.tests import tagged
+
+from odoo.addons.grap_custom_import_base.tests.test_module import TestModuleBase
+
+
+@tagged("post_install", "-at_install")
+class TestModuleProduct(TestModuleBase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.ProductProduct = cls.env["product.product"]
+
+ def test_01_import_product(self):
+ products, messages = self._test_import_file(
+ "grap_custom_import_product", "product.product", "product.csv"
+ )
+ self.assertFalse(messages)
+ self.assertEqual(len(products), 3)
+ coca_cola = products.filtered(lambda x: x.name == "Coca Cola (Import)")
+ self.assertEqual(len(coca_cola), 1)
+ self.assertEqual(coca_cola.standard_price, 3.33)
+ self.assertEqual(coca_cola.mapped("seller_ids.partner_id.name"), ["Coke Corp"])
+ self.assertEqual(coca_cola.mapped("seller_ids.product_code"), ["CC"])
+ self.assertEqual(coca_cola.mapped("seller_ids.product_name"), ["BOTTLE 33CL"])
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/README.rst b/grap_custom_import_product_supplierinfo_qty_multiplier/README.rst
new file mode 100644
index 0000000..e69de29
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/__init__.py b/grap_custom_import_product_supplierinfo_qty_multiplier/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/__manifest__.py b/grap_custom_import_product_supplierinfo_qty_multiplier/__manifest__.py
new file mode 100644
index 0000000..7252558
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/__manifest__.py
@@ -0,0 +1,17 @@
+# Copyright (C) 2019 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+{
+ "name": "GRAP - Custom Import Product Supplierinfo Quantity Multiplier Module",
+ "summary": "Extra GRAP Tools to import data for"
+ " product Supplierinfo Quantity Multiplier module",
+ "version": "16.0.1.0.0",
+ "category": "Tools",
+ "author": "GRAP",
+ "website": "https://github.com/grap/grap-odoo-import",
+ "license": "AGPL-3",
+ "depends": ["grap_custom_import_product", "product_supplierinfo_qty_multiplier"],
+ "auto_install": True,
+ "installable": True,
+}
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/models/__init__.py b/grap_custom_import_product_supplierinfo_qty_multiplier/models/__init__.py
new file mode 100644
index 0000000..5c74c8c
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/models/__init__.py
@@ -0,0 +1 @@
+from . import product_product
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/models/product_product.py b/grap_custom_import_product_supplierinfo_qty_multiplier/models/product_product.py
new file mode 100644
index 0000000..83921a7
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/models/product_product.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo import fields, models
+
+
+class ProductProduct(models.Model):
+ _inherit = "product.product"
+
+ grap_import_supplier_multiplier_qty = fields.Float(
+ string="Product Package Quantity - Supplier (For import)", store=False
+ )
+
+ def _custom_import_prepare_supplierinfo_vals(self, partner, vals):
+ res = super()._custom_import_prepare_supplierinfo_vals(partner, vals)
+ res["multiplier_qty"] = vals.get("grap_import_supplier_multiplier_qty")
+ return res
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/readme/CONTRIBUTORS.rst b/grap_custom_import_product_supplierinfo_qty_multiplier/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000..9f76a75
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Sylvain LE GAL
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/readme/DESCRIPTION.rst b/grap_custom_import_product_supplierinfo_qty_multiplier/readme/DESCRIPTION.rst
new file mode 100644
index 0000000..6723531
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/readme/DESCRIPTION.rst
@@ -0,0 +1,5 @@
+This module improve the "import" features provided by Odoo.
+
+* ``product.product``:
+
+ * Allow to recover ``multiplier_qty`` field in the supplier info level.
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/tests/__init__.py b/grap_custom_import_product_supplierinfo_qty_multiplier/tests/__init__.py
new file mode 100644
index 0000000..d9b96c4
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_module
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/tests/templates/product.product/product.csv b/grap_custom_import_product_supplierinfo_qty_multiplier/tests/templates/product.product/product.csv
new file mode 100644
index 0000000..dfa2cc3
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/tests/templates/product.product/product.csv
@@ -0,0 +1,2 @@
+name,uom_id,categ_id,barcode,list_price,grap_import_supplier_name,grap_import_supplier_product_code,grap_import_supplier_product_name,grap_import_supplier_gross_price,grap_import_supplier_multiplier_qty
+Mention Good (Late chocolate),Units,All / Saleable,3222472195092,2.29,Ready Mat,GOOD,MENTION GOOD,1.98,24
diff --git a/grap_custom_import_product_supplierinfo_qty_multiplier/tests/test_module.py b/grap_custom_import_product_supplierinfo_qty_multiplier/tests/test_module.py
new file mode 100644
index 0000000..d2fe301
--- /dev/null
+++ b/grap_custom_import_product_supplierinfo_qty_multiplier/tests/test_module.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop)
+# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo.tests import tagged
+
+from odoo.addons.grap_custom_import_product.tests.test_module import TestModuleProduct
+
+
+@tagged("post_install", "-at_install")
+class TestModuleProductSupplierinfoQtyMultiplier(TestModuleProduct):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.ProductProduct = cls.env["product.product"]
+
+ def test_01_import_product_supplierinfo_qty_multiplier(self):
+ products, messages = self._test_import_file(
+ "grap_custom_import_product_supplierinfo_qty_multiplier",
+ "product.product",
+ "product.csv",
+ )
+ self.assertFalse(messages)
+ self.assertEqual(len(products), 1)
+ self.assertEqual(products.mapped("seller_ids.multiplier_qty"), [24.0])
diff --git a/setup/grap_custom_import_account_product_fiscal_classification/odoo/addons/grap_custom_import_account_product_fiscal_classification b/setup/grap_custom_import_account_product_fiscal_classification/odoo/addons/grap_custom_import_account_product_fiscal_classification
new file mode 120000
index 0000000..5b341ff
--- /dev/null
+++ b/setup/grap_custom_import_account_product_fiscal_classification/odoo/addons/grap_custom_import_account_product_fiscal_classification
@@ -0,0 +1 @@
+../../../../grap_custom_import_account_product_fiscal_classification
\ No newline at end of file
diff --git a/setup/grap_custom_import_account_product_fiscal_classification/setup.py b/setup/grap_custom_import_account_product_fiscal_classification/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/grap_custom_import_account_product_fiscal_classification/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/grap_custom_import_base/odoo/addons/grap_custom_import_base b/setup/grap_custom_import_base/odoo/addons/grap_custom_import_base
new file mode 120000
index 0000000..45b52aa
--- /dev/null
+++ b/setup/grap_custom_import_base/odoo/addons/grap_custom_import_base
@@ -0,0 +1 @@
+../../../../grap_custom_import_base
\ No newline at end of file
diff --git a/setup/grap_custom_import_base/setup.py b/setup/grap_custom_import_base/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/grap_custom_import_base/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/grap_custom_import_product/odoo/addons/grap_custom_import_product b/setup/grap_custom_import_product/odoo/addons/grap_custom_import_product
new file mode 120000
index 0000000..3a0e364
--- /dev/null
+++ b/setup/grap_custom_import_product/odoo/addons/grap_custom_import_product
@@ -0,0 +1 @@
+../../../../grap_custom_import_product
\ No newline at end of file
diff --git a/setup/grap_custom_import_product/setup.py b/setup/grap_custom_import_product/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/grap_custom_import_product/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/grap_custom_import_product_supplierinfo_qty_multiplier/odoo/addons/grap_custom_import_product_supplierinfo_qty_multiplier b/setup/grap_custom_import_product_supplierinfo_qty_multiplier/odoo/addons/grap_custom_import_product_supplierinfo_qty_multiplier
new file mode 120000
index 0000000..b633d52
--- /dev/null
+++ b/setup/grap_custom_import_product_supplierinfo_qty_multiplier/odoo/addons/grap_custom_import_product_supplierinfo_qty_multiplier
@@ -0,0 +1 @@
+../../../../grap_custom_import_product_supplierinfo_qty_multiplier
\ No newline at end of file
diff --git a/setup/grap_custom_import_product_supplierinfo_qty_multiplier/setup.py b/setup/grap_custom_import_product_supplierinfo_qty_multiplier/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/grap_custom_import_product_supplierinfo_qty_multiplier/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..e69de29