diff --git a/base_data_import/README.rst b/base_data_import/README.rst new file mode 100644 index 00000000..99bc6cb1 --- /dev/null +++ b/base_data_import/README.rst @@ -0,0 +1,58 @@ +================ +Base Data Import +================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-qrtl%2Fmi7--custom-lightgray.png?logo=github + :target: https://github.com/qrtl/mi7-custom/tree/15.0/base_data_import + :alt: qrtl/mi7-custom + +|badge1| |badge2| |badge3| + +This module does the following: + +- Adds a generic wizard to import a CSV file to create/update records. +- Adds a generic logging models and views for import results. + +This module is not useful by itself, but is expected to be used as a dependency +of specific data import functions. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Quartile Limited + +Maintainers +~~~~~~~~~~~ + +This module is part of the `qrtl/mi7-custom `_ project on GitHub. + +You are welcome to contribute. diff --git a/base_data_import/__init__.py b/base_data_import/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/base_data_import/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/base_data_import/__manifest__.py b/base_data_import/__manifest__.py new file mode 100644 index 00000000..95695c55 --- /dev/null +++ b/base_data_import/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2020-2022 Quartile Limited +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). +{ + "name": "Base Data Import", + "version": "16.0.1.0.0", + "category": "Hidden/Tools", + "author": "Quartile Limited", + "website": "https://www.quartile.co", + "license": "LGPL-3", + "depends": ["mail"], + "data": [ + "security/data_import_security.xml", + "security/ir.model.access.csv", + "views/data_import_log_views.xml", + "wizards/data_import_views.xml", + ], + "installable": True, +} diff --git a/base_data_import/i18n/ja.po b/base_data_import/i18n/ja.po new file mode 100644 index 00000000..99ba3e55 --- /dev/null +++ b/base_data_import/i18n/ja.po @@ -0,0 +1,406 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_data_import +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-28 14:38+0000\n" +"PO-Revision-Date: 2023-08-28 14:38+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_data_import +#. odoo-python +#: code:addons/base_data_import/wizards/data_import.py:0 +#, python-format +msgid "%(label)s is missing." +msgstr "%(label)sがありません。" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_needaction +msgid "Action Needed" +msgstr "要アクション" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_ids +msgid "Activities" +msgstr "アクティビティ" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_state +msgid "Activity State" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_attachment_count +msgid "Attachment Count" +msgstr "添付数" + +#. module: base_data_import +#: model_terms:ir.ui.view,arch_db:base_data_import.view_data_import +msgid "Cancel" +msgstr "取消" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__company_id +msgid "Company" +msgstr "会社" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__create_uid +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__create_uid +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__create_uid +msgid "Created by" +msgstr "作成者" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__create_date +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__create_date +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__create_date +msgid "Created on" +msgstr "作成日" + +#. module: base_data_import +#: model:ir.model,name:base_data_import.model_data_import +#: model:ir.ui.menu,name:base_data_import.data_import_main_menu +#: model:ir.ui.menu,name:base_data_import.menu_data_import_log +msgid "Data Import" +msgstr "データインポート" + +#. module: base_data_import +#: model:ir.model,name:base_data_import.model_data_import_error +msgid "Data Import Error" +msgstr "データインポートエラー" + +#. module: base_data_import +#: model:ir.model,name:base_data_import.model_data_import_log +#: model:res.groups,name:base_data_import.group_data_import_log +msgid "Data Import Log" +msgstr "データインポートログ" + +#. module: base_data_import +#: model:ir.ui.menu,name:base_data_import.menu_data_import_setting +msgid "Data Import Settings" +msgstr "データインポート設定" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__display_name +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__display_name +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__display_name +msgid "Display Name" +msgstr "表示名" + +#. module: base_data_import +#: model:ir.model.fields.selection,name:base_data_import.selection__data_import_log__state__done +msgid "Done" +msgstr "完了" + +#. module: base_data_import +#: model_terms:ir.ui.view,arch_db:base_data_import.data_import_log_form +msgid "Error Lines" +msgstr "エラー行" + +#. module: base_data_import +#: model:ir.model.fields.selection,name:base_data_import.selection__data_import_log__state__failed +msgid "Failed" +msgstr "失敗" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__import_file +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__input_file +msgid "File" +msgstr "ファイル" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__file_name +msgid "File Name" +msgstr "ファイル名" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: base_data_import +#. odoo-python +#: code:addons/base_data_import/wizards/data_import.py:0 +#, python-format +msgid "" +"Following columns are missing: \n" +"%s" +msgstr "" +"次のカラムが必要です: \n" +"%s" + +#. module: base_data_import +#: model:ir.model.fields,help:base_data_import.field_data_import_log__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: base_data_import +#: model_terms:ir.ui.view,arch_db:base_data_import.import_log_filter +msgid "Group By..." +msgstr "グループ化..." + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__has_message +msgid "Has Message" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__id +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__id +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__id +msgid "ID" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,help:base_data_import.field_data_import_log__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,help:base_data_import.field_data_import_log__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,help:base_data_import.field_data_import_log__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: base_data_import +#: model_terms:ir.ui.view,arch_db:base_data_import.import_log_filter +msgid "Import Date" +msgstr "インポート日" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__import_log_id +#: model_terms:ir.ui.view,arch_db:base_data_import.data_import_log_form +msgid "Import Log" +msgstr "インポートログ" + +#. module: base_data_import +#. odoo-python +#: code:addons/base_data_import/wizards/data_import.py:0 +#, python-format +msgid "Import Result" +msgstr "インポート結果" + +#. module: base_data_import +#: model:ir.model.fields.selection,name:base_data_import.selection__data_import_log__state__imported +msgid "Imported" +msgstr "インポート済" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__import_user_id +#: model_terms:ir.ui.view,arch_db:base_data_import.import_log_filter +msgid "Imported By" +msgstr "インポート担当者" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__file_path +msgid "Imported File" +msgstr "インポートファイル" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__import_date +msgid "Imported On" +msgstr "インポート日" + +#. module: base_data_import +#. odoo-python +#: code:addons/base_data_import/wizards/data_import.py:0 +#, python-format +msgid "Invalid file!" +msgstr "ファイルが無効です。" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_is_follower +msgid "Is Follower" +msgstr "フォロワーである" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import____last_update +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error____last_update +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log____last_update +msgid "Last Modified on" +msgstr "最終修正日" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__write_uid +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__write_uid +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__write_uid +msgid "Last Updated by" +msgstr "最終更新者" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import__write_date +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__write_date +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__write_date +msgid "Last Updated on" +msgstr "最終更新日" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__log_id +msgid "Log" +msgstr "ログ" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__error_ids +msgid "Log Lines" +msgstr "ログ行" + +#. module: base_data_import +#: model_terms:ir.ui.view,arch_db:base_data_import.data_import_log_form +msgid "Logs" +msgstr "ログ" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__error_message +msgid "Message" +msgstr "メッセージ" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_ids +msgid "Messages" +msgstr "メッセージ" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__model_id +#: model_terms:ir.ui.view,arch_db:base_data_import.import_log_filter +msgid "Model" +msgstr "モデル" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__model_name +msgid "Model Name" +msgstr "モデル名" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__file_name +msgid "Name" +msgstr "名称" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,help:base_data_import.field_data_import_log__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,help:base_data_import.field_data_import_log__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__reference +msgid "Reference" +msgstr "参照" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__activity_user_id +msgid "Responsible User" +msgstr "担当者" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_error__row_no +msgid "Row Number" +msgstr "行番号" + +#. module: base_data_import +#: model_terms:ir.ui.view,arch_db:base_data_import.import_log_filter +msgid "Search Logs" +msgstr "ログ検索" + +#. module: base_data_import +#: model:ir.model.fields,field_description:base_data_import.field_data_import_log__state +#: model_terms:ir.ui.view,arch_db:base_data_import.import_log_filter +msgid "Status" +msgstr "ステータス" + +#. module: base_data_import +#: model:ir.model.fields,help:base_data_import.field_data_import_log__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: base_data_import +#: model:ir.model.fields,help:base_data_import.field_data_import_log__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: base_data_import +#. odoo-python +#: code:addons/base_data_import/wizards/data_import.py:0 +#, python-format +msgid "Unexpected value for %(label)s (%(errored_type)s)" +msgstr "%(label)s(%(errored_type)s)の値が不正です。" diff --git a/base_data_import/models/__init__.py b/base_data_import/models/__init__.py new file mode 100644 index 00000000..ae8399d0 --- /dev/null +++ b/base_data_import/models/__init__.py @@ -0,0 +1,2 @@ +from . import data_import_error +from . import data_import_log diff --git a/base_data_import/models/data_import_error.py b/base_data_import/models/data_import_error.py new file mode 100644 index 00000000..87147d23 --- /dev/null +++ b/base_data_import/models/data_import_error.py @@ -0,0 +1,14 @@ +# Copyright 2020-2021 Quartile Limited +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class DataImportError(models.Model): + _name = "data.import.error" + _description = "Data Import Error" + + row_no = fields.Integer("Row Number") + reference = fields.Char() + error_message = fields.Text("Message") + log_id = fields.Many2one("data.import.log", string="Log") diff --git a/base_data_import/models/data_import_log.py b/base_data_import/models/data_import_log.py new file mode 100644 index 00000000..a4b91bda --- /dev/null +++ b/base_data_import/models/data_import_log.py @@ -0,0 +1,35 @@ +# Copyright 2020-2021 Quartile Limited +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class DataImportLog(models.Model): + _name = "data.import.log" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Data Import Log" + _rec_name = "file_name" + _order = "id DESC" + + import_date = fields.Datetime("Imported On") + import_user_id = fields.Many2one("res.users", "Imported By") + company_id = fields.Many2one( + "res.company", "Company", default=lambda self: self.env.company + ) + error_ids = fields.One2many("data.import.error", "log_id", string="Log Lines") + input_file = fields.Many2one("ir.attachment", string="File") + file_path = fields.Binary(related="input_file.datas", string="Imported File") + file_name = fields.Char(related="input_file.name") + state = fields.Selection( + [("failed", "Failed"), ("imported", "Imported"), ("done", "Done")], + string="Status", + ) + model_id = fields.Many2one("ir.model", string="Model") + model_name = fields.Char(related="model_id.model", string="Model Name") + # error_handling = fields.Selection( + # selection=[("stop", "Stop"), ("ignore", "Ignore")], + # help="If you select 'Stop', no record should be imported when there is an " + # "error in the import file (error log will be generated)." + # "If you select 'Ignore', the import process will continue for all records " + # "except for the ones with an error.", + # ) diff --git a/base_data_import/readme/DESCRIPTION.rst b/base_data_import/readme/DESCRIPTION.rst new file mode 100644 index 00000000..f08422f1 --- /dev/null +++ b/base_data_import/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +This module does the following: + +- Adds a generic wizard to import a CSV file to create/update records. +- Adds a generic logging models and views for import results. + +This module is not useful by itself, but is expected to be used as a dependency +of specific data import functions. diff --git a/base_data_import/security/data_import_security.xml b/base_data_import/security/data_import_security.xml new file mode 100644 index 00000000..d02339ff --- /dev/null +++ b/base_data_import/security/data_import_security.xml @@ -0,0 +1,14 @@ + + + + Data Import Log + + + + Data Import Log multi-company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + diff --git a/base_data_import/security/ir.model.access.csv b/base_data_import/security/ir.model.access.csv new file mode 100644 index 00000000..0a73d267 --- /dev/null +++ b/base_data_import/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_data_import_user,data.import.user,model_data_import,base.group_user,1,0,0,0 +access_data_import_log_user,data.import.log.user,model_data_import_log,base.group_user,1,0,1,0 +access_data_import_log_system,data.import.log.system,model_data_import_log,base.group_system,1,1,1,1 +access_data_import_error_user,data.import.error.user,model_data_import_error,base.group_user,1,0,1,0 +access_data_import_error_system,data.import.error.system,model_data_import_log,base.group_system,1,1,1,1 diff --git a/base_data_import/static/description/index.html b/base_data_import/static/description/index.html new file mode 100644 index 00000000..ceb361a3 --- /dev/null +++ b/base_data_import/static/description/index.html @@ -0,0 +1,413 @@ + + + + + + +Base Data Import + + + +
+

Base Data Import

+ + +

Beta License: LGPL-3 qrtl/mi7-custom

+

This module does the following:

+
    +
  • Adds a generic wizard to import a CSV file to create/update records.
  • +
  • Adds a generic logging models and views for import results.
  • +
+

This module is not useful by itself, but is expected to be used as a dependency +of specific data import functions.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile Limited
  • +
+
+
+

Maintainers

+

This module is part of the qrtl/mi7-custom project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/base_data_import/views/data_import_log_views.xml b/base_data_import/views/data_import_log_views.xml new file mode 100644 index 00000000..a308bbbe --- /dev/null +++ b/base_data_import/views/data_import_log_views.xml @@ -0,0 +1,128 @@ + + + + + data.import.log.form + data.import.log + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+ + data.import.log.tree + data.import.log + + + + + + + + + + + + + data.import.log.select + data.import.log + + + + + + + + + + + + + + + + + + + + + +
diff --git a/base_data_import/wizards/__init__.py b/base_data_import/wizards/__init__.py new file mode 100644 index 00000000..3d071559 --- /dev/null +++ b/base_data_import/wizards/__init__.py @@ -0,0 +1 @@ +from . import data_import diff --git a/base_data_import/wizards/data_import.py b/base_data_import/wizards/data_import.py new file mode 100644 index 00000000..81e04c8c --- /dev/null +++ b/base_data_import/wizards/data_import.py @@ -0,0 +1,155 @@ +# Copyright 2020-2023 Quartile Limited +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +import csv +import io +import logging +from base64 import b64decode +from collections import OrderedDict +from datetime import datetime +from html import escape, unescape + +from odoo import _, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class DataImport(models.TransientModel): + _name = "data.import" + _description = "Data Import" + + import_file = fields.Binary(string="File") + file_name = fields.Char() + # error_handling = fields.Selection( + # selection=[("stop", "Stop"), ("ignore", "Ignore")], + # help="If you select 'Stop', no record should be imported when there is an " + # "error in the import file (error log will be generated)." + # "If you select 'Ignore', the import process will continue for all records " + # "except for the ones with an error.", + # default="stop", + # ) + import_log_id = fields.Many2one("data.import.log", string="Import Log") + + def _create_import_log(self, res_model_name, log_model_name=None): + if not log_model_name: + log_model_name = "data.import.log" + res_model = self.env["ir.model"].search([("model", "=", res_model_name)]) + attachment = self.env["ir.attachment"] + if self.import_file: + attachment = self.env["ir.attachment"].create( + {"name": self.file_name, "datas": self.import_file} + ) + import_log = self.env[log_model_name].create( + { + "input_file": attachment.id, + "import_user_id": self.env.user.id, + "import_date": datetime.now(), + "state": "failed", + "model_id": res_model.id, + } + ) + return import_log + + def _get_field_defs(self, FIELD_KEYS, FIELD_VALS): + ordered_index = OrderedDict(sorted(FIELD_KEYS.items())) + field_defs = [] + for field in FIELD_VALS: + field_def = {} + for k, v in ordered_index.items(): + field_def[v] = field[k] + field_defs.append(field_def) + return field_defs + + def _load_import_file(self, field_defs, encodings=None): + """We assume that there is a header line in the imported CSV.""" + if encodings is None: + encodings = ["utf-8"] + csv_data = b64decode(self.import_log_id.input_file.datas) + sheet_fields = [] + for encoding in encodings: + try: + csv_data = csv_data.decode(encoding) + csv_iterator = csv.reader(io.StringIO(csv_data), delimiter=",") + sheet_fields = next(csv_iterator) + break + except Exception: + _logger.exception("Error while capturing sheet fields.") + if not sheet_fields: + raise UserError(_("Invalid file!")) + missing_columns = list( + {field_def["label"] for field_def in field_defs} - set(sheet_fields) + ) + if missing_columns: + raise UserError( + _("Following columns are missing: \n%s") % "\n".join(missing_columns) + ) + return sheet_fields, csv_iterator + + def _check_value_type(self, field_type, value, date_formats): + # numeric fields + if field_type == "float": + try: + float(value) + return False + except Exception: + return field_type + # date fields + elif field_type == "date": + date = False + for date_format in date_formats: + try: + date = datetime.strptime(value, date_format) + break + except Exception: + _logger.exception("Error while validating the date.") + return field_type if not date else False + + def _unescape_field_vals(self, row_dict, field_list): + """Revert escaping of HTML special characters (e.g. '>') for some fields.""" + for key, val in row_dict.items(): + if val and key in field_list: + row_dict[key] = unescape(val) + return row_dict + + def _check_field_vals(self, field_defs, row, sheet_fields, date_formats=None): + if date_formats is None: + date_formats = ["%Y-%m-%d", "%Y/%m/%d"] + error_list = [] + row_dict = {} + for field_def in field_defs: + field = field_def["field"] + label = field_def["label"] + field_type = field_def["field_type"] + required = field_def["required"] + # Unescape the value in inheriting modules as necessary. + value = escape(row[sheet_fields.index(label)]) + if required and not value: + error_list.append(_("%(label)s is missing.", label=label)) + else: + errored_type = self._check_value_type(field_type, value, date_formats) + if errored_type: + message = _( + "Unexpected value for %(label)s (%(errored_type)s)", + label=label, + errored_type=errored_type, + ) + error_list.append(message) + row_dict[field] = value + return row_dict, error_list + + def _action_open_import_log(self, import_log, view_id=None, log_model_name=None): + if not log_model_name: + log_model_name = "data.import.log" + if not view_id: + view_id = self.env.ref("base_data_import.data_import_log_form").id + return { + "type": "ir.actions.act_window", + "name": _("Import Result"), + "res_model": log_model_name, + "view_type": "form", + "view_mode": "form", + "res_id": import_log.id, + "view_id": view_id, + "target": "current", + } diff --git a/base_data_import/wizards/data_import_views.xml b/base_data_import/wizards/data_import_views.xml new file mode 100644 index 00000000..4c67bb8f --- /dev/null +++ b/base_data_import/wizards/data_import_views.xml @@ -0,0 +1,26 @@ + + + + view.data.import + data.import + +
+ + + + + + + + +
+
+
+
+
+
diff --git a/product_plm_import/README.rst b/product_plm_import/README.rst new file mode 100644 index 00000000..3ee6c5c7 --- /dev/null +++ b/product_plm_import/README.rst @@ -0,0 +1,84 @@ +================== +Product PLM Import +================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-qrtl%2Fqrtl--custom-lightgray.png?logo=github + :target: https://github.com/qrtl/qrtl-custom/tree/16.0/product_plm_import + :alt: qrtl/qrtl-custom + +|badge1| |badge2| |badge3| + +This module adds a CSV import function to create new products based on the data received +from the PLM system. + +This module depends on base_data_import module. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Update fields in the company (in the 'PLM I/F' tab): + +- PLM Path: the absolute path to the PLM directory to fetch the files from. +- PLM Notification Body: the text will be included in the notification email body. +- PLM Notified Groups: assign groups to notify when a new file is fetched from the PLM. + +Usage +===== + +There are three ir.cron records added by this module: + +#. PLM: Import PLM Products + The main cron job that imports PLM product records into Odoo in a periodical manner. +#. PLM: Create products based on imported PLM records + A job that creates products in Odoo based on the imported PLM records. Triggerd by + the main job. +#. PLM: Send email notification on PLM data import + A job that sends email notifications to the relevant users. Triggerd by the main job. + Notification email is designed to be sent only once per the log record. + +Alternatively, users can import PLM product records manually via 'Product PLM Import' +wizard, which also triggers the last two jobs. + +The status of an import log record becomes 'Done' when a product is successfully created +or marked as 'Solved' for all the imported records. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Quartile Limited + +Maintainers +~~~~~~~~~~~ + +This module is part of the `qrtl/qrtl-custom `_ project on GitHub. + +You are welcome to contribute. diff --git a/product_plm_import/__init__.py b/product_plm_import/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/product_plm_import/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/product_plm_import/__manifest__.py b/product_plm_import/__manifest__.py new file mode 100644 index 00000000..2b89109a --- /dev/null +++ b/product_plm_import/__manifest__.py @@ -0,0 +1,33 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Product PLM Import", + "version": "16.0.1.0.0", + "category": "Stock", + "license": "AGPL-3", + "author": "Quartile Limited", + "website": "https://www.quartile.co", + "depends": [ + "purchase_stock", + "product_lot_sequence", # lot_sequence_padding, lot_sequence_prefix + "stock_picking_auto_create_lot", # auto_create_lot + "base_data_import", + "product_alternative_code", + ], + "data": [ + "security/ir.model.access.csv", + "security/security.xml", + "data/ir_cron_data.xml", + "data/menu_item_data.xml", + "views/data_import_log_views.xml", + "views/plm_category_views.xml", + "views/plm_item_type_views.xml", + "views/plm_procure_flag_views.xml", + "views/plm_product_mapping_views.xml", + "views/product_plm_views.xml", + "views/product_template_views.xml", + "views/res_company_views.xml", + "wizards/product_plm_import_views.xml", + ], + "installable": True, +} diff --git a/product_plm_import/data/ir_cron_data.xml b/product_plm_import/data/ir_cron_data.xml new file mode 100644 index 00000000..5359b945 --- /dev/null +++ b/product_plm_import/data/ir_cron_data.xml @@ -0,0 +1,32 @@ + + + + PLM: Import PLM Products + + code + model.import_product_from_plm_path() + + 1 + days + -1 + + + + PLM: Create products based on imported PLM records + + code + model.create_products(batch_size=10) + + days + -1 + + + PLM: Send email notification on PLM data import + + code + model._send_plm_import_notification() + + days + -1 + + diff --git a/product_plm_import/data/menu_item_data.xml b/product_plm_import/data/menu_item_data.xml new file mode 100644 index 00000000..5cf69450 --- /dev/null +++ b/product_plm_import/data/menu_item_data.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/product_plm_import/i18n/ja.po b/product_plm_import/i18n/ja.po new file mode 100644 index 00000000..0312a58f --- /dev/null +++ b/product_plm_import/i18n/ja.po @@ -0,0 +1,728 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_plm_import +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-12 03:36+0000\n" +"PO-Revision-Date: 2023-09-12 03:36+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_plm_import +#. odoo-python +#: code:addons/product_plm_import/models/plm_import_log.py:0 +#, python-format +msgid " [Odoo] Product IF Notification: %s" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_category__name +msgid "" +"Accepts wildcard matching. E.g. `*` matches any string, `?` matches any " +"single character." +msgstr "ワイルドカードマッチングに対応。`*`: 文字列にマッチ、`?`: 特定の1文字にマッチ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_needaction +msgid "Action Needed" +msgstr "要アクション" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__active +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__active +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__active +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__active +msgid "Active" +msgstr "有効" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_ids +msgid "Activities" +msgstr "アクティビティ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_state +msgid "Activity State" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_attachment_count +msgid "Attachment Count" +msgstr "添付数" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__auto_create_lot +msgid "Auto Create Lot" +msgstr "ロット自動作成" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__category_ids +msgid "Categories" +msgstr "カテゴリ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__category +msgid "Category" +msgstr "カテゴリ" + +#. module: product_plm_import +#: model:ir.model,name:product_plm_import.model_res_company +msgid "Companies" +msgstr "会社" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__company_id +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__company_id +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__company_id +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__company_id +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__company_id +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__company_id +msgid "Company" +msgstr "会社" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__create_uid +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__create_uid +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__create_uid +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__create_uid +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__create_uid +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__create_uid +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__create_uid +msgid "Created by" +msgstr "作成者" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__create_date +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__create_date +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__create_date +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__create_date +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__create_date +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__create_date +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__create_date +msgid "Created on" +msgstr "作成日" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__default_active +msgid "Default Active" +msgstr "有効/無効初期値" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_product_mapping__default_active +msgid "Default value for active field of the created product." +msgstr "作成されるプロダクトの有効/無効初期値" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__description +msgid "Description" +msgstr "内部説明" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__display_name +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__display_name +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__display_name +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__display_name +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__display_name +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__display_name +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__display_name +msgid "Display Name" +msgstr "表示名" + +#. module: product_plm_import +#: model:ir.model.fields.selection,name:product_plm_import.selection__plm_import_log__plm_product_state__done +#: model:ir.model.fields.selection,name:product_plm_import.selection__product_plm__state__done +msgid "Done" +msgstr "完了" + +#. module: product_plm_import +#: model:ir.model.fields.selection,name:product_plm_import.selection__plm_import_log__plm_product_state__draft +#: model:ir.model.fields.selection,name:product_plm_import.selection__product_plm__state__draft +msgid "Draft" +msgstr "ドラフト" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__drawing +msgid "Drawing No." +msgstr "図面番号" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__esc_code +msgid "ESC ID" +msgstr "ESC番号" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__error_message +msgid "Error Message" +msgstr "エラーメッセージ" + +#. module: product_plm_import +#: model:ir.model.fields.selection,name:product_plm_import.selection__plm_import_log__plm_product_state__failed +#: model:ir.model.fields.selection,name:product_plm_import.selection__product_plm__state__failed +msgid "Failed" +msgstr "失敗" + +#. module: product_plm_import +#. odoo-python +#: code:addons/product_plm_import/models/product_plm.py:0 +#, python-format +msgid "Failed to create product." +msgstr "プロダクト作成に失敗しました。" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__input_file +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__import_file +msgid "File" +msgstr "ファイル" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__file_name +msgid "File Name" +msgstr "ファイル名" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__generic_name +msgid "Generic Name" +msgstr "汎用名称" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__has_message +msgid "Has Message" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__id +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__id +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__id +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__id +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__id +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__id +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__id +msgid "ID" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: product_plm_import +#: model_terms:ir.ui.view,arch_db:product_plm_import.view_product_plm_import +msgid "Import Data" +msgstr "インポート" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__import_log_id +msgid "Import Log" +msgstr "インポートログ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__import_user_id +msgid "Imported By" +msgstr "インポート担当者" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__file_path +msgid "Imported File" +msgstr "インポートファイル" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__import_date +msgid "Imported On" +msgstr "インポート日" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__plm_product_ids +#: model_terms:ir.ui.view,arch_db:product_plm_import.view_product_plm_import_log_form +msgid "Imported PLM Products" +msgstr "インポート済PLMプロダクト" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_is_follower +msgid "Is Follower" +msgstr "フォロワーである" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_product__is_via_plm +#: model:ir.model.fields,field_description:product_plm_import.field_product_template__is_via_plm +msgid "Is Via Plm" +msgstr "PLM経由" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__item_type_id +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__item_type +msgid "Item Type" +msgstr "アイテムタイプ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category____last_update +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log____last_update +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type____last_update +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag____last_update +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping____last_update +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm____last_update +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import____last_update +msgid "Last Modified on" +msgstr "最終変更日" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__write_uid +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__write_uid +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__write_uid +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__write_uid +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__write_uid +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__write_uid +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__write_uid +msgid "Last Updated by" +msgstr "最終更新者" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__write_date +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__write_date +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__write_date +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__write_date +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__write_date +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__write_date +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm_import__write_date +msgid "Last Updated on" +msgstr "最終更新日" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__log_id +msgid "Log" +msgstr "ログ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__error_ids +msgid "Log Lines" +msgstr "ログ明細行" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__lot_sequence_padding +msgid "Lot Sequence Padding" +msgstr "ロット付番パディング" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__lot_sequence_prefix +msgid "Lot Sequence Prefix" +msgstr "ロット付番プレフィクス" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__mapping_id +msgid "Mapping" +msgstr "マッピング" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_ids +msgid "Messages" +msgstr "メッセージ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__model_id +msgid "Model" +msgstr "モデル" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__model_name +msgid "Model Name" +msgstr "モデル名" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields.selection,name:product_plm_import.selection__plm_import_log__plm_product_state__na +msgid "N/A" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_category__name +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__file_name +#: model:ir.model.fields,field_description:product_plm_import.field_plm_item_type__name +#: model:ir.model.fields,field_description:product_plm_import.field_plm_procure_flag__name +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__name +msgid "Name" +msgstr "名称" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_calendar_event_id +msgid "Next Activity Calendar Event" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: product_plm_import +#. odoo-python +#: code:addons/product_plm_import/wizards/product_plm_import.py:0 +#, python-format +msgid "No PLM-product mapping record found." +msgstr "PLM-プロダクトマッピングレコードが見つかりません。" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__notification_sent +msgid "Notification Sent" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: product_plm_import +#: model:ir.actions.act_window,name:product_plm_import.action_plm_category +#: model:ir.ui.menu,name:product_plm_import.menu_plm_category +msgid "PLM Categories" +msgstr "PLMカテゴリ" + +#. module: product_plm_import +#: model:ir.model,name:product_plm_import.model_plm_category +msgid "PLM Category" +msgstr "PLMカテゴリ" + +#. module: product_plm_import +#: model:ir.model,name:product_plm_import.model_plm_import_log +msgid "PLM Data Import Log" +msgstr "PLMデータインポートログ" + +#. module: product_plm_import +#: model_terms:ir.ui.view,arch_db:product_plm_import.product_template_form_view +#: model_terms:ir.ui.view,arch_db:product_plm_import.view_company_form +msgid "PLM I/F" +msgstr "PLM連携" + +#. module: product_plm_import +#: model:ir.model,name:product_plm_import.model_plm_item_type +msgid "PLM Item Type" +msgstr "PLMアイテムタイプ" + +#. module: product_plm_import +#: model:ir.actions.act_window,name:product_plm_import.action_plm_item_type +#: model:ir.ui.menu,name:product_plm_import.menu_plm_item_type +msgid "PLM Item Types" +msgstr "PLMアイテムタイプ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_res_company__plm_last_import_date +msgid "PLM Last Import Date" +msgstr "PLM最終インポート日" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_res_company__plm_notif_body +msgid "PLM Notification Body" +msgstr "PLM通知本文" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_res_company__plm_notif_group_ids +msgid "PLM Notified Groups" +msgstr "PLM通知グループ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_res_company__plm_path +msgid "PLM Path" +msgstr "PLMパス" + +#. module: product_plm_import +#: model:ir.model,name:product_plm_import.model_plm_procure_flag +msgid "PLM Procure Flag" +msgstr "PLM調達フラグ" + +#. module: product_plm_import +#: model:ir.actions.act_window,name:product_plm_import.action_plm_procure_flag +#: model:ir.ui.menu,name:product_plm_import.menu_plm_procure_flag +msgid "PLM Procure Flags" +msgstr "PLM調達フラグ" + +#. module: product_plm_import +#: model:ir.actions.act_window,name:product_plm_import.action_product_plm +#: model:ir.ui.menu,name:product_plm_import.menu_product_followup +#: model:ir.ui.menu,name:product_plm_import.menu_product_plm +msgid "PLM Products" +msgstr "PLMプロダクト" + +#. module: product_plm_import +#: model:ir.ui.menu,name:product_plm_import.menu_product_plm_import_setting +msgid "PLM Settings" +msgstr "PLM設定" + +#. module: product_plm_import +#: model:ir.actions.act_window,name:product_plm_import.action_plm_product_mapping +#: model:ir.model,name:product_plm_import.model_plm_product_mapping +#: model:ir.ui.menu,name:product_plm_import.menu_plm_product_mapping +msgid "PLM-Product Mapping" +msgstr "PLM-プロダクトマッピング" + +#. module: product_plm_import +#: model:ir.actions.server,name:product_plm_import.create_products_for_plm_import_ir_actions_server +#: model:ir.actions.server,name:product_plm_import.ir_cron_create_products_for_plm_import_ir_actions_server +#: model:ir.cron,cron_name:product_plm_import.ir_cron_create_products_for_plm_import +msgid "PLM: Create products based on imported PLM records" +msgstr "PLM: インポートされたPLMレコードにつきプロダクトを作成" + +#. module: product_plm_import +#: model:ir.actions.server,name:product_plm_import.ir_cron_import_product_plm_ir_actions_server +#: model:ir.cron,cron_name:product_plm_import.ir_cron_import_product_plm +msgid "PLM: Import PLM Products" +msgstr "PLM: PLMプロダクトインポート" + +#. module: product_plm_import +#: model:ir.actions.server,name:product_plm_import.ir_cron_send_plm_import_notification_ir_actions_server +#: model:ir.cron,cron_name:product_plm_import.ir_cron_send_plm_import_notification +msgid "PLM: Send email notification on PLM data import" +msgstr "PLM: PLMインポートの通知メール送信" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__part_number +msgid "Part Number" +msgstr "部品番号" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_res_company__plm_path +msgid "" +"Path to PLM CSV files. It can end with a slash (/) or without - both are " +"acceptable." +msgstr "" +"PLMのCSVファイルディレクトリのパス。末尾はスラッシュ(/)があってもなくてもよいです。" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__plm_product_state +msgid "Plm Product State" +msgstr "PLMプロダクトステータス" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__procure_flag_ids +msgid "Procure Flags" +msgstr "調達フラグ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__procure_flag +msgid "Procure flag" +msgstr "調達フラグ" + +#. module: product_plm_import +#: model:ir.model,name:product_plm_import.model_product_template +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__product_id +msgid "Product" +msgstr "プロダクト" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__product_categ_id +msgid "Product Category" +msgstr "プロダクトカテゴリ" + +#. module: product_plm_import +#: model:ir.model,name:product_plm_import.model_product_plm +msgid "Product PLM" +msgstr "プロダクトPLM" + +#. module: product_plm_import +#: model:ir.actions.act_window,name:product_plm_import.action_product_plm_import_wizard +msgid "Product PLM CSV Import" +msgstr "プロダクトPLM CSVインポート" + +#. module: product_plm_import +#: model:ir.model,name:product_plm_import.model_product_plm_import +#: model:ir.ui.menu,name:product_plm_import.menu_product_plm_import +#: model:ir.ui.menu,name:product_plm_import.product_plm_import_wizard_menu +msgid "Product PLM Import" +msgstr "プロダクトPLMインポート" + +#. module: product_plm_import +#: model:ir.actions.act_window,name:product_plm_import.action_product_plm_import_log +#: model:ir.ui.menu,name:product_plm_import.menu_product_plm_import_log +msgid "Product PLM Import Log" +msgstr "プロダクトPLMインポートログ" + +#. module: product_plm_import +#: model:ir.actions.act_window,name:product_plm_import.action_product_followup +msgid "Product Templates (WIP)" +msgstr "プロダクトテンプレート(仕掛中)" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__product_type +msgid "Product Type" +msgstr "プロダクトタイプ" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__activity_user_id +msgid "Responsible User" +msgstr "担当者" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__route_ids +msgid "Routes" +msgstr "ルート" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__row_no +msgid "Row No." +msgstr "行番号" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__solved +msgid "Solved" +msgstr "解決済" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__spec +msgid "Spec" +msgstr "規格" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__state +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__state +#: model_terms:ir.ui.view,arch_db:product_plm_import.view_product_plm_import_log_select +#: model_terms:ir.ui.view,arch_db:product_plm_import.view_product_plm_import_log_tree +msgid "Status" +msgstr "ステータス" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: product_plm_import +#. odoo-python +#: code:addons/product_plm_import/wizards/product_plm_import.py:0 +#, python-format +msgid "There is already a product for %s." +msgstr "%s に対応するプロダクトが既に存在します。" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_res_company__plm_last_import_date +msgid "" +"This date is updated every time PLM files for import are searched in PLM " +"Path, and is used as a threshold for the next search." +msgstr "" +"この日付はPLMパスにてインポート対象ファイルが検索される度に更新され、次回の検索の基準日として使われます。" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_product_mapping__tracking +msgid "Tracking" +msgstr "追跡" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_product_plm__uom +msgid "Unit of Material" +msgstr "単位" + +#. module: product_plm_import +#: model_terms:ir.ui.view,arch_db:product_plm_import.product_template_search_view +msgid "Via PLM" +msgstr "PLM経由" + +#. module: product_plm_import +#: model:ir.ui.menu,name:product_plm_import.menu_product_wip +msgid "WIP Products" +msgstr "仕掛中プロダクト" + +#. module: product_plm_import +#: model:ir.model.fields,field_description:product_plm_import.field_plm_import_log__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: product_plm_import +#: model:ir.model.fields,help:product_plm_import.field_plm_import_log__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: product_plm_import +#. odoo-python +#: code:addons/product_plm_import/models/product_plm.py:0 +#, python-format +msgid "You cannot set a record as solved and done at the same time." +msgstr "レコードを解決済かつ完了とすることはできません。" diff --git a/product_plm_import/models/__init__.py b/product_plm_import/models/__init__.py new file mode 100644 index 00000000..779649b8 --- /dev/null +++ b/product_plm_import/models/__init__.py @@ -0,0 +1,8 @@ +from . import plm_category +from . import plm_import_log +from . import plm_item_type +from . import plm_procure_flag +from . import plm_product_mapping +from . import product_plm +from . import product_template +from . import res_company diff --git a/product_plm_import/models/plm_category.py b/product_plm_import/models/plm_category.py new file mode 100644 index 00000000..70fa702e --- /dev/null +++ b/product_plm_import/models/plm_category.py @@ -0,0 +1,18 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PlmCategory(models.Model): + _name = "plm.category" + _description = "PLM Category" + _order = "name" + + name = fields.Char( + required=True, + help="Accepts wildcard matching. E.g. `*` matches any string, `?` matches any " + "single character.", + ) + company_id = fields.Many2one("res.company") + active = fields.Boolean(default=True) diff --git a/product_plm_import/models/plm_import_log.py b/product_plm_import/models/plm_import_log.py new file mode 100644 index 00000000..5f20780b --- /dev/null +++ b/product_plm_import/models/plm_import_log.py @@ -0,0 +1,115 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class PlmDataImportLog(models.Model): + _name = "plm.import.log" + _inherit = "data.import.log" + _description = "PLM Data Import Log" + + plm_product_state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("failed", "Failed"), + ("done", "Done"), + ("na", "N/A"), + ], + compute="_compute_plm_product_state", + store=True, + tracking=True, + ) + plm_product_ids = fields.One2many( + "product.plm", "log_id", string="Imported PLM Products" + ) + notification_sent = fields.Boolean(copy=False) + + @api.depends("plm_product_ids.state", "plm_product_ids.solved") + def _compute_plm_product_state(self): + for rec in self: + # processed_init = rec.processed + if not rec.plm_product_ids: + rec.plm_product_state = "na" + continue + if all( + plm_prod.state == "done" or plm_prod.solved + for plm_prod in rec.plm_product_ids + ): + rec.plm_product_state = "done" + elif any( + plm_prod.state == "failed" and not plm_prod.solved + for plm_prod in rec.plm_product_ids + ): + rec.plm_product_state = "failed" + else: + rec.plm_product_state = "draft" + + @api.model + def _get_state_description(self, state_field, state_val, rec=None): + if not rec: + rec = self + # Use list comprehension to filter out the description + return next( + ( + desc + for value, desc in rec._fields[state_field].selection + if value == state_val + ), + None, + ) + + @api.model + def _get_plm_notification_domain(self): + return [("notification_sent", "=", False)] + + @api.model + def _send_plm_import_notification(self): + domain = self._get_plm_notification_domain() + log_recs = self.search(domain) + for rec in log_recs: + company = rec.company_id + notified_groups = company.plm_notif_group_ids + notified_partners = notified_groups.users.partner_id + if not notified_partners: + return + rec.message_subscribe(partner_ids=notified_partners.ids) + state_desc = rec._get_state_description( + "plm_product_state", rec.plm_product_state + ) + subject = state_desc + _( + " [Odoo] Product IF Notification: %s", rec.file_name + ) + body = _( + f"

Import File: {rec.file_name}

" + f"

Status: {state_desc}

", + ) + if company.plm_notif_body: + body += f"

{company.plm_notif_body}

" + style = "border: 1px solid black; padding: 5px;" + table = f""" + + + + + + + """ + for plm_prod in rec.plm_product_ids: + row_state_desc = rec._get_state_description( + "state", plm_prod.state, plm_prod + ) + row = f""" + + + + + + """ + table += row + table += "
PLM P/NStateMessage
{plm_prod.part_number}{row_state_desc}{plm_prod.error_message or ""}
" + body += table + rec.message_post( + subject=subject, body=body, subtype_xmlid="mail.mt_comment" + ) + rec.write({"notification_sent": True}) diff --git a/product_plm_import/models/plm_item_type.py b/product_plm_import/models/plm_item_type.py new file mode 100644 index 00000000..f27a12a5 --- /dev/null +++ b/product_plm_import/models/plm_item_type.py @@ -0,0 +1,14 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PlmItemType(models.Model): + _name = "plm.item.type" + _description = "PLM Item Type" + _order = "name" + + name = fields.Char(required=True) + company_id = fields.Many2one("res.company") + active = fields.Boolean(default=True) diff --git a/product_plm_import/models/plm_procure_flag.py b/product_plm_import/models/plm_procure_flag.py new file mode 100644 index 00000000..d88589b8 --- /dev/null +++ b/product_plm_import/models/plm_procure_flag.py @@ -0,0 +1,14 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PlmProcureFlag(models.Model): + _name = "plm.procure.flag" + _description = "PLM Procure Flag" + _order = "name" + + name = fields.Char(required=True) + company_id = fields.Many2one("res.company") + active = fields.Boolean(default=True) diff --git a/product_plm_import/models/plm_product_mapping.py b/product_plm_import/models/plm_product_mapping.py new file mode 100644 index 00000000..56530b9c --- /dev/null +++ b/product_plm_import/models/plm_product_mapping.py @@ -0,0 +1,87 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import fnmatch + +from odoo import api, fields, models + + +class PlmProductMapping(models.Model): + _name = "plm.product.mapping" + _description = "PLM-Product Mapping" + _order = "item_type_id" + _rec_name = "item_type_id" + + item_type_id = fields.Many2one("plm.item.type", string="Item Type", required=True) + category_ids = fields.Many2many("plm.category", string="Categories") + procure_flag_ids = fields.Many2many("plm.procure.flag", string="Procure Flags") + product_type = fields.Selection( + selection=lambda self: self.env["product.template"] + ._fields["detailed_type"] + .selection, + required=True, + ) + product_categ_id = fields.Many2one( + "product.category", + string="Product Category", + required=True, + ) + route_ids = fields.Many2many("stock.route", string="Routes") + tracking = fields.Selection( + selection=lambda self: self.env["product.template"] + ._fields["tracking"] + .selection, + default="none", + required=True, + ) + auto_create_lot = fields.Boolean() + lot_sequence_padding = fields.Integer() + lot_sequence_prefix = fields.Char() + default_active = fields.Boolean( + help="Default value for active field of the created product." + ) + company_id = fields.Many2one("res.company") + active = fields.Boolean(default=True) + + @api.onchange("product_type") + def onchange_product_type(self): + if self.product_type != "product": + self.tracking = "none" + self.auto_create_lot = False + self.lot_sequence_padding = False + self.lot_sequence_prefix = False + + @api.onchange("tracking") + def onchange_tracking(self): + if self.tracking == "none": + self.auto_create_lot = False + + @api.onchange("auto_create_lot") + def onchange_auto_create_lot(self): + if not self.auto_create_lot: + self.lot_sequence_padding = False + self.lot_sequence_prefix = False + + def _get_score(self, plm): + """Gives the score of the mapping record with field values of self.""" + self.ensure_one() + score = 0.0 + if plm.category: + category_names = self.category_ids.mapped("name") + if category_names: + if plm.category in category_names: + score += 1.0 + # Wildcard matching with `*`/`?` + elif any( + fnmatch.fnmatch(plm.category, pattern) for pattern in category_names + ): + score += 0.5 + else: + score -= 1.0 + if plm.procure_flag: + procure_flag_names = self.procure_flag_ids.mapped("name") + if plm.procure_flag in procure_flag_names: + score += 1 + elif procure_flag_names and plm.procure_flag not in procure_flag_names: + score -= 1 + return score diff --git a/product_plm_import/models/product_plm.py b/product_plm_import/models/product_plm.py new file mode 100644 index 00000000..7a6d1ea6 --- /dev/null +++ b/product_plm_import/models/product_plm.py @@ -0,0 +1,141 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class ProductPlm(models.Model): + _name = "product.plm" + _description = "Product PLM" + _order = "id desc" + + part_number = fields.Char() + name = fields.Char() + esc_code = fields.Char("ESC ID") + procure_flag = fields.Char("Procure flag") + item_type = fields.Char() + category = fields.Char() + uom = fields.Char("Unit of Material") + description = fields.Text() + spec = fields.Char() + drawing = fields.Char("Drawing No.") + generic_name = fields.Char() + company_id = fields.Many2one("res.company", required=True) + error_message = fields.Text() + state = fields.Selection( + selection=[("draft", "Draft"), ("done", "Done"), ("failed", "Failed")], + default="draft", + string="Status", + ) + product_id = fields.Many2one("product.product", "Product") + solved = fields.Boolean() + log_id = fields.Many2one("plm.import.log", string="Log", copy=False) + row_no = fields.Integer("Row No.", copy=False) + mapping_id = fields.Many2one("plm.product.mapping", string="Mapping") + + @api.constrains("solved", "state") + def _check_solved(self): + for record in self: + if record.solved and record.state == "done": + raise UserError( + _("You cannot set a record as solved and done at the same time.") + ) + + def _get_plm_product_mapping(self): + best_score = -1 + matched_map = self.env["plm.product.mapping"] + mappings = self.env["plm.product.mapping"].search( + [("item_type_id.name", "=", self.item_type)] + ) + for mapping in mappings: + score_mapping = mapping._get_score(self) + if score_mapping > best_score: + matched_map = mapping + best_score = score_mapping + return matched_map + + def _get_description_purchase(self): + self.ensure_one() + return " / ".join([s for s in [self.description, self.drawing, self.spec] if s]) + + def _get_uom(self): + self.ensure_one() + uom = False + if self.uom: + uom = ( + self.env["uom.uom"] + .with_context(lang="en_US") + .search([("name", "=", self.uom)], limit=1) + ) + if not uom: + uom = self.env.ref("uom.product_uom_unit") + return uom + + def _create_product(self): + self.ensure_one() + product = self.env["product.product"] + description_purchase = self._get_description_purchase() + uom = self._get_uom() + mapping = self.mapping_id + vals = { + "default_code": self.part_number, + "name": self.name, + "alt_code": self.esc_code, + "detailed_type": mapping.product_type, + "categ_id": mapping.product_categ_id.id, + "uom_id": uom.id, + "uom_po_id": uom.id, + "description": self.description, + "description_purchase": description_purchase, + "route_ids": [(6, 0, mapping.route_ids.ids)], + "tracking": mapping.tracking, + "auto_create_lot": mapping.auto_create_lot, + "is_via_plm": True, + } + try: + product = self.env["product.product"].create(vals) + except Exception as e: + _logger.error( + "ProductPlm._create_product - failed to create product: %s", str(e) + ) + return product + + @api.model + def _get_create_products_domain(self): + return [("state", "=", "draft"), ("solved", "=", False)] + + @api.model + def create_products(self, batch_size=30): + domain = self._get_create_products_domain() + plm_recs = self.search(domain, limit=batch_size) + for plm_rec in plm_recs: + if plm_rec.state != "draft" or plm_rec.error_message or plm_rec.solved: + continue + product = plm_rec._create_product() + if not product: + plm_rec.write( + { + "error_message": _("Failed to create product."), + "state": "failed", + } + ) + continue + mapping = plm_rec.mapping_id + if mapping.lot_sequence_padding: + product.lot_sequence_id.padding = mapping.lot_sequence_padding + if mapping.lot_sequence_prefix: + product.lot_sequence_id.prefix = mapping.lot_sequence_prefix + product.product_tmpl_id.active = mapping.default_active + plm_rec.write({"state": "done", "product_id": product.id}) + # This step fails with CasheMiss error in case product creation in + # _create_product() fails with an exception. + plm_recs_remain = self.search(domain) + if plm_recs_remain: + self.env.ref( + "product_plm_import.ir_cron_create_products_for_plm_import" + )._trigger() diff --git a/product_plm_import/models/product_template.py b/product_plm_import/models/product_template.py new file mode 100644 index 00000000..bda3272c --- /dev/null +++ b/product_plm_import/models/product_template.py @@ -0,0 +1,10 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_via_plm = fields.Boolean(readonly=True) diff --git a/product_plm_import/models/res_company.py b/product_plm_import/models/res_company.py new file mode 100644 index 00000000..3d38f358 --- /dev/null +++ b/product_plm_import/models/res_company.py @@ -0,0 +1,22 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + plm_path = fields.Char( + "PLM Path", + placeholder="e.g. /mnt/plmprod/CSV", + help="Path to PLM CSV files. It can end with a slash (/) or without - both are " + "acceptable.", + ) + plm_last_import_date = fields.Datetime( + "PLM Last Import Date", + help="This date is updated every time PLM files for import are searched in PLM " + "Path, and is used as a threshold for the next search.", + ) + plm_notif_body = fields.Html("PLM Notification Body", translate=True) + plm_notif_group_ids = fields.Many2many("res.groups", string="PLM Notified Groups") diff --git a/product_plm_import/readme/CONFIGURE.rst b/product_plm_import/readme/CONFIGURE.rst new file mode 100644 index 00000000..22906149 --- /dev/null +++ b/product_plm_import/readme/CONFIGURE.rst @@ -0,0 +1,5 @@ +Update fields in the company (in the 'PLM I/F' tab): + +- PLM Path: the absolute path to the PLM directory to fetch the files from. +- PLM Notification Body: the text will be included in the notification email body. +- PLM Notified Groups: assign groups to notify when a new file is fetched from the PLM. diff --git a/product_plm_import/readme/DESCRIPTION.rst b/product_plm_import/readme/DESCRIPTION.rst new file mode 100644 index 00000000..ee28571d --- /dev/null +++ b/product_plm_import/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module adds a CSV import function to create new products based on the data received +from the PLM system. + +This module depends on base_data_import module. diff --git a/product_plm_import/readme/USAGE.rst b/product_plm_import/readme/USAGE.rst new file mode 100644 index 00000000..69dba38d --- /dev/null +++ b/product_plm_import/readme/USAGE.rst @@ -0,0 +1,16 @@ +There are three ir.cron records added by this module: + +#. PLM: Import PLM Products + The main cron job that imports PLM product records into Odoo in a periodical manner. +#. PLM: Create products based on imported PLM records + A job that creates products in Odoo based on the imported PLM records. Triggerd by + the main job. +#. PLM: Send email notification on PLM data import + A job that sends email notifications to the relevant users. Triggerd by the main job. + Notification email is designed to be sent only once per the log record. + +Alternatively, users can import PLM product records manually via 'Product PLM Import' +wizard, which also triggers the last two jobs. + +The status of an import log record becomes 'Done' when a product is successfully created +or marked as 'Solved' for all the imported records. diff --git a/product_plm_import/security/ir.model.access.csv b/product_plm_import/security/ir.model.access.csv new file mode 100644 index 00000000..2276d1f6 --- /dev/null +++ b/product_plm_import/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_plm_import_log_user,access.plm.import.log.user,model_plm_import_log,base.group_user,1,1,1,0 +access_product_plm_import_user,access_product_plm_import_user,model_product_plm_import,base.group_user,1,1,1,0 +access_product_plm_user,access_product_plm_user,model_product_plm,base.group_user,1,0,0,0 +access_product_plm_purchase_manager,access_product_plm_purchase_manager,model_product_plm,purchase.group_purchase_manager,1,1,1,1 +access_plm_item_type_user,access_plm_item_type_user,model_plm_item_type,base.group_user,1,0,0,0 +access_plm_item_type_manager,access_plm_item_type_manager,model_plm_item_type,base.group_system,1,1,1,1 +access_plm_category_user,access_plm_category_user,model_plm_category,base.group_user,1,0,0,0 +access_plm_category_manager,access_plm_category_manager,model_plm_category,base.group_system,1,1,1,1 +access_plm_procure_flag_user,access_plm_procure_flag_user,model_plm_procure_flag,base.group_user,1,0,0,0 +access_plm_procure_flag_manager,access_plm_procure_flag_manager,model_plm_procure_flag,base.group_system,1,1,1,1 +access_plm_product_mapping_user,access_plm_product_mapping_user,model_plm_product_mapping,base.group_user,1,0,0,0 +access_plm_product_mapping_manager,access_plm_product_mapping_manager,model_plm_product_mapping,base.group_system,1,1,1,1 diff --git a/product_plm_import/security/security.xml b/product_plm_import/security/security.xml new file mode 100644 index 00000000..95a35f47 --- /dev/null +++ b/product_plm_import/security/security.xml @@ -0,0 +1,17 @@ + + + + Product PLM multi-company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + PLM Item Type multi-company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + diff --git a/product_plm_import/static/description/index.html b/product_plm_import/static/description/index.html new file mode 100644 index 00000000..ad983dd0 --- /dev/null +++ b/product_plm_import/static/description/index.html @@ -0,0 +1,438 @@ + + + + + + +Product PLM Import + + + +
+

Product PLM Import

+ + +

Beta License: AGPL-3 qrtl/qrtl-custom

+

This module adds a CSV import function to create new products based on the data received +from the PLM system.

+

This module depends on base_data_import module.

+

Table of contents

+ +
+

Configuration

+

Update fields in the company (in the ‘PLM I/F’ tab):

+
    +
  • PLM Path: the absolute path to the PLM directory to fetch the files from.
  • +
  • PLM Notification Body: the text will be included in the notification email body.
  • +
  • PLM Notified Groups: assign groups to notify when a new file is fetched from the PLM.
  • +
+
+
+

Usage

+

There are three ir.cron records added by this module:

+
    +
  1. PLM: Import PLM Products +The main cron job that imports PLM product records into Odoo in a periodical manner.
  2. +
  3. PLM: Create products based on imported PLM records +A job that creates products in Odoo based on the imported PLM records. Triggerd by +the main job.
  4. +
  5. PLM: Send email notification on PLM data import +A job that sends email notifications to the relevant users. Triggerd by the main job. +Notification email is designed to be sent only once per the log record.
  6. +
+

Alternatively, users can import PLM product records manually via ‘Product PLM Import’ +wizard, which also triggers the last two jobs.

+

The status of an import log record becomes ‘Done’ when a product is successfully created +or marked as ‘Solved’ for all the imported records.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile Limited
  • +
+
+
+

Maintainers

+

This module is part of the qrtl/qrtl-custom project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/product_plm_import/views/data_import_log_views.xml b/product_plm_import/views/data_import_log_views.xml new file mode 100644 index 00000000..004bc185 --- /dev/null +++ b/product_plm_import/views/data_import_log_views.xml @@ -0,0 +1,138 @@ + + + + plm.import.log.form + plm.import.log + primary + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + plm.import.log.tree + plm.import.log + primary + + + + plm_product_state=='failed' + plm_product_state=='done' + + + 1 + + + + + + + + plm.import.log.select + plm.import.log + primary + + + + 1 + + + + + + 1 + + + + + + + + Product PLM Import Log + plm.import.log + tree,form + + + + + diff --git a/product_plm_import/views/plm_category_views.xml b/product_plm_import/views/plm_category_views.xml new file mode 100644 index 00000000..afefe54b --- /dev/null +++ b/product_plm_import/views/plm_category_views.xml @@ -0,0 +1,31 @@ + + + + view.plm.category.tree + plm.category + + + + + + + + + PLM Categories + plm.category + tree + [] + {} + + + diff --git a/product_plm_import/views/plm_item_type_views.xml b/product_plm_import/views/plm_item_type_views.xml new file mode 100644 index 00000000..9f082aa3 --- /dev/null +++ b/product_plm_import/views/plm_item_type_views.xml @@ -0,0 +1,31 @@ + + + + view.plm.item.type.tree + plm.item.type + + + + + + + + + PLM Item Types + plm.item.type + tree + [] + {} + + + diff --git a/product_plm_import/views/plm_procure_flag_views.xml b/product_plm_import/views/plm_procure_flag_views.xml new file mode 100644 index 00000000..44e2558a --- /dev/null +++ b/product_plm_import/views/plm_procure_flag_views.xml @@ -0,0 +1,31 @@ + + + + view.plm.procure.flag.tree + plm.procure.flag + + + + + + + + + PLM Procure Flags + plm.procure.flag + tree + [] + {} + + + diff --git a/product_plm_import/views/plm_product_mapping_views.xml b/product_plm_import/views/plm_product_mapping_views.xml new file mode 100644 index 00000000..139063bc --- /dev/null +++ b/product_plm_import/views/plm_product_mapping_views.xml @@ -0,0 +1,100 @@ + + + + view.plm.product.mapping.tree + plm.product.mapping + + + + + + + + + + + + + + + + + + + view.plm.product.mapping.form + plm.product.mapping + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + PLM-Product Mapping + plm.product.mapping + tree + [] + {} + + +
diff --git a/product_plm_import/views/product_plm_views.xml b/product_plm_import/views/product_plm_views.xml new file mode 100644 index 00000000..9ca85ed1 --- /dev/null +++ b/product_plm_import/views/product_plm_views.xml @@ -0,0 +1,108 @@ + + + + view.product.plm.tree + product.plm + + + + + + + + + + + + + + + + + + + + + + + + + + view.product.plm.form + product.plm + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + PLM Products + product.plm + tree + + [] + {} + + + +
diff --git a/product_plm_import/views/product_template_views.xml b/product_plm_import/views/product_template_views.xml new file mode 100644 index 00000000..ae45e4eb --- /dev/null +++ b/product_plm_import/views/product_template_views.xml @@ -0,0 +1,51 @@ + + + + product.template.common.form + product.template + + + + + + + + + + + + + product.template.search + product.template + + + + + + + + + + Product Templates (WIP) + product.template + tree,form + [] + + {"search_default_is_via_plm": 1, "search_default_inactive": 1} + + + + diff --git a/product_plm_import/views/res_company_views.xml b/product_plm_import/views/res_company_views.xml new file mode 100644 index 00000000..848170b9 --- /dev/null +++ b/product_plm_import/views/res_company_views.xml @@ -0,0 +1,20 @@ + + + + res.company.form + res.company + + + + + + + + + + + + + + + diff --git a/product_plm_import/wizards/__init__.py b/product_plm_import/wizards/__init__.py new file mode 100644 index 00000000..8cab664d --- /dev/null +++ b/product_plm_import/wizards/__init__.py @@ -0,0 +1 @@ +from . import product_plm_import diff --git a/product_plm_import/wizards/product_plm_import.py b/product_plm_import/wizards/product_plm_import.py new file mode 100644 index 00000000..77e70c34 --- /dev/null +++ b/product_plm_import/wizards/product_plm_import.py @@ -0,0 +1,184 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +import os +from base64 import b64encode +from datetime import datetime, timedelta + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +FIELD_KEYS = {0: "field", 1: "label", 2: "field_type", 3: "required"} + +# Prepare values corresponding with the keys +FIELD_VALS = [ + ["part_number", "Part Number", "char", True], + ["name", "Name", "char", True], + ["esc_code", "ESC ID", "char", False], + ["procure_flag", "Procure flag", "char", True], + ["item_type", "Item Type", "char", True], + ["category", "Category", "char", True], + ["uom", "Unit of Material", "char", True], + ["description", "Description", "char", True], + ["spec", "Spec", "char", False], + ["drawing", "Drawing No", "char", False], + ["generic_name", "Generic Name", "char", False], +] + +FIELD_TO_UNESCAPE = [ + "name", + "category", + "description", + "spec", + "drawing", + "generic_name", +] + + +class ProductPlmImport(models.TransientModel): + _name = "product.plm.import" + _inherit = "data.import" + _description = "Product PLM Import" + + import_log_id = fields.Many2one("plm.import.log") + + @api.model + def _get_product_domain(self, part_number): + return [ + "&", + "|", + ("default_code", "=", part_number), + ("alt_code", "=", part_number), + "&", + "|", + ("company_id", "=", self.env.company.id), + ("company_id", "=", False), + "|", + ("active", "=", True), + ("active", "=", False), + ] + + @api.model + def _update_vals_list(self, row_dict, error_list, vals_list): + company = self.env.company + part_number = row_dict.get("part_number") + product_domain = self._get_product_domain(part_number) + product = self.env["product.product"].search(product_domain) + if product: + error_list.append(_("There is already a product for %s.", part_number)) + # We update vals_list regardless of whether there is an error or not + row_dict["company_id"] = company.id + row_dict["log_id"] = self.import_log_id.id + # TODO: Find a better way to handle unescaping. + row_dict = self._unescape_field_vals(row_dict, FIELD_TO_UNESCAPE) + vals_list.append(row_dict) + + def import_product_plm(self, attachment=None): + # In case this method is called via a cron method. + if not self: + self = self.create({}) + self.ensure_one() + # To send notification emails in English + self = self.with_context(lang="en_US") + self.import_log_id = self._create_import_log("product.plm", "plm.import.log") + if not self.import_log_id.input_file and attachment: + self.import_log_id.input_file = attachment + field_defs = self._get_field_defs(FIELD_KEYS, FIELD_VALS) + sheet_fields, csv_iterator = self._load_import_file( + field_defs, ["cp932", "utf-8-sig", "utf-8"] + ) + vals_list = [] + # csv_iterator.line_num gets incremented by more than 1 when there is a text + # field with a line break. Therefore, we need to use our own counter. + # The counter will effectively start with 2 which is the first row after the + # header. + row_no = 1 + for row in csv_iterator: + row_no += 1 + row_dict, error_list = self._check_field_vals(field_defs, row, sheet_fields) + row_dict["row_no"] = row_no + # Here is the module specific logic + if row_dict: + self._update_vals_list(row_dict, error_list, vals_list) + if error_list: + # We are not using date.import.error in this module. + row_dict["error_message"] = "\n".join(error_list) + row_dict["state"] = "failed" + plm_recs = self.env["product.plm"].create(vals_list) + for plm_rec in plm_recs: + plm_rec.mapping_id = plm_rec._get_plm_product_mapping() + if not plm_rec.mapping_id: + plm_rec.write( + { + "error_message": _("No PLM-product mapping record found."), + "state": "failed", + } + ) + self.env.ref( + "product_plm_import.ir_cron_create_products_for_plm_import" + )._trigger() + self.env.ref( + "product_plm_import.ir_cron_send_plm_import_notification" + )._trigger(fields.Datetime.now() + timedelta(minutes=1)) + if attachment: + return True + view_id = self.env.ref("product_plm_import.view_product_plm_import_log_form").id + return self._action_open_import_log( + self.import_log_id, view_id, "plm.import.log" + ) + + def _get_new_plm_files(self, plm_path): + company = self.env.company + threshold_date = company.plm_last_import_date or ( + fields.Datetime.now() - timedelta(days=1) + ) + company.plm_last_import_date = fields.Datetime.now() + all_files = os.listdir(plm_path) + # Filter the CSV files that are newer than the threshold date + return [ + file + for file in all_files + if file.endswith(".csv") + and datetime.fromtimestamp(os.path.getmtime(os.path.join(plm_path, file))) + > threshold_date + ] + + def _create_plm_attachments(self, new_files, plm_path): + attachments = self.env["ir.attachment"] + for file_name in new_files: + plm_file_path = os.path.join(plm_path, file_name) + try: + with open(plm_file_path, "rb") as file: + file_content = file.read() + encoded_content = b64encode(file_content) + attachment = self.env["ir.attachment"].create( + { + "name": file_name, + "type": "binary", + "datas": encoded_content, + "mimetype": "text/csv", + } + ) + attachments += attachment + except Exception as e: + _logger.error( + "ProductPlmImport._create_plm_attachments - failed to read and " + "encode the file: %s", + str(e), + ) + return attachments + + @api.model + def import_product_from_plm_path(self): + plm_path = self.env.company.plm_path + new_files = self._get_new_plm_files(plm_path) + # Avoid importing files that are already imported (identify those by name) + attachments = self.env["ir.attachment"].search([("name", "in", new_files)]) + for attachment in attachments: + new_files.remove(attachment.name) + new_attachments = self._create_plm_attachments(new_files, plm_path) + for new_attachment in new_attachments: + self.import_product_plm(new_attachment) diff --git a/product_plm_import/wizards/product_plm_import_views.xml b/product_plm_import/wizards/product_plm_import_views.xml new file mode 100644 index 00000000..c764f688 --- /dev/null +++ b/product_plm_import/wizards/product_plm_import_views.xml @@ -0,0 +1,34 @@ + + + + view.product.plm.import + product.plm.import + + + +