diff --git a/instant_import/.DS_Store b/instant_import/.DS_Store new file mode 100644 index 000000000..a20d6e7e8 Binary files /dev/null and b/instant_import/.DS_Store differ diff --git a/instant_import/README.md b/instant_import/README.md new file mode 100644 index 000000000..d510fb13d --- /dev/null +++ b/instant_import/README.md @@ -0,0 +1,75 @@ +# Instant Import for Odoo 17 + +[![Odoo](https://img.shields.io/badge/Odoo-%23A24689.svg?style=for-the-badge&logo=Odoo&logoColor=white)](https://www.odoo.com) +[![License](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](https://opensource.org/licenses/MIT) + + +## Overview + +The Instant Import module is an Odoo extension that streamlines the data import +process by providing a user-friendly interface built with OWL. It lets users +test and import Excel data into any model with validation, error feedback, and +smooth record creation. Designed for performance, the module ensures that large +volumes of data are imported quickly and efficiently. + +## Features + +- Features +- **Fast Data Import**: Quickly import large volumes of records from Excel files into any Odoo model. +- **Real-Time Validation**: Instantly checks for field mismatches or errors before importing. +- **OWL-Based Interface**: Modern and responsive UI built with Odoo Web Library (OWL) for smooth user experience. +- **Seamless Record Creation**: Automatically creates records without navigating away from the current view. +## Screenshots + +Here are some glimpses of Odoo Instant Import: + +### Sales module + +
+ + + step 1 + + +
+
+ + + step 2 + + +
+
+ + + step 3 + + +
+
+ + + step 4 + + +
+
+ + + step 5 + + +
+
+ + + step 6 + + +
+ +## Prerequisites + +Before you begin, ensure you have the following installed: + +- An active Odoo Community/Enterprise Edition instance (local or hosted) diff --git a/instant_import/__init__.py b/instant_import/__init__.py new file mode 100644 index 000000000..3dcffec2b --- /dev/null +++ b/instant_import/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# 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 LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################# +from . import wizard +from .hooks import setup_db_level_functions, delete_contact diff --git a/instant_import/__manifest__.py b/instant_import/__manifest__.py new file mode 100644 index 000000000..2579d47eb --- /dev/null +++ b/instant_import/__manifest__.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# 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 LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################# +{ + 'name': 'Instant Import', + 'version': '17.0.1.0.0', + 'depends': ['base', 'web', 'base_import'], + 'category': 'Tools', + 'summary': 'Module for fast bulk imports using PostgreSQL COPY', + 'author': "Cybrosys Techno Solutions", + 'company': 'Cybrosys Techno Solutions', + 'maintainer': 'Cybrosys Techno Solutions', + 'website': "https://www.cybrosys.com", + 'application': False, + 'data': [ + 'security/ir.model.access.csv', + ], + 'assets': { + 'web.assets_backend': [ + 'instant_import/static/src/js/import_component.js', + 'instant_import/static/src/js/import_component.xml', + 'instant_import/static/src/js/instant_import.js', + 'instant_import/static/src/js/templates.xml', + ], + }, + 'images': [ + 'static/description/banner.jpg', + ], + 'license': 'LGPL-3', + 'post_init_hook': 'setup_db_level_functions', + 'uninstall_hook': 'delete_contact', +} diff --git a/instant_import/doc/RELEASE_NOTES.md b/instant_import/doc/RELEASE_NOTES.md new file mode 100644 index 000000000..728001609 --- /dev/null +++ b/instant_import/doc/RELEASE_NOTES.md @@ -0,0 +1,7 @@ +## Module + +#### 17.07.2025 +#### Version 17.0.1.0.0 +##### ADD + +- Initial Commit for Custom Import \ No newline at end of file diff --git a/instant_import/hooks.py b/instant_import/hooks.py new file mode 100644 index 000000000..7dad2c9e3 --- /dev/null +++ b/instant_import/hooks.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# 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 LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################# +def setup_db_level_functions(env): + env.cr.execute( + """ + CREATE OR REPLACE FUNCTION process_m2m_mapping() + RETURNS TRIGGER AS $$ + DECLARE + col record; + value_array text[]; + single_value text; + id1 integer; + id2 integer; + dynamic_sql text; + mapping_config jsonb; + column_type text; + field_config jsonb; + BEGIN + -- Get the mapping configuration from TG_ARGV[0] + -- Expected format: + -- { + -- "m2m__field1": {"data_table": "table1", "mapping_table": "map1", "column1": "col1", "column2": "col2"}, + -- "m2m__field2": {"data_table": "table2", "mapping_table": "map2", "column1": "col3", "column2": "col4"} + -- } + mapping_config := TG_ARGV[0]::jsonb; + + -- Loop through all columns of the table + FOR col IN ( + SELECT column_name + FROM information_schema.columns + WHERE table_name = TG_TABLE_NAME::text + AND column_name LIKE 'm2m__%' + ) LOOP + -- Get configuration for this m2m field + field_config := mapping_config->col.column_name; + + IF field_config IS NOT NULL THEN + -- Only process if the column has a value + EXECUTE format('SELECT $1.%I', col.column_name) USING NEW INTO dynamic_sql; + IF dynamic_sql IS NOT NULL THEN + -- Get the ID from the currently triggered table + id1 := NEW.id; + + -- Get the data type of the name column + EXECUTE format( + 'SELECT data_type + FROM information_schema.columns + WHERE table_name = %L + AND column_name = ''name''', + field_config->>'data_table' + ) INTO column_type; + + -- Split the m2m values + value_array := string_to_array(dynamic_sql, ','); + + -- Process each value in the array + FOREACH single_value IN ARRAY value_array LOOP + -- Get the ID from the related table based on column type + IF column_type = 'jsonb' THEN + EXECUTE format( + 'SELECT id FROM %I WHERE (name->>''en_US'' = %L OR name->>''fr_FR'' = %L)', + field_config->>'data_table', + TRIM(single_value), + TRIM(single_value) + ) INTO id2; + + -- If not found, try searching without language code + IF id2 IS NULL THEN + EXECUTE format( + 'SELECT id FROM %I WHERE (name->''en_US'' ? %L OR name->''fr_FR'' ? %L)', + field_config->>'data_table', + TRIM(single_value), + TRIM(single_value) + ) INTO id2; + END IF; + ELSE + -- For text type + EXECUTE format( + 'SELECT id FROM %I WHERE name = %L', + field_config->>'data_table', + TRIM(single_value) + ) INTO id2; + END IF; + + -- Insert into mapping table if both IDs are found + IF id1 IS NOT NULL AND id2 IS NOT NULL THEN + EXECUTE format( + 'INSERT INTO %I (%I, %I) + VALUES (%L, %L) + ON CONFLICT (%I, %I) DO NOTHING', + field_config->>'mapping_table', + field_config->>'column1', + field_config->>'column2', + id1, id2, + field_config->>'column1', + field_config->>'column2' + ); + END IF; + END LOOP; + END IF; + END IF; + END LOOP; + + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + """ + ) + +def delete_contact(env): + env.cr.execute( + """ + DROP FUNCTION IF EXISTS process_m2m_mapping(); + """ + ) \ No newline at end of file diff --git a/instant_import/security/ir.model.access.csv b/instant_import/security/ir.model.access.csv new file mode 100644 index 000000000..751f69c1d --- /dev/null +++ b/instant_import/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_custom_import_wizard,access_custom.import.wizard,model_custom_import_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/instant_import/static/description/assets/cybro-icon.png b/instant_import/static/description/assets/cybro-icon.png new file mode 100644 index 000000000..06e73e11d Binary files /dev/null and b/instant_import/static/description/assets/cybro-icon.png differ diff --git a/instant_import/static/description/assets/cybro-odoo.png b/instant_import/static/description/assets/cybro-odoo.png new file mode 100644 index 000000000..ed02e07a4 Binary files /dev/null and b/instant_import/static/description/assets/cybro-odoo.png differ diff --git a/instant_import/static/description/assets/h2.png b/instant_import/static/description/assets/h2.png new file mode 100644 index 000000000..0bfc4707d Binary files /dev/null and b/instant_import/static/description/assets/h2.png differ diff --git a/instant_import/static/description/assets/icons/arrows-repeat.svg b/instant_import/static/description/assets/icons/arrows-repeat.svg new file mode 100644 index 000000000..1d7efabc5 --- /dev/null +++ b/instant_import/static/description/assets/icons/arrows-repeat.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/banner-1.png b/instant_import/static/description/assets/icons/banner-1.png new file mode 100644 index 000000000..c180db172 Binary files /dev/null and b/instant_import/static/description/assets/icons/banner-1.png differ diff --git a/instant_import/static/description/assets/icons/banner-2.svg b/instant_import/static/description/assets/icons/banner-2.svg new file mode 100644 index 000000000..e606d97d9 --- /dev/null +++ b/instant_import/static/description/assets/icons/banner-2.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/banner-bg.png b/instant_import/static/description/assets/icons/banner-bg.png new file mode 100644 index 000000000..a8238d3c0 Binary files /dev/null and b/instant_import/static/description/assets/icons/banner-bg.png differ diff --git a/instant_import/static/description/assets/icons/banner-bg.svg b/instant_import/static/description/assets/icons/banner-bg.svg new file mode 100644 index 000000000..b1378103e --- /dev/null +++ b/instant_import/static/description/assets/icons/banner-bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/banner-call.svg b/instant_import/static/description/assets/icons/banner-call.svg new file mode 100644 index 000000000..96c687e81 --- /dev/null +++ b/instant_import/static/description/assets/icons/banner-call.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/instant_import/static/description/assets/icons/banner-mail.svg b/instant_import/static/description/assets/icons/banner-mail.svg new file mode 100644 index 000000000..cbf0d158d --- /dev/null +++ b/instant_import/static/description/assets/icons/banner-mail.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/instant_import/static/description/assets/icons/banner-pattern.svg b/instant_import/static/description/assets/icons/banner-pattern.svg new file mode 100644 index 000000000..9c1c7e101 --- /dev/null +++ b/instant_import/static/description/assets/icons/banner-pattern.svg @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/banner-promo.svg b/instant_import/static/description/assets/icons/banner-promo.svg new file mode 100644 index 000000000..d52791b11 --- /dev/null +++ b/instant_import/static/description/assets/icons/banner-promo.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/brand-pair.svg b/instant_import/static/description/assets/icons/brand-pair.svg new file mode 100644 index 000000000..d8db7fc1e --- /dev/null +++ b/instant_import/static/description/assets/icons/brand-pair.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/check.png b/instant_import/static/description/assets/icons/check.png new file mode 100644 index 000000000..c8e85f51d Binary files /dev/null and b/instant_import/static/description/assets/icons/check.png differ diff --git a/instant_import/static/description/assets/icons/chevron.png b/instant_import/static/description/assets/icons/chevron.png new file mode 100644 index 000000000..2089293d6 Binary files /dev/null and b/instant_import/static/description/assets/icons/chevron.png differ diff --git a/instant_import/static/description/assets/icons/close-icon.svg b/instant_import/static/description/assets/icons/close-icon.svg new file mode 100644 index 000000000..df8cce37a --- /dev/null +++ b/instant_import/static/description/assets/icons/close-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/instant_import/static/description/assets/icons/cogs.png b/instant_import/static/description/assets/icons/cogs.png new file mode 100644 index 000000000..95d0bad62 Binary files /dev/null and b/instant_import/static/description/assets/icons/cogs.png differ diff --git a/instant_import/static/description/assets/icons/collabarate-icon.svg b/instant_import/static/description/assets/icons/collabarate-icon.svg new file mode 100644 index 000000000..dd4e10518 --- /dev/null +++ b/instant_import/static/description/assets/icons/collabarate-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/instant_import/static/description/assets/icons/consultation.png b/instant_import/static/description/assets/icons/consultation.png new file mode 100644 index 000000000..8319d4baa Binary files /dev/null and b/instant_import/static/description/assets/icons/consultation.png differ diff --git a/instant_import/static/description/assets/icons/cybro-logo.png b/instant_import/static/description/assets/icons/cybro-logo.png new file mode 100644 index 000000000..ff4b78220 Binary files /dev/null and b/instant_import/static/description/assets/icons/cybro-logo.png differ diff --git a/instant_import/static/description/assets/icons/down.svg b/instant_import/static/description/assets/icons/down.svg new file mode 100644 index 000000000..f21c36271 --- /dev/null +++ b/instant_import/static/description/assets/icons/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/instant_import/static/description/assets/icons/ecom-black.png b/instant_import/static/description/assets/icons/ecom-black.png new file mode 100644 index 000000000..a9385ff13 Binary files /dev/null and b/instant_import/static/description/assets/icons/ecom-black.png differ diff --git a/instant_import/static/description/assets/icons/education-black.png b/instant_import/static/description/assets/icons/education-black.png new file mode 100644 index 000000000..3eb09b27b Binary files /dev/null and b/instant_import/static/description/assets/icons/education-black.png differ diff --git a/instant_import/static/description/assets/icons/faq.png b/instant_import/static/description/assets/icons/faq.png new file mode 100644 index 000000000..4250b5b81 Binary files /dev/null and b/instant_import/static/description/assets/icons/faq.png differ diff --git a/instant_import/static/description/assets/icons/feature-icon.svg b/instant_import/static/description/assets/icons/feature-icon.svg new file mode 100644 index 000000000..fa0ea6850 --- /dev/null +++ b/instant_import/static/description/assets/icons/feature-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/feature.png b/instant_import/static/description/assets/icons/feature.png new file mode 100644 index 000000000..ac7a785c0 Binary files /dev/null and b/instant_import/static/description/assets/icons/feature.png differ diff --git a/instant_import/static/description/assets/icons/gear.svg b/instant_import/static/description/assets/icons/gear.svg new file mode 100644 index 000000000..0cc66b6ea --- /dev/null +++ b/instant_import/static/description/assets/icons/gear.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/hero.gif b/instant_import/static/description/assets/icons/hero.gif new file mode 100644 index 000000000..d28160470 Binary files /dev/null and b/instant_import/static/description/assets/icons/hero.gif differ diff --git a/instant_import/static/description/assets/icons/hire-odoo.svg b/instant_import/static/description/assets/icons/hire-odoo.svg new file mode 100644 index 000000000..e1ac089b0 --- /dev/null +++ b/instant_import/static/description/assets/icons/hire-odoo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/hotel-black.png b/instant_import/static/description/assets/icons/hotel-black.png new file mode 100644 index 000000000..130f613be Binary files /dev/null and b/instant_import/static/description/assets/icons/hotel-black.png differ diff --git a/instant_import/static/description/assets/icons/license.png b/instant_import/static/description/assets/icons/license.png new file mode 100644 index 000000000..a5869797e Binary files /dev/null and b/instant_import/static/description/assets/icons/license.png differ diff --git a/instant_import/static/description/assets/icons/life-ring-icon.svg b/instant_import/static/description/assets/icons/life-ring-icon.svg new file mode 100644 index 000000000..3ae6e1d89 --- /dev/null +++ b/instant_import/static/description/assets/icons/life-ring-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/lifebuoy.png b/instant_import/static/description/assets/icons/lifebuoy.png new file mode 100644 index 000000000..658d56ccc Binary files /dev/null and b/instant_import/static/description/assets/icons/lifebuoy.png differ diff --git a/instant_import/static/description/assets/icons/mail.svg b/instant_import/static/description/assets/icons/mail.svg new file mode 100644 index 000000000..1eedde695 --- /dev/null +++ b/instant_import/static/description/assets/icons/mail.svg @@ -0,0 +1,3 @@ + + + diff --git a/instant_import/static/description/assets/icons/manufacturing-black.png b/instant_import/static/description/assets/icons/manufacturing-black.png new file mode 100644 index 000000000..697eb0e9f Binary files /dev/null and b/instant_import/static/description/assets/icons/manufacturing-black.png differ diff --git a/instant_import/static/description/assets/icons/notes.png b/instant_import/static/description/assets/icons/notes.png new file mode 100644 index 000000000..ee5e95404 Binary files /dev/null and b/instant_import/static/description/assets/icons/notes.png differ diff --git a/instant_import/static/description/assets/icons/notification icon.svg b/instant_import/static/description/assets/icons/notification icon.svg new file mode 100644 index 000000000..053189973 --- /dev/null +++ b/instant_import/static/description/assets/icons/notification icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/odoo-consultancy.svg b/instant_import/static/description/assets/icons/odoo-consultancy.svg new file mode 100644 index 000000000..e05f65bde --- /dev/null +++ b/instant_import/static/description/assets/icons/odoo-consultancy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/instant_import/static/description/assets/icons/odoo-licencing.svg b/instant_import/static/description/assets/icons/odoo-licencing.svg new file mode 100644 index 000000000..2606c88b0 --- /dev/null +++ b/instant_import/static/description/assets/icons/odoo-licencing.svg @@ -0,0 +1,3 @@ + + + diff --git a/instant_import/static/description/assets/icons/odoo-logo.png b/instant_import/static/description/assets/icons/odoo-logo.png new file mode 100644 index 000000000..0e4d0eb5a Binary files /dev/null and b/instant_import/static/description/assets/icons/odoo-logo.png differ diff --git a/instant_import/static/description/assets/icons/patter.svg b/instant_import/static/description/assets/icons/patter.svg new file mode 100644 index 000000000..25c9c0a8f --- /dev/null +++ b/instant_import/static/description/assets/icons/patter.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/pattern1.png b/instant_import/static/description/assets/icons/pattern1.png new file mode 100644 index 000000000..09ab0fb2d Binary files /dev/null and b/instant_import/static/description/assets/icons/pattern1.png differ diff --git a/instant_import/static/description/assets/icons/pos-black.png b/instant_import/static/description/assets/icons/pos-black.png new file mode 100644 index 000000000..97c0f90c1 Binary files /dev/null and b/instant_import/static/description/assets/icons/pos-black.png differ diff --git a/instant_import/static/description/assets/icons/puzzle-piece-icon.svg b/instant_import/static/description/assets/icons/puzzle-piece-icon.svg new file mode 100644 index 000000000..3e9ad9373 --- /dev/null +++ b/instant_import/static/description/assets/icons/puzzle-piece-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/puzzle.png b/instant_import/static/description/assets/icons/puzzle.png new file mode 100644 index 000000000..65cf854e7 Binary files /dev/null and b/instant_import/static/description/assets/icons/puzzle.png differ diff --git a/instant_import/static/description/assets/icons/replace-icon.svg b/instant_import/static/description/assets/icons/replace-icon.svg new file mode 100644 index 000000000..d0e3a7af1 --- /dev/null +++ b/instant_import/static/description/assets/icons/replace-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/restaurant-black.png b/instant_import/static/description/assets/icons/restaurant-black.png new file mode 100644 index 000000000..4a35eb939 Binary files /dev/null and b/instant_import/static/description/assets/icons/restaurant-black.png differ diff --git a/instant_import/static/description/assets/icons/screenshot-main.png b/instant_import/static/description/assets/icons/screenshot-main.png new file mode 100644 index 000000000..575f8e676 Binary files /dev/null and b/instant_import/static/description/assets/icons/screenshot-main.png differ diff --git a/instant_import/static/description/assets/icons/screenshot.png b/instant_import/static/description/assets/icons/screenshot.png new file mode 100644 index 000000000..cef272529 Binary files /dev/null and b/instant_import/static/description/assets/icons/screenshot.png differ diff --git a/instant_import/static/description/assets/icons/service-black.png b/instant_import/static/description/assets/icons/service-black.png new file mode 100644 index 000000000..301ab51cb Binary files /dev/null and b/instant_import/static/description/assets/icons/service-black.png differ diff --git a/instant_import/static/description/assets/icons/skype-fill.svg b/instant_import/static/description/assets/icons/skype-fill.svg new file mode 100644 index 000000000..c17423639 --- /dev/null +++ b/instant_import/static/description/assets/icons/skype-fill.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/skype.png b/instant_import/static/description/assets/icons/skype.png new file mode 100644 index 000000000..51b409fb3 Binary files /dev/null and b/instant_import/static/description/assets/icons/skype.png differ diff --git a/instant_import/static/description/assets/icons/skype.svg b/instant_import/static/description/assets/icons/skype.svg new file mode 100644 index 000000000..df3dad39b --- /dev/null +++ b/instant_import/static/description/assets/icons/skype.svg @@ -0,0 +1,3 @@ + + + diff --git a/instant_import/static/description/assets/icons/star-1.svg b/instant_import/static/description/assets/icons/star-1.svg new file mode 100644 index 000000000..7e55ab162 --- /dev/null +++ b/instant_import/static/description/assets/icons/star-1.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/star-2.svg b/instant_import/static/description/assets/icons/star-2.svg new file mode 100644 index 000000000..5ae9f507a --- /dev/null +++ b/instant_import/static/description/assets/icons/star-2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/support.png b/instant_import/static/description/assets/icons/support.png new file mode 100644 index 000000000..4f18b8b82 Binary files /dev/null and b/instant_import/static/description/assets/icons/support.png differ diff --git a/instant_import/static/description/assets/icons/test-1 - Copy.png b/instant_import/static/description/assets/icons/test-1 - Copy.png new file mode 100644 index 000000000..f6a902663 Binary files /dev/null and b/instant_import/static/description/assets/icons/test-1 - Copy.png differ diff --git a/instant_import/static/description/assets/icons/test-1.png b/instant_import/static/description/assets/icons/test-1.png new file mode 100644 index 000000000..0908add2b Binary files /dev/null and b/instant_import/static/description/assets/icons/test-1.png differ diff --git a/instant_import/static/description/assets/icons/test-2.png b/instant_import/static/description/assets/icons/test-2.png new file mode 100644 index 000000000..4671fe91e Binary files /dev/null and b/instant_import/static/description/assets/icons/test-2.png differ diff --git a/instant_import/static/description/assets/icons/trading-black.png b/instant_import/static/description/assets/icons/trading-black.png new file mode 100644 index 000000000..9398ba2f1 Binary files /dev/null and b/instant_import/static/description/assets/icons/trading-black.png differ diff --git a/instant_import/static/description/assets/icons/training.png b/instant_import/static/description/assets/icons/training.png new file mode 100644 index 000000000..884ca024d Binary files /dev/null and b/instant_import/static/description/assets/icons/training.png differ diff --git a/instant_import/static/description/assets/icons/translate.svg b/instant_import/static/description/assets/icons/translate.svg new file mode 100644 index 000000000..af9c8a1aa --- /dev/null +++ b/instant_import/static/description/assets/icons/translate.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/update.png b/instant_import/static/description/assets/icons/update.png new file mode 100644 index 000000000..ecbc5a01a Binary files /dev/null and b/instant_import/static/description/assets/icons/update.png differ diff --git a/instant_import/static/description/assets/icons/user.png b/instant_import/static/description/assets/icons/user.png new file mode 100644 index 000000000..6ffb23d9f Binary files /dev/null and b/instant_import/static/description/assets/icons/user.png differ diff --git a/instant_import/static/description/assets/icons/video.png b/instant_import/static/description/assets/icons/video.png new file mode 100644 index 000000000..576705b17 Binary files /dev/null and b/instant_import/static/description/assets/icons/video.png differ diff --git a/instant_import/static/description/assets/icons/whatsapp.png b/instant_import/static/description/assets/icons/whatsapp.png new file mode 100644 index 000000000..d513a5356 Binary files /dev/null and b/instant_import/static/description/assets/icons/whatsapp.png differ diff --git a/instant_import/static/description/assets/icons/wrench-icon.svg b/instant_import/static/description/assets/icons/wrench-icon.svg new file mode 100644 index 000000000..174b5a465 --- /dev/null +++ b/instant_import/static/description/assets/icons/wrench-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/instant_import/static/description/assets/icons/wrench.png b/instant_import/static/description/assets/icons/wrench.png new file mode 100644 index 000000000..6c04dea0f Binary files /dev/null and b/instant_import/static/description/assets/icons/wrench.png differ diff --git a/instant_import/static/description/assets/modules/b1.png b/instant_import/static/description/assets/modules/b1.png new file mode 100644 index 000000000..6e617f3d3 Binary files /dev/null and b/instant_import/static/description/assets/modules/b1.png differ diff --git a/instant_import/static/description/assets/modules/b2.png b/instant_import/static/description/assets/modules/b2.png new file mode 100644 index 000000000..696582fa8 Binary files /dev/null and b/instant_import/static/description/assets/modules/b2.png differ diff --git a/instant_import/static/description/assets/modules/b3.png b/instant_import/static/description/assets/modules/b3.png new file mode 100644 index 000000000..cf81c09f8 Binary files /dev/null and b/instant_import/static/description/assets/modules/b3.png differ diff --git a/instant_import/static/description/assets/modules/b4.png b/instant_import/static/description/assets/modules/b4.png new file mode 100644 index 000000000..206e14c47 Binary files /dev/null and b/instant_import/static/description/assets/modules/b4.png differ diff --git a/instant_import/static/description/assets/modules/b5.png b/instant_import/static/description/assets/modules/b5.png new file mode 100644 index 000000000..1b0ce4674 Binary files /dev/null and b/instant_import/static/description/assets/modules/b5.png differ diff --git a/instant_import/static/description/assets/modules/b6.png b/instant_import/static/description/assets/modules/b6.png new file mode 100644 index 000000000..0249a98f1 Binary files /dev/null and b/instant_import/static/description/assets/modules/b6.png differ diff --git a/instant_import/static/description/assets/screenshots/Gif.gif b/instant_import/static/description/assets/screenshots/Gif.gif new file mode 100644 index 000000000..7699e2a46 Binary files /dev/null and b/instant_import/static/description/assets/screenshots/Gif.gif differ diff --git a/instant_import/static/description/assets/screenshots/instant_import_01.png b/instant_import/static/description/assets/screenshots/instant_import_01.png new file mode 100644 index 000000000..72e201b98 Binary files /dev/null and b/instant_import/static/description/assets/screenshots/instant_import_01.png differ diff --git a/instant_import/static/description/assets/screenshots/instant_import_02.png b/instant_import/static/description/assets/screenshots/instant_import_02.png new file mode 100644 index 000000000..390d6f0b3 Binary files /dev/null and b/instant_import/static/description/assets/screenshots/instant_import_02.png differ diff --git a/instant_import/static/description/assets/screenshots/instant_import_03.png b/instant_import/static/description/assets/screenshots/instant_import_03.png new file mode 100644 index 000000000..d88253251 Binary files /dev/null and b/instant_import/static/description/assets/screenshots/instant_import_03.png differ diff --git a/instant_import/static/description/assets/screenshots/instant_import_04.png b/instant_import/static/description/assets/screenshots/instant_import_04.png new file mode 100644 index 000000000..ff9d5b683 Binary files /dev/null and b/instant_import/static/description/assets/screenshots/instant_import_04.png differ diff --git a/instant_import/static/description/assets/screenshots/instant_import_05.png b/instant_import/static/description/assets/screenshots/instant_import_05.png new file mode 100644 index 000000000..cd747d841 Binary files /dev/null and b/instant_import/static/description/assets/screenshots/instant_import_05.png differ diff --git a/instant_import/static/description/assets/screenshots/instant_import_06.png b/instant_import/static/description/assets/screenshots/instant_import_06.png new file mode 100644 index 000000000..f270a82e1 Binary files /dev/null and b/instant_import/static/description/assets/screenshots/instant_import_06.png differ diff --git a/instant_import/static/description/assets/y18.jpg b/instant_import/static/description/assets/y18.jpg new file mode 100644 index 000000000..eea1714f2 Binary files /dev/null and b/instant_import/static/description/assets/y18.jpg differ diff --git a/instant_import/static/description/banner.jpg b/instant_import/static/description/banner.jpg new file mode 100644 index 000000000..c7ba42d00 Binary files /dev/null and b/instant_import/static/description/banner.jpg differ diff --git a/instant_import/static/description/icon.png b/instant_import/static/description/icon.png new file mode 100644 index 000000000..5d4a23272 Binary files /dev/null and b/instant_import/static/description/icon.png differ diff --git a/instant_import/static/description/index.html b/instant_import/static/description/index.html new file mode 100644 index 000000000..532afb9c7 --- /dev/null +++ b/instant_import/static/description/index.html @@ -0,0 +1,927 @@ + + + + + + Instant Import + + + + + + + + + + +
+
+ + + +
+
+ Community +
+
+ Enterprise +
+
+ Odoo.sh +
+
+
+ +
+
+
+
+

+ Odoo Instant Import is a fast and user-friendly + module that allows you to import Excel data easily. +

+

Instant Import +

+
+
+ +
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+

Key + Highlights

+
+
+
+
+ +
+
+ Fast Data Import. +
+

+

+
+
+
+
+
+ +
+
+ Real-Time Validation. +
+

+

+
+
+
+
+
+ +
+
+ OWL-Based Interface. + Seamless Record Creation. +
+

+

+
+
+
+
+
+ +
+
+ Seamless Record Creation. +
+

+

+
+
+
+
+ +
+
+
+ Instant Import +

+ Are you ready to make your business more + organized? +
Improve now! +

+ +
+
+ +
+
+
+ + + + +
+
+ +
+
+
+
+ acc_bg +
+ +
+
+
+
+

+ Click on the action button in the view then click on the Instant import menu +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Upload the Data to be imported + +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Test the data by clicking the Test Button + +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Import the data by clicking on the Import Button + +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Data is being imported + +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Data successfully imported + +

+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+

+ Import bulk data instantly. +

+
+
+
+
+
+
+
+ +
+

+ Ensures clean and consistent import for Many2one and Many2many relational fields. +

+
+ +
+
+
+
+
+
+ +
+

+ Works effortlessly with Odoo 17 models. +

+
+ +
+
+
+
+
+ +
+
+

+ Latest Release 17.0.1.0.0 +

+ + 17th September, 2025 + +
+
+
+
+
+ Add +
+
+
+
    +
  • + Initial Commit +
  • + +
+
+
+
+
+
+
+
+
+
+ + + +
+

+ Related Products +

+ +
+ + +
+

+ Our Services

+
+ +
+
+ .... +
+
+ +
+ + +
+
+ + + + + + diff --git a/instant_import/static/src/js/import_component.js b/instant_import/static/src/js/import_component.js new file mode 100644 index 000000000..78c6575c5 --- /dev/null +++ b/instant_import/static/src/js/import_component.js @@ -0,0 +1,138 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { ImportAction } from "@base_import/import_action/import_action"; +import { useService } from "@web/core/utils/hooks"; +import { BlockUI } from "@web/core/ui/block_ui"; + +export class InstantImport extends ImportAction { + static template = "instant_import.importaction"; + static components = { ...ImportAction.components, BlockUI }; + + setup() { + super.setup(); + this.orm = useService('orm'); + this.action = useService('action'); + this.notification = useService("notification"); + this.blockUI = useService("ui"); + } + + async handleImport() { + try { + this.blockUI.block({ + message: "Your records are being imported...", + }); + + console.log("Importing model:", this.props.action.params.model); + const result = await this.orm.call( + "custom.import.wizard", + "copy_import", + [this.model.id, this.props.action.params.model, this.model.columns], + {} + ); + + if (result && result.record_count) { + this.notification.add(`${result.record_count} Records successfully imported`, { + type: "success", + }); + + // Redirect to the relevant view after successful import + this.action.doAction({ + type: "ir.actions.act_window", + res_model: this.props.action.params.model, + name: result.name, + views: [[false, 'list'], [false, 'kanban'], [false, 'form']], + target: 'main', + }); + } + + } catch (error) { + console.error("Import error:", error); + + // Extract the actual error message from the error object + let errorMessage = "Import failed. Please check your data."; + + if (error && error.data && error.data.message) { + // This handles UserError messages from Python + errorMessage = error.data.message; + } else if (error && error.message) { + // This handles other types of error messages + errorMessage = error.message; + } else if (typeof error === 'string') { + errorMessage = error; + } + + this.notification.add(errorMessage, { + type: "danger", + sticky: true, // Keep the error message visible longer + }); + } finally { + this.blockUI.unblock(); + } + } + + async handleTest() { + try { + this.blockUI.block({ + message: "Your records are being tested...", + }); + + const validationResult = await this.orm.call( + "custom.import.wizard", + "validate_columns", + [this.model.id, this.props.action.params.model, this.model.columns], + {} + ); + + console.log('Validation result:', validationResult); + + if (validationResult && validationResult.is_valid) { + this.notification.add(`Everything seems valid`, { + type: "success", + }); + } else { + // Handle different types of validation errors + let errorMessage = "Validation failed"; + + if (validationResult.error_type === 'missing_required_fields') { + errorMessage = validationResult.error_message || + `Required fields missing: ${validationResult.missing_required_fields.join(", ")}. Please add these columns to your Excel file.`; + } else if (validationResult.error_type === 'invalid_columns') { + errorMessage = `The following columns do not match Odoo fields: ${validationResult.invalid_columns.join(", ")}`; + } else if (validationResult.error_message) { + errorMessage = validationResult.error_message; + } else if (validationResult.invalid_columns) { + errorMessage = `The following columns do not match Odoo fields: ${validationResult.invalid_columns.join(", ")}`; + } + + this.notification.add(errorMessage, { + type: "danger", + sticky: true, // Keep the error message visible longer + }); + } + + } catch (error) { + console.error("Error during column validation:", error); + + // Extract the actual error message from the error object + let errorMessage = "Validation failed. Please check your data."; + + if (error && error.data && error.data.message) { + errorMessage = error.data.message; + } else if (error && error.message) { + errorMessage = error.message; + } else if (typeof error === 'string') { + errorMessage = error; + } + + this.notification.add(errorMessage, { + type: "danger", + sticky: true, + }); + } finally { + this.blockUI.unblock(); + } + } +} + +registry.category("actions").add("instant_import", InstantImport); \ No newline at end of file diff --git a/instant_import/static/src/js/import_component.xml b/instant_import/static/src/js/import_component.xml new file mode 100644 index 000000000..4015a0fdb --- /dev/null +++ b/instant_import/static/src/js/import_component.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + display:None; + + + \ No newline at end of file diff --git a/instant_import/static/src/js/instant_import.js b/instant_import/static/src/js/instant_import.js new file mode 100644 index 000000000..1c455f095 --- /dev/null +++ b/instant_import/static/src/js/instant_import.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ + +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { Component } from "@odoo/owl"; +import { patch } from "@web/core/utils/patch"; +import { ImportRecords } from "@base_import/import_records/import_records"; + + patch(ImportRecords.prototype, { + setup() { + super.setup(); + this.orm = useService("orm"); + this.action = useService("action"); + }, + async InstantImport() { + const { context, resModel } = this.env.searchModel; + this.action.doAction({ + type: "ir.actions.client", + tag: "instant_import", + params: { model: resModel, context }, + }); + } +}); diff --git a/instant_import/static/src/js/templates.xml b/instant_import/static/src/js/templates.xml new file mode 100644 index 000000000..5756cd793 --- /dev/null +++ b/instant_import/static/src/js/templates.xml @@ -0,0 +1,10 @@ + + + + + + Instant Import + + + + diff --git a/instant_import/wizard/__init__.py b/instant_import/wizard/__init__.py new file mode 100644 index 000000000..132a0853d --- /dev/null +++ b/instant_import/wizard/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# 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 LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################# +from . import import_wizard + diff --git a/instant_import/wizard/import_wizard.py b/instant_import/wizard/import_wizard.py new file mode 100644 index 000000000..71c4f6001 --- /dev/null +++ b/instant_import/wizard/import_wizard.py @@ -0,0 +1,780 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# 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 LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################# +import json +import csv +from datetime import datetime +from io import BytesIO + +import pandas as pd +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from odoo.addons.base_import.models.base_import import FIELDS_RECURSION_LIMIT + + +class ImportWizard(models.TransientModel): + _name = 'custom.import.wizard' + _description = 'Custom Import Wizard' + + model_id = fields.Many2one( + 'ir.model', 'Model', + required=True, + domain=[('transient', '=', False)] + ) + + def remove_m2m_temp_columns(self, table, m2m_columns): + for column in m2m_columns: + self.env.cr.execute(f"ALTER TABLE {table} DROP COLUMN IF EXISTS {column};") + self.env.cr.execute(f"DROP TRIGGER IF EXISTS trg_process_m2m_mapping ON {table};") + + def get_m2m_details(self, model_name, field_name): + model = self.env[model_name] + field = model._fields[field_name] + return { + 'relation_table': field.relation, + 'column1': field.column1, + 'column2': field.column2 + } + + @api.model + def validate_columns(self, res_id, model, columns): + try: + uploaded_columns = [item['fieldInfo']['id'] for item in columns if 'fieldInfo' in item] + if len(uploaded_columns) < len(columns): + invalid_columns = [col.get('name', 'Unknown') for col in columns if 'fieldInfo' not in col] + return { + 'is_valid': False, + 'invalid_columns': invalid_columns, + 'error_type': 'invalid_columns' + } + + missing_required = self._check_missing_required_fields_for_validation(model, columns) + if missing_required: + return { + 'is_valid': False, + 'missing_required_fields': missing_required, + 'error_type': 'missing_required_fields', + 'error_message': _("Required fields missing: %s. Please add these columns to your Excel file.") % ', '.join(missing_required) + } + + return {'is_valid': True} + except Exception as e: + return { + 'is_valid': False, + 'error_type': 'validation_error', + 'error_message': _("Validation failed: %s") % str(e) + } + + def _check_missing_required_fields_for_validation(self, model_name, columns): + try: + imported_fields = set() + for item in columns: + if 'fieldInfo' in item: + field_name = item['fieldInfo']['id'].split('/')[0] if '/' in item['fieldInfo']['id'] else item['fieldInfo']['id'] + imported_fields.add(field_name) + + Model = self.env[model_name] + model_fields = Model.fields_get() + required_fields = [ + fname for fname, field in model_fields.items() + if field.get('required') and field.get('store') and not field.get('deprecated', False) + ] + + defaults = Model.default_get(list(model_fields.keys())) + odoo_defaults = {k: v for k, v in defaults.items() if v is not None} + auto_generated_fields = self._get_auto_generated_fields(model_name, required_fields) + + missing = [] + for field in set(required_fields) - imported_fields: + if field not in odoo_defaults and field not in auto_generated_fields: + missing.append(field) + + return missing + except Exception: + return [] + + def _get_auto_generated_fields(self, model_name, required_fields): + auto_generated_fields = set() + try: + model_obj = self.env[model_name] + try: + all_field_names = list(model_obj.fields_get().keys()) + defaults = model_obj.default_get(all_field_names) + fields_with_defaults = {k for k, v in defaults.items() if v is not None} + except Exception: + fields_with_defaults = set() + + for field_name in required_fields: + if field_name in model_obj._fields: + field_obj = model_obj._fields[field_name] + if ( + getattr(field_obj, 'compute', False) + or getattr(field_obj, 'related', False) + or getattr(field_obj, 'default', False) + or field_name in fields_with_defaults + or field_name in ['create_date', 'write_date', 'create_uid', 'write_uid'] + ): + auto_generated_fields.add(field_name) + if field_obj.type == 'many2one': + # Generic company/currency lookup if attribute exists + attr_name = field_name.replace('_id', '') + if hasattr(self.env, 'company') and hasattr(self.env.company, attr_name): + auto_generated_fields.add(field_name) + except Exception: + pass + return auto_generated_fields + + def _handle_special_required_fields(self, data, model_name, model_fields, missing_required): + try: + Model = self.env[model_name] + defaults = Model.default_get(list(model_fields.keys())) + + for field in missing_required: + if field in model_fields and model_fields[field]['type'] == 'many2one' and field not in data.columns: + rel_model = model_fields[field]['relation'] + rec = self.env[rel_model].search([], limit=1) + if rec: + data[field] = [rec.id] * len(data) + elif field not in data.columns and field in defaults: + data[field] = [defaults[field]] * len(data) + elif field == 'user_id' and hasattr(self.env, 'uid') and field not in data.columns: + data[field] = [self.env.uid] * len(data) + + for field in missing_required: + if (field in model_fields and + model_fields[field]['type'] in ['date', 'datetime'] + and (field not in data.columns or data[field].isna().all()) + ): + now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + data[field] = [now_str] * len(data) + except Exception: + pass + + def _build_reference_cache(self, model, values, reference_cache): + cache_key = model + if cache_key not in reference_cache: + reference_cache[cache_key] = {} + + Model = self.env[model] + unique_values = list(set(str(v).strip() for v in values if pd.notna(v) and v not in ['', 0, '0'])) + if not unique_values: + return + + id_values = [] + for val in unique_values: + try: + id_val = int(float(val)) + id_values.append(id_val) + except Exception: + pass + + if id_values: + records = Model.browse(id_values).exists() + for record in records: + reference_cache[cache_key][str(record.id)] = record.id + + search_fields = ['name', 'complete_name', 'code'] + for field_name in search_fields: + if field_name in Model._fields and Model._fields[field_name].store: + try: + records = Model.search([(field_name, 'in', unique_values)]) + for record in records: + field_value = getattr(record, field_name, None) + if field_value: + reference_cache[cache_key][str(field_value)] = record.id + except Exception: + continue + + def _resolve_reference(self, model, value, reference_cache): + if pd.isna(value) or value in ['', 0, '0']: + return None + + cache_key = model + str_val = str(value).strip() + + if cache_key in reference_cache: + cached_id = reference_cache[cache_key].get(str_val) + if cached_id is not None: + return cached_id + + Model = self.env[model] + try: + record_id = int(float(str_val)) + record = Model.browse(record_id).exists() + if record: + return record.id + except Exception: + pass + + try: + return self.env.ref(str_val).id + except Exception: + pass + + if model == 'res.users': + user = Model.search([ + '|', '|', + ('name', '=ilike', str_val), + ('login', '=ilike', str_val), + ('email', '=ilike', str_val) + ], limit=1) + if user: + if cache_key not in reference_cache: + reference_cache[cache_key] = {} + reference_cache[cache_key][str_val] = user.id + return user.id + + search_fields = ['name', 'complete_name', 'code'] + for field_name in search_fields: + if field_name in Model._fields and Model._fields[field_name].store: + try: + record = Model.search([(field_name, '=ilike', str_val)], limit=1) + if record: + if cache_key not in reference_cache: + reference_cache[cache_key] = {} + reference_cache[cache_key][str_val] = record.id + return record.id + except Exception: + continue + + return None + + def get_required_fields(self, model_name): + Model = self.env[model_name] + model_fields = Model.fields_get() + required_fields = [] + for field_name, field in model_fields.items(): + if field.get('required') and field.get('store') and not field.get('deprecated', False): + required_fields.append({'name': field_name, 'type': field['type']}) + return required_fields + + def _get_sequence_for_model(self, model_name): + seq = self.env['ir.sequence'].search([('code', '=', model_name)], limit=1) + return seq or False + + def _generate_bulk_sequences(self, sequence, count): + if not sequence or count <= 0: + return [] + if hasattr(sequence, '_next_do'): + return [sequence._next_do() for _ in range(count)] + else: + return [sequence._next() for _ in range(count)] + + def _get_model_defaults(self, model_name): + try: + Model = self.env[model_name] + field_names = list(Model.fields_get().keys()) + defaults = Model.default_get(field_names) + return {k: v for k, v in defaults.items() if v is not None} + except Exception: + return {} + + def _prepare_generic_defaults(self, model_name): + return self._get_model_defaults(model_name) + + def _check_missing_required_fields(self, model_name, imported_fields, defaults): + Model = self.env[model_name] + model_fields = Model.fields_get() + required_fields = [] + for field_name, field_info in model_fields.items(): + if field_info.get('required') and field_info.get('store') and not field_info.get('deprecated', False): + required_fields.append(field_name) + missing_required = set(required_fields) - imported_fields + auto_generated_fields = self._get_auto_generated_fields(model_name, list(missing_required)) + missing_without_fallback = [] + for field in missing_required: + if field not in defaults and field not in auto_generated_fields: + missing_without_fallback.append(field) + return missing_without_fallback + + def _get_next_sequence_values(self, table_name, count): + try: + self.env.cr.execute(f"SELECT COALESCE(MAX(id), 0) FROM {table_name}") + max_id = self.env.cr.fetchone()[0] + ids_to_use = list(range(max_id + 1, max_id + count + 1)) + sequence_name = f"{table_name}_id_seq" + new_seq_val = max_id + count + 100 + self.env.cr.execute(f"SELECT setval('{sequence_name}', %s, false)", (new_seq_val,)) + return ids_to_use + except Exception as e: + try: + self.env.cr.execute(f"SELECT COALESCE(MAX(id), 0) FROM {table_name}") + max_id = self.env.cr.fetchone()[0] + return list(range(max_id + 1, max_id + count + 1)) + except Exception: + raise UserError(f"Unable to generate unique IDs: {str(e)}") + + def _sync_sequence_after_import(self, table_name): + try: + self.env.cr.execute(f"SELECT COALESCE(MAX(id), 0) FROM {table_name}") + max_id = self.env.cr.fetchone()[0] + sequence_name = f"{table_name}_id_seq" + new_seq_val = max_id + 1000 + self.env.cr.execute(f"SELECT setval('{sequence_name}', %s)", (new_seq_val,)) + except Exception: + try: + self.env.cr.execute(f"SELECT COALESCE(MAX(id), 0) FROM {table_name}") + max_id = self.env.cr.fetchone()[0] + sequence_name = f"{table_name}_id_seq" + self.env.cr.execute(f"SELECT setval('{sequence_name}', %s)", (max_id + 1,)) + except Exception: + pass + + def _prepare_audit_fields(self): + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + return { + 'create_uid': self.env.uid, + 'write_uid': self.env.uid, + 'create_date': current_time, + 'write_date': current_time + } + + @api.model + def copy_import(self, res_id, model, columns): + try: + reference_cache = {} + validation_result = self.validate_columns(res_id, model, columns) + if not validation_result.get('is_valid', False): + error_message = validation_result.get('error_message', 'Validation failed') + raise UserError(error_message) + + required_fields = [f['name'] for f in self.get_required_fields(model)] + model_fields = self.env[model].fields_get() + + column_mapping = {} + imported_fields = set() + for item in columns: + if 'fieldInfo' in item: + field_name = item['fieldInfo']['id'].split('/')[0] if '/' in item['fieldInfo']['id'] else item['fieldInfo']['id'] + column_mapping[item.get('name', field_name)] = field_name + imported_fields.add(field_name) + + defaults = self._prepare_generic_defaults(model) + missing_without_fallback = self._check_missing_required_fields(model, imported_fields, defaults) + if missing_without_fallback: + missing_fields_str = ', '.join(missing_without_fallback) + error_message = _( + "The following required fields are missing from your Excel file and cannot be auto-generated: %s. Please add these columns to your Excel file or ensure they have values." + ) % missing_fields_str + raise UserError(error_message) + + missing_required = set(required_fields) - imported_fields + table_name = self.env[model]._table + + m2m_columns = [] + m2m_trigger_val = {} + has_complex_fields = False + + for item in columns: + if 'fieldInfo' not in item: + continue + if item["fieldInfo"].get("type") == "many2many": + has_complex_fields = True + val = self.get_m2m_details(item['fieldInfo']['model_name'], item['fieldInfo']['id']) + m2m = f"m2m__{item['fieldInfo']['id']}" + m2m_trigger_val[m2m] = { + "data_table": self.env[item['fieldInfo']['comodel_name']]._table, + "mapping_table": val['relation_table'], + "column1": val['column1'], + "column2": val['column2'], + } + m2m_columns.append(m2m) + self.env.cr.execute(f"ALTER TABLE {table_name} ADD COLUMN IF NOT EXISTS {m2m} TEXT;") + + model_record = self.env['ir.model'].search([('model', '=', model)], limit=1) + if not model_record: + raise UserError(_("Model '%s' does not exist.") % model) + + initial_count = self.env[model].search_count([]) + + import_record = self.env['base_import.import'].browse(res_id).file + file_stream = BytesIO(import_record) + + data = pd.read_excel(file_stream, dtype=str) + data = data.replace({pd.NA: None, '': None}) + data = data.drop_duplicates() + data = data.rename(columns=column_mapping) + + for field in missing_required: + if field not in data.columns and field in defaults: + data[field] = [defaults[field]] * len(data) + + self._handle_special_required_fields(data, model, model_fields, missing_required) + + if 'state' in model_fields and model_fields['state']['type'] == 'selection': + if 'state' not in data.columns: + state_default = defaults.get('state', 'draft') + data['state'] = [state_default] * len(data) + else: + state_default = defaults.get('state', 'draft') + state_values = [] + for val in data['state']: + if pd.isna(val) or (isinstance(val, str) and val.strip() == ''): + state_values.append(state_default) + else: + state_values.append(val) + data['state'] = state_values + + many2one_fields = {} + for column in data.columns: + if column in model_fields and model_fields[column]['type'] == 'many2one': + comodel = model_fields[column]['relation'] + many2one_fields[column] = comodel + self._build_reference_cache(comodel, data[column], reference_cache) + + for column, comodel in many2one_fields.items(): + resolved_values = [] + for value in data[column]: + resolved_id = self._resolve_reference(comodel, value, reference_cache) if pd.notna(value) else None + resolved_values.append(resolved_id) + data[column] = resolved_values + + for column in data.columns: + if column in model_fields and model_fields[column]['type'] == 'selection': + selection_values = model_fields[column]['selection'] + if isinstance(selection_values, list): + selection_map = {v.lower(): k for k, v in selection_values} + selection_map.update({k.lower(): k for k, v in selection_values}) + mapped_values = [] + for value in data[column]: + if pd.isna(value): + mapped_values.append(None) + else: + str_val = str(value).strip().lower() + mapped_values.append(selection_map.get(str_val, value)) + data[column] = mapped_values + + fields_to_import = list(imported_fields.union(missing_required)) + + if 'state' in model_fields and 'state' in data.columns and 'state' not in fields_to_import: + fields_to_import.append('state') + + available_fields = [f for f in fields_to_import if f in data.columns] + + for field in fields_to_import: + if field not in available_fields and (field in defaults or field in data.columns): + if field in data.columns: + available_fields.append(field) + elif field in defaults: + data[field] = defaults[field] + available_fields.append(field) + + final_fields = [f for f in available_fields if f in model_fields or f == 'id'] + + if not final_fields: + raise UserError(_("No valid fields found for import")) + + try: + self.env.cr.execute(f"DROP TRIGGER IF EXISTS trg_process_m2m_mapping ON {table_name};") + except Exception: + pass + + return self._postgres_bulk_import(data, model, final_fields, m2m_trigger_val, m2m_columns, + table_name, model_fields, initial_count, model_record, + has_complex_fields, reference_cache) + except UserError: + raise + except Exception as e: + raise UserError(_("Import failed: %s") % str(e)) + + def _postgres_bulk_import(self, data, model, final_fields, m2m_trigger_val, m2m_columns, + table_name, model_fields, initial_count, model_record, + has_complex_fields, reference_cache): + try: + if 'name' in model_fields: + sequence = self._get_sequence_for_model(model) + needs_sequence = False + name_in_data = 'name' in data.columns + + if not name_in_data: + needs_sequence = True + else: + non_null_names = data['name'].dropna() + if len(non_null_names) == 0: + needs_sequence = True + else: + name_check_results = [ + str(val).strip().lower() in ['new', '', 'false'] for val in non_null_names + ] + needs_sequence = all(name_check_results) + + if sequence and needs_sequence: + record_count = len(data) + if record_count > 0: + try: + sequence_values = self._generate_bulk_sequences(sequence, record_count) + data['name'] = sequence_values + if 'name' not in final_fields: + final_fields.append('name') + except Exception: + pass + elif not sequence and needs_sequence: + timestamp = datetime.now().strftime('%Y%m%d-%H%M%S') + data['name'] = [f"New-{timestamp}-{i + 1}" for i in range(len(data))] + if 'name' not in final_fields: + final_fields.append('name') + + audit_fields = self._prepare_audit_fields() + audit_field_names = ['create_uid', 'write_uid', 'create_date', 'write_date'] + + for audit_field in audit_field_names: + if audit_field in model_fields and audit_field not in final_fields: + data[audit_field] = [audit_fields[audit_field]] * len(data) + final_fields.append(audit_field) + + if 'id' not in final_fields: + record_count = len(data) + if record_count > 0: + try: + next_ids = self._get_next_sequence_values(table_name, record_count) + data['id'] = next_ids + final_fields.insert(0, 'id') + except Exception: + if 'id' in final_fields: + final_fields.remove('id') + if 'id' in data.columns: + data = data.drop(columns=['id']) + + if has_complex_fields and m2m_trigger_val: + vals = json.dumps(m2m_trigger_val) + self.env.cr.execute( + f"CREATE OR REPLACE TRIGGER trg_process_m2m_mapping AFTER INSERT ON {table_name} " + f"FOR EACH ROW EXECUTE FUNCTION process_m2m_mapping('{vals}');" + ) + + data = data[final_fields] + default_lang = self.env.context.get('lang') or getattr(self.env, 'lang', None) or 'en_US' + translatable_columns = set() + + for column in data.columns: + if column in model_fields: + field_info = model_fields[column] + if field_info.get('translate') and field_info.get('store'): + translatable_columns.add(column) + + def _to_jsonb_value(val): + if pd.isna(val) or val is None: + return None + if isinstance(val, dict): + try: + return json.dumps(val, ensure_ascii=False) + except Exception: + return json.dumps({default_lang: str(val)}, ensure_ascii=False) + s = str(val).strip() + if s == '': + return None + if s.startswith('{') or s.startswith('['): + try: + parsed = json.loads(s) + return json.dumps(parsed, ensure_ascii=False) + except Exception: + return json.dumps({default_lang: s}, ensure_ascii=False) + return json.dumps({default_lang: s}, ensure_ascii=False) + + try: + jsonb_values = [_to_jsonb_value(val) for val in data[column]] + data[column] = jsonb_values + except Exception: + pass + + for column in data.columns: + if column in model_fields and column not in translatable_columns: + field_info = model_fields[column] + field_type = field_info['type'] + + if field_type in ['char', 'text']: + data[column] = (data[column].astype(str) + .str.replace(r'[\n\r]+', ' ', regex=True) + .str.strip()) + data[column] = data[column].replace(['nan', 'None'], None) + elif field_type in ['integer', 'float']: + if model_fields[column].get('required', False): + data[column] = pd.to_numeric(data[column], errors='coerce').fillna(0) + else: + data[column] = pd.to_numeric(data[column], errors='coerce') + elif field_type == 'boolean': + data[column] = data[column].fillna(False) + data[column] = ['t' if bool(val) else 'f' for val in data[column]] + elif field_type in ['date', 'datetime']: + formatted_dates = [] + current_datetime = datetime.now() + + for val in data[column]: + if pd.isna(val) or val in ['', None, 'nan', 'None']: + formatted_dates.append(current_datetime.strftime('%Y-%m-%d %H:%M:%S')) + else: + try: + if isinstance(val, str): + parsed_date = pd.to_datetime(val, errors='coerce') + if pd.isna(parsed_date): + parsed_date = datetime.strptime(val, '%Y-%m-%d') + formatted_dates.append(parsed_date.strftime('%Y-%m-%d %H:%M:%S')) + elif hasattr(val, 'strftime'): + formatted_dates.append(val.strftime('%Y-%m-%d %H:%M:%S')) + else: + formatted_dates.append(str(val)) + except Exception: + formatted_dates.append(current_datetime.strftime('%Y-%m-%d %H:%M:%S')) + data[column] = formatted_dates + elif field_type == 'many2one': + data[column] = pd.to_numeric(data[column], errors='coerce').astype('Int64') + + csv_buffer = BytesIO() + data_for_copy = data.copy() + + for column in data_for_copy.columns: + if column in model_fields: + field_info = model_fields[column] + field_type = field_info['type'] + + if field_info.get('translate') and field_info.get('store'): + translate_values = [str(val) if val is not None and not pd.isna(val) else '' for val in data_for_copy[column]] + data_for_copy[column] = translate_values + elif field_type in ['integer', 'float', 'many2one']: + numeric_values = [] + for val in data_for_copy[column]: + if val is None or pd.isna(val): + numeric_values.append('') + else: + try: + if field_type in ['integer', 'many2one']: + numeric_values.append(str(int(float(val)))) + else: + numeric_values.append(str(val)) + except Exception: + numeric_values.append('') + data_for_copy[column] = numeric_values + else: + other_values = [str(val) if val is not None and not pd.isna(val) else '' for val in data_for_copy[column]] + data_for_copy[column] = other_values + + data_for_copy.to_csv( + csv_buffer, index=False, header=False, sep='|', + na_rep='', quoting=csv.QUOTE_MINIMAL, doublequote=True + ) + csv_buffer.seek(0) + + self.env.cr.execute(f"ALTER TABLE {table_name} DISABLE TRIGGER ALL;") + if has_complex_fields and m2m_trigger_val: + self.env.cr.execute(f"ALTER TABLE {table_name} ENABLE TRIGGER trg_process_m2m_mapping;") + + fields_str = ",".join(final_fields) + copy_sql = f""" + COPY {table_name} ({fields_str}) + FROM STDIN WITH ( + FORMAT CSV, + HEADER FALSE, + DELIMITER '|', + NULL '', + QUOTE '"' + ) + """ + + start_time = datetime.now() + self.env.cr.copy_expert(copy_sql, csv_buffer) + record_count = len(data) + + if record_count > 500: + self.env.cr.commit() + + end_time = datetime.now() + import_duration = (end_time - start_time).total_seconds() + + self._sync_sequence_after_import(table_name) + self.env.cr.execute(f"ALTER TABLE {table_name} ENABLE TRIGGER ALL;") + + if has_complex_fields and m2m_trigger_val: + self.remove_m2m_temp_columns(table_name, m2m_columns) + + self.env.invalidate_all() + self.env.cr.execute(f"ANALYZE {table_name};") + + final_count = self.env[model].search_count([]) + imported_count = final_count - initial_count + + return { + 'name': model_record.name, + 'record_count': imported_count, + 'duration': import_duration + } + + except Exception as e: + try: + self.env.cr.execute(f"ALTER TABLE {table_name} ENABLE TRIGGER ALL;") + except Exception: + pass + + if has_complex_fields and m2m_trigger_val: + try: + self.remove_m2m_temp_columns(table_name, m2m_columns) + except Exception: + pass + + raise UserError(_("Failed to import data: %s") % str(e)) + + +class Import(models.TransientModel): + _inherit = 'base_import.import' + + @api.model + def get_fields_tree(self, model, depth=FIELDS_RECURSION_LIMIT): + Model = self.env[model] + importable_fields = [{ + 'id': 'id', + 'name': 'id', + 'string': _("External ID"), + 'required': False, + 'fields': [], + 'type': 'id', + }] + + if not depth: + return importable_fields + + model_fields = Model.fields_get() + for name, field in model_fields.items(): + if field.get('deprecated', False) is not False: + continue + if not field.get('store'): + continue + if field['type'] == 'one2many': + continue + + field_value = { + 'id': name, + 'name': name, + 'string': field['string'], + 'required': bool(field.get('required')), + 'fields': [], + 'type': field['type'], + 'model_name': model + } + + if field['type'] in ('many2many', 'many2one'): + field_value['fields'] = [ + dict(field_value, name='id', string=_("External ID"), type='id'), + dict(field_value, name='.id', string=_("Database ID"), type='id'), + ] + field_value['comodel_name'] = field['relation'] + + importable_fields.append(field_value) + + return importable_fields \ No newline at end of file