From dd1e955afd6d5797f6b01ec214b46f656ef3387f Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Wed, 12 Feb 2025 15:56:00 +0800 Subject: [PATCH 01/22] [ADD] spp_area_base: Initial commit. --- spp_area_base/README.rst | 165 +++++ spp_area_base/__init__.py | 6 + spp_area_base/__manifest__.py | 38 ++ spp_area_base/data/area_kind_data.xml | 5 + spp_area_base/data/queue_job_channel.xml | 6 + spp_area_base/i18n/ar.po | 547 +++++++++++++++ spp_area_base/i18n/ckb.po | 543 +++++++++++++++ spp_area_base/i18n/fr.po | 562 ++++++++++++++++ spp_area_base/i18n/spp_area.pot | 591 +++++++++++++++++ spp_area_base/models/__init__.py | 5 + spp_area_base/models/area.py | 303 +++++++++ spp_area_base/models/area_import.py | 620 ++++++++++++++++++ spp_area_base/pyproject.toml | 3 + spp_area_base/readme/DESCRIPTION.md | 42 ++ spp_area_base/security/ir.model.access.csv | 5 + spp_area_base/static/description/icon.png | Bin 0 -> 12567 bytes .../static/description/icon_area.png | Bin 0 -> 6338 bytes spp_area_base/static/description/index.html | 496 ++++++++++++++ .../static/src/img/icons/contacts.png | Bin 0 -> 591 bytes spp_area_base/tests/__init__.py | 6 + spp_area_base/tests/common.py | 51 ++ .../irq_adminboundaries_tabulardata.xlsx | Bin 0 -> 42057 bytes .../pse_adminboundaries_tabulardata.xlsx | Bin 0 -> 12175 bytes spp_area_base/tests/test_area.py | 33 + spp_area_base/tests/test_area_import.py | 140 ++++ spp_area_base/tests/test_area_import_raw.py | 75 +++ spp_area_base/views/area.xml | 196 ++++++ spp_area_base/views/area_import_views.xml | 258 ++++++++ spp_area_base/views/area_kind.xml | 58 ++ 29 files changed, 4754 insertions(+) create mode 100644 spp_area_base/README.rst create mode 100644 spp_area_base/__init__.py create mode 100644 spp_area_base/__manifest__.py create mode 100644 spp_area_base/data/area_kind_data.xml create mode 100644 spp_area_base/data/queue_job_channel.xml create mode 100644 spp_area_base/i18n/ar.po create mode 100644 spp_area_base/i18n/ckb.po create mode 100644 spp_area_base/i18n/fr.po create mode 100644 spp_area_base/i18n/spp_area.pot create mode 100644 spp_area_base/models/__init__.py create mode 100644 spp_area_base/models/area.py create mode 100644 spp_area_base/models/area_import.py create mode 100644 spp_area_base/pyproject.toml create mode 100644 spp_area_base/readme/DESCRIPTION.md create mode 100644 spp_area_base/security/ir.model.access.csv create mode 100644 spp_area_base/static/description/icon.png create mode 100644 spp_area_base/static/description/icon_area.png create mode 100644 spp_area_base/static/description/index.html create mode 100644 spp_area_base/static/src/img/icons/contacts.png create mode 100644 spp_area_base/tests/__init__.py create mode 100644 spp_area_base/tests/common.py create mode 100644 spp_area_base/tests/irq_adminboundaries_tabulardata.xlsx create mode 100644 spp_area_base/tests/pse_adminboundaries_tabulardata.xlsx create mode 100644 spp_area_base/tests/test_area.py create mode 100644 spp_area_base/tests/test_area_import.py create mode 100644 spp_area_base/tests/test_area_import_raw.py create mode 100644 spp_area_base/views/area.xml create mode 100644 spp_area_base/views/area_import_views.xml create mode 100644 spp_area_base/views/area_kind.xml diff --git a/spp_area_base/README.rst b/spp_area_base/README.rst new file mode 100644 index 000000000..aaae5ca5b --- /dev/null +++ b/spp_area_base/README.rst @@ -0,0 +1,165 @@ +======================= +OpenSPP Area Management +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e6e98dfbc0dfec92d49f42a8d99cd1c4e2fd8edd889a3165a76df9d78c301196 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |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-OpenSPP%2Fopenspp--modules-lightgray.png?logo=github + :target: https://github.com/OpenSPP/openspp-modules/tree/17.0/spp_area + :alt: OpenSPP/openspp-modules + +|badge1| |badge2| |badge3| + +OpenSPP Area +============ + +This document describes the **OpenSPP Area** module, which extends the +OpenSPP framework by providing features to manage and organize +geographical areas within the system. It integrates with the core +registry modules to allow associating registrants and other data with +specific locations. + +Purpose +------- + +The **OpenSPP Area** module is designed to: + +- **Define and Structure Geographical Areas**: Establish a hierarchical + structure for representing administrative regions, from the highest + level (e.g., country) down to the most granular level (e.g., + village). +- **Manage Area Information**: Store key details about each area, + including its name, code, alternate names, geographical size, and + parent-child relationships within the hierarchy. +- **Associate Registrants with Areas**: Enable the linking of + individual and group registrants to specific areas, facilitating + location-based targeting, analysis, and program implementation. + +Dependencies and Integration +---------------------------- + +1. **G2P Registry: Base + (**\ `g2p_registry_base `__\ **)**: The Area + module utilizes the **Districts (g2p.district)** feature from the + **G2P Registry: Base** module as a foundation. It extends this + concept to create a more comprehensive and flexible system for + managing area data. + +2. **G2P Registry: Individual + (**\ `g2p_registry_individual `__\ **)**: + Integrates with the Individual module by adding a dedicated "Area" + field to the individual registrant form. This field allows users to + assign a specific area to each individual, linking registrant data to + geographical locations. + +3. **G2P Registry: Group + (**\ `g2p_registry_group `__\ **)**: Similar to + the Individual module integration, this module incorporates an "Area" + field into the group registrant form, enabling the association of + groups with specific areas. + +4. **Queue Job (**\ `queue_job `__\ **)**: Leverages the + **Queue Job** module for background processing of large data imports, + improving performance and user experience. This is particularly + beneficial when importing extensive area hierarchies from external + sources. + +Additional Functionality +------------------------ + +- **Hierarchical Area Structure (**\ `spp.area `__\ **)**: + + - Introduces a dedicated model for managing areas, allowing for the + creation of multi-level administrative boundaries with + parent-child relationships. + - Computes and displays the complete area path (e.g., "Country > + Province > District > Village") to provide clear context within + the hierarchy. + - Enforces unique codes for each area to ensure proper + identification and prevent duplicates. + +- **Area Types (**\ `spp.area.kind `__\ **)**: + + - Includes a model for defining and managing different types of + areas (e.g., administrative regions, ecological zones, project + implementation areas). + - Allows for the creation of a hierarchy of area types, providing + further categorization and flexibility. + +- **Area Import Functionality**: + + - Provides tools for importing area data in bulk from Excel files, + streamlining the process of populating the area hierarchy. + - Implements validation rules during the import process to ensure + data integrity, such as checking for required fields, data types, + and hierarchical consistency. + - Utilizes the Queue Job module to perform data validation and + import operations in the background, preventing performance issues + and providing a smoother user experience. + - Ability to localize the name of the imported area. + +Conclusion +---------- + +The **OpenSPP Area** module enhances the OpenSPP platform by providing a +robust and flexible system for managing geographical areas and linking +them to registrant data. Its integration with the core registry modules +ensures that location information is seamlessly incorporated into the +overall system, supporting location-based targeting, analysis, and +program management for social protection programs and farmer registries. + +**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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-reichie020212| image:: https://github.com/reichie020212.png?size=40px + :target: https://github.com/reichie020212 + :alt: reichie020212 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-reichie020212| + +This module is part of the `OpenSPP/openspp-modules `_ project on GitHub. + +You are welcome to contribute. diff --git a/spp_area_base/__init__.py b/spp_area_base/__init__.py new file mode 100644 index 000000000..23b379697 --- /dev/null +++ b/spp_area_base/__init__.py @@ -0,0 +1,6 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + + +from . import models + +# from . import controllers diff --git a/spp_area_base/__manifest__.py b/spp_area_base/__manifest__.py new file mode 100644 index 000000000..fa5fee4cc --- /dev/null +++ b/spp_area_base/__manifest__.py @@ -0,0 +1,38 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + + +{ + "name": "OpenSPP Area Management (Base)", + "summary": "This module enables management of geographical areas, linking them to registrants for targeted interventions and analysis in social protection programs.", + "category": "OpenSPP", + "version": "17.0.1.3.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/openspp-modules", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "reichie020212"], + "depends": [ + "base", + "queue_job", + ], + "external_dependencies": { + "python": [ + "xlrd", + ] + }, + "data": [ + "data/area_kind_data.xml", + "data/queue_job_channel.xml", + "security/ir.model.access.csv", + "views/area.xml", + "views/area_kind.xml", + "views/area_import_views.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": True, + "installable": True, + "auto_install": False, +} diff --git a/spp_area_base/data/area_kind_data.xml b/spp_area_base/data/area_kind_data.xml new file mode 100644 index 000000000..897a528a6 --- /dev/null +++ b/spp_area_base/data/area_kind_data.xml @@ -0,0 +1,5 @@ + + + Admin Area + + diff --git a/spp_area_base/data/queue_job_channel.xml b/spp_area_base/data/queue_job_channel.xml new file mode 100644 index 000000000..3ac763ab9 --- /dev/null +++ b/spp_area_base/data/queue_job_channel.xml @@ -0,0 +1,6 @@ + + + area_import + + + diff --git a/spp_area_base/i18n/ar.po b/spp_area_base/i18n/ar.po new file mode 100644 index 000000000..6d5ef05ae --- /dev/null +++ b/spp_area_base/i18n/ar.po @@ -0,0 +1,547 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * spp_area +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-02-24 02:22+0000\n" +"Last-Translator: Ammar ALBAYATI \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" +"X-Generator: Weblate 4.14\n" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_alt1 +msgid "Admin Alt1" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_alt2 +msgid "Admin Alt2" +msgstr "" + +#. module: spp_area +#: model:spp.area.kind,complete_name:spp_area.admin_area_kind +msgid "Admin Area" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_code +msgid "Admin Code" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_kind +msgid "Admin Kind" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_name +msgid "Admin Name" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_ref +msgid "Admin Ref" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__altnames +msgid "Alternate Name" +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_spparea +#: model:ir.model,name:spp_area.model_spp_area +#: model:ir.model.fields,field_description:spp_area.field_res_partner__area_id +#: model:ir.model.fields,field_description:spp_area.field_res_users__area_id +#: model:ir.ui.menu,name:spp_area.area_main_menu_root +#: model:ir.ui.menu,name:spp_area.menu_spparea +#: model_terms:ir.ui.view,arch_db:spp_area.view_area_groups_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_area_individuals_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Area" +msgstr "المركز" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__excel_file +msgid "Area Excel File" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Area Excel File:" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__area_import_id +#: model:ir.ui.menu,name:spp_area.menu_spparea_import +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Area Import" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import_raw +msgid "Area Import Raw Data" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import_lang +msgid "Area Import Raw Data Languages" +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_spparea_kind +#: model:ir.model,name:spp_area.model_spp_area_kind +#: model:ir.ui.menu,name:spp_area.menu_spparea_kind +#, fuzzy +msgid "Area Kind" +msgstr "المركز" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__area_level +msgid "Area Level" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Area Name..." +msgstr "المركز..." + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__raw_id +msgid "Area Raw ID" +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_view_spparea_import +msgid "Area Upload" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Area already exist!" +msgstr "" + +#. module: spp_area +#: model:ir.ui.menu,name:spp_area.area_main_top_menu +msgid "Areas" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import +msgid "Areas Import Table" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't delete default Area Type" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't delete used Area Type" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't edit default Area Type" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Cancel" +msgstr "الغاء" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__cancelled +msgid "Cancelled" +msgstr "ألغيت" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__child_ids +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Child" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea_kind +msgid "Click the create button to enter the information of the Area Kind." +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea +msgid "Click the create button to enter the information of the Area." +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_view_spparea_import +msgid "Click the create button to upload a new excel file." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__code +msgid "Code" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_kind_tree +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_tree +msgid "Complete Name" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_res_partner +msgid "Contact" +msgstr "اتصال" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea_kind +msgid "Create a new Area Kind!" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea +msgid "Create a new Area!" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__create_uid +msgid "Created by" +msgstr "انشأ من قبل" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__create_date +msgid "Created on" +msgstr "انشأ على" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_res_partner__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__dms_directory_ids +#, fuzzy +msgid "DMS Directories" +msgstr "دليل الوثائق" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "DONE" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_imported +msgid "Date Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_uploaded +msgid "Date Uploaded" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_validated +msgid "Date Validated" +msgstr "تاريخ التحقق" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__display_name +msgid "Display Name" +msgstr "الاسم" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__done +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Done" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__draft_name +#, fuzzy +msgid "Draft Name" +msgstr "المركز..." + +#. module: spp_area +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "ERROR: {}" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__error +msgid "Error" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__name +msgid "File Name" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Group By" +msgstr "مجموعة من" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__id +msgid "ID" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "IMPORTED" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__iso_code +msgid "ISO Code" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Import" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__imported +msgid "Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__import_id +msgid "Imported by" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__kind +msgid "Kind" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__lang_id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__lang_ids +msgid "Languages" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind____last_update +msgid "Last Modified on" +msgstr "تاريخ آخر تعديل" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__write_uid +msgid "Last Updated by" +msgstr "آخر تحديث بواسطة" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__write_date +msgid "Last Updated on" +msgstr "آخر تحديث في" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__level +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__level +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +msgid "Level" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "NEW" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__complete_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area__name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__complete_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__name +msgid "Name" +msgstr "الإسم" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Name:" +msgstr "اسم:" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__new +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__new +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "New" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__parent_id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__parent_id +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +msgid "Parent" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__parent_path +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__parent_path +msgid "Parent Path" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Parent:" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__posted +msgid "Posted" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__raw_data_ids +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Raw Data" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__remarks +msgid "Remarks/Errors" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__row_index +msgid "Row Index" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Save to Area" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_kind_filter +msgid "Search Area" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Search Imported File" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Select a Parent..." +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "State" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__state +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__state +msgid "Status" +msgstr "الحالة" + +#. module: spp_area +#: model:ir.model.fields,help:spp_area.field_spp_area__level +msgid "This is the area level for importing" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,help:spp_area.field_spp_area__area_level +msgid "This is the main area level" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__tot_rows_imported +msgid "Total Rows Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__tot_rows_error +msgid "Total Rows with Error" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__name +msgid "Translate Name" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "UPLOADED" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__updated +msgid "Updated" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_view_spparea_import +msgid "Upload an excel file!" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__uploaded +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Uploaded" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__upload_id +msgid "Uploaded by" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__validated +msgid "Validated" +msgstr "تم التحقق" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__validate_id +msgid "Validated by" +msgstr "تم التحقق من قبل" diff --git a/spp_area_base/i18n/ckb.po b/spp_area_base/i18n/ckb.po new file mode 100644 index 000000000..c4bd56a9a --- /dev/null +++ b/spp_area_base/i18n/ckb.po @@ -0,0 +1,543 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * spp_area +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-01-18 14:42+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: Kurdish (Central) \n" +"Language: ckb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14\n" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_alt1 +msgid "Admin Alt1" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_alt2 +msgid "Admin Alt2" +msgstr "" + +#. module: spp_area +#: model:spp.area.kind,complete_name:spp_area.admin_area_kind +msgid "Admin Area" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_code +msgid "Admin Code" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_kind +msgid "Admin Kind" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_name +msgid "Admin Name" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_ref +msgid "Admin Ref" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__altnames +msgid "Alternate Name" +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_spparea +#: model:ir.model,name:spp_area.model_spp_area +#: model:ir.model.fields,field_description:spp_area.field_res_partner__area_id +#: model:ir.model.fields,field_description:spp_area.field_res_users__area_id +#: model:ir.ui.menu,name:spp_area.area_main_menu_root +#: model:ir.ui.menu,name:spp_area.menu_spparea +#: model_terms:ir.ui.view,arch_db:spp_area.view_area_groups_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_area_individuals_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Area" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__excel_file +msgid "Area Excel File" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Area Excel File:" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__area_import_id +#: model:ir.ui.menu,name:spp_area.menu_spparea_import +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Area Import" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import_raw +msgid "Area Import Raw Data" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import_lang +msgid "Area Import Raw Data Languages" +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_spparea_kind +#: model:ir.model,name:spp_area.model_spp_area_kind +#: model:ir.ui.menu,name:spp_area.menu_spparea_kind +msgid "Area Kind" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__area_level +msgid "Area Level" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Area Name..." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__raw_id +msgid "Area Raw ID" +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_view_spparea_import +msgid "Area Upload" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Area already exist!" +msgstr "" + +#. module: spp_area +#: model:ir.ui.menu,name:spp_area.area_main_top_menu +msgid "Areas" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import +msgid "Areas Import Table" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't delete default Area Type" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't delete used Area Type" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't edit default Area Type" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Cancel" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__cancelled +msgid "Cancelled" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__child_ids +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Child" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea_kind +msgid "Click the create button to enter the information of the Area Kind." +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea +msgid "Click the create button to enter the information of the Area." +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_view_spparea_import +msgid "Click the create button to upload a new excel file." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__code +msgid "Code" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_kind_tree +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_tree +msgid "Complete Name" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_res_partner +msgid "Contact" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea_kind +msgid "Create a new Area Kind!" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea +msgid "Create a new Area!" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__create_uid +msgid "Created by" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__create_date +msgid "Created on" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_res_partner__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__dms_directory_ids +msgid "DMS Directories" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "DONE" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_imported +msgid "Date Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_uploaded +msgid "Date Uploaded" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_validated +msgid "Date Validated" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__display_name +msgid "Display Name" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__done +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Done" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__draft_name +msgid "Draft Name" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "ERROR: {}" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__error +msgid "Error" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__name +msgid "File Name" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Group By" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__id +msgid "ID" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "IMPORTED" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__iso_code +msgid "ISO Code" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Import" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__imported +msgid "Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__import_id +msgid "Imported by" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__kind +msgid "Kind" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__lang_id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__lang_ids +msgid "Languages" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind____last_update +msgid "Last Modified on" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__write_date +msgid "Last Updated on" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__level +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__level +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +msgid "Level" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "NEW" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__complete_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area__name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__complete_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__name +msgid "Name" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Name:" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__new +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__new +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "New" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__parent_id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__parent_id +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +msgid "Parent" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__parent_path +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__parent_path +msgid "Parent Path" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Parent:" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__posted +msgid "Posted" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__raw_data_ids +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Raw Data" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__remarks +msgid "Remarks/Errors" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__row_index +msgid "Row Index" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Save to Area" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_kind_filter +msgid "Search Area" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Search Imported File" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Select a Parent..." +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "State" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__state +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__state +msgid "Status" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,help:spp_area.field_spp_area__level +msgid "This is the area level for importing" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,help:spp_area.field_spp_area__area_level +msgid "This is the main area level" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__tot_rows_imported +msgid "Total Rows Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__tot_rows_error +msgid "Total Rows with Error" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__name +msgid "Translate Name" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "UPLOADED" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__updated +msgid "Updated" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_view_spparea_import +msgid "Upload an excel file!" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__uploaded +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Uploaded" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__upload_id +msgid "Uploaded by" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__validated +msgid "Validated" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__validate_id +msgid "Validated by" +msgstr "" diff --git a/spp_area_base/i18n/fr.po b/spp_area_base/i18n/fr.po new file mode 100644 index 000000000..9776df01c --- /dev/null +++ b/spp_area_base/i18n/fr.po @@ -0,0 +1,562 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * spp_area +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-01-18 14:42+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.14\n" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_alt1 +msgid "Admin Alt1" +msgstr "Administrateur Alt1" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_alt2 +msgid "Admin Alt2" +msgstr "Administrateur Alt2" + +#. module: spp_area +#: model:spp.area.kind,complete_name:spp_area.admin_area_kind +msgid "Admin Area" +msgstr "Zone administrative" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_code +msgid "Admin Code" +msgstr "Code de zone" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_kind +msgid "Admin Kind" +msgstr "Type de zone" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_name +msgid "Admin Name" +msgstr "Nom de la zone" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_ref +msgid "Admin Ref" +msgstr "Réf zone" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__altnames +msgid "Alternate Name" +msgstr "Nom alternatif" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_spparea +#: model:ir.model,name:spp_area.model_spp_area +#: model:ir.model.fields,field_description:spp_area.field_res_partner__area_id +#: model:ir.model.fields,field_description:spp_area.field_res_users__area_id +#: model:ir.ui.menu,name:spp_area.area_main_menu_root +#: model:ir.ui.menu,name:spp_area.menu_spparea +#: model_terms:ir.ui.view,arch_db:spp_area.view_area_groups_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_area_individuals_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Area" +msgstr "Zone" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__excel_file +msgid "Area Excel File" +msgstr "Fichier Excel de la Zone" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Area Excel File:" +msgstr "Fichier Excel de la zone :" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__area_import_id +#: model:ir.ui.menu,name:spp_area.menu_spparea_import +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Area Import" +msgstr "Importation de Zone" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import_raw +msgid "Area Import Raw Data" +msgstr "Import de données brutes de la zone" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import_lang +msgid "Area Import Raw Data Languages" +msgstr "Importation de données brutes par zone - Langues" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_spparea_kind +#: model:ir.model,name:spp_area.model_spp_area_kind +#: model:ir.ui.menu,name:spp_area.menu_spparea_kind +msgid "Area Kind" +msgstr "Type de zone" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__area_level +msgid "Area Level" +msgstr "Niveau de zone" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Area Name..." +msgstr "Nom de la zone..." + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__raw_id +msgid "Area Raw ID" +msgstr "ID brut de la zone" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_view_spparea_import +msgid "Area Upload" +msgstr "Téléchargement de zone" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Area already exist!" +msgstr "La zone existe déjà !" + +#. module: spp_area +#: model:ir.ui.menu,name:spp_area.area_main_top_menu +msgid "Areas" +msgstr "Zones" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import +msgid "Areas Import Table" +msgstr "Tableau d'importation des zones" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't delete default Area Type" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't delete used Area Type" +msgstr "" + +#. module: spp_area +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't edit default Area Type" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Cancel" +msgstr "Annuler" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__cancelled +msgid "Cancelled" +msgstr "Annulé" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__child_ids +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Child" +msgstr "Enfant" + +# +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea_kind +msgid "Click the create button to enter the information of the Area Kind." +msgstr "" +"Cliquez sur le bouton créer pour saisir les informations du Type de Zone." + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea +msgid "Click the create button to enter the information of the Area." +msgstr "Cliquez sur le bouton Créer pour saisir les informations de la Zone." + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_view_spparea_import +msgid "Click the create button to upload a new excel file." +msgstr "Cliquez sur le bouton Créer pour télécharger un nouveau fichier Excel." + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__code +msgid "Code" +msgstr "Code" + +# +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_kind_tree +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_tree +msgid "Complete Name" +msgstr "Nom complet" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_res_partner +msgid "Contact" +msgstr "Contact" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea_kind +msgid "Create a new Area Kind!" +msgstr "Créez un nouveau type de zone !" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea +msgid "Create a new Area!" +msgstr "Créez une nouvelle zone !" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__create_date +msgid "Created on" +msgstr "Créé sur" + +# +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_res_partner__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__dms_directory_ids +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__dms_directory_ids +msgid "DMS Directories" +msgstr "Répertoires du DMS" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "DONE" +msgstr "FINI" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_imported +msgid "Date Imported" +msgstr "Date d'importation" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_uploaded +msgid "Date Uploaded" +msgstr "Date de téléchargement" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_validated +msgid "Date Validated" +msgstr "Date de Validation" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__display_name +msgid "Display Name" +msgstr "Afficher un nom" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__done +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Done" +msgstr "Fait" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__draft_name +msgid "Draft Name" +msgstr "Nom du brouillon" + +#. module: spp_area +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "ERROR: {}" +msgstr "ERREUR : {}" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__error +msgid "Error" +msgstr "Erreur" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__name +msgid "File Name" +msgstr "Nom de fichier" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Group By" +msgstr "Groupé par" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__id +msgid "ID" +msgstr "ID" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "IMPORTED" +msgstr "IMPORTÉ" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__iso_code +msgid "ISO Code" +msgstr "Code ISO" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Import" +msgstr "Importer" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__imported +msgid "Imported" +msgstr "Importé" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__import_id +msgid "Imported by" +msgstr "Importé par" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__kind +msgid "Kind" +msgstr "Type" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__lang_id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__lang_ids +msgid "Languages" +msgstr "Langues" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw____last_update +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind____last_update +msgid "Last Modified on" +msgstr "Modification le" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__write_date +msgid "Last Updated on" +msgstr "Mise à jour le" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__level +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__level +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +msgid "Level" +msgstr "Niveau" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "NEW" +msgstr "NOUVEAU" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__complete_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area__name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__complete_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__name +msgid "Name" +msgstr "Nom" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Name:" +msgstr "Nom :" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__new +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__new +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "New" +msgstr "Nouveau" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__parent_id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__parent_id +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +msgid "Parent" +msgstr "Parent" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__parent_path +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__parent_path +msgid "Parent Path" +msgstr "Chemin parent" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Parent:" +msgstr "Parent :" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__posted +msgid "Posted" +msgstr "Posté" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__raw_data_ids +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Raw Data" +msgstr "Données brutes" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__remarks +msgid "Remarks/Errors" +msgstr "Remarques/Erreurs" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__row_index +msgid "Row Index" +msgstr "Index de ligne" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Save to Area" +msgstr "Enregistrer dans la zone" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_kind_filter +msgid "Search Area" +msgstr "Zone de recherche" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Search Imported File" +msgstr "Rechercher un fichier importé" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Select a Parent..." +msgstr "Sélectionnez un parent..." + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "State" +msgstr "État" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__state +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__state +msgid "Status" +msgstr "Statut" + +#. module: spp_area +#: model:ir.model.fields,help:spp_area.field_spp_area__level +#, fuzzy +msgid "This is the area level for importing" +msgstr "Il s'agit du niveau de zone pour l'importation" + +#. module: spp_area +#: model:ir.model.fields,help:spp_area.field_spp_area__area_level +#, fuzzy +msgid "This is the main area level" +msgstr "Il s'agit du niveau principal de la zone" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__tot_rows_imported +msgid "Total Rows Imported" +msgstr "Nombre total de lignes importées" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__tot_rows_error +#, fuzzy +msgid "Total Rows with Error" +msgstr "Nombre total de lignes avec erreur" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_lang__name +msgid "Translate Name" +msgstr "Traduire le nom" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "UPLOADED" +msgstr "TÉLÉCHARGÉ" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__updated +msgid "Updated" +msgstr "Actualisé" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_view_spparea_import +msgid "Upload an excel file!" +msgstr "Téléchargez un fichier excel !" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__uploaded +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Uploaded" +msgstr "Téléchargé" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__upload_id +msgid "Uploaded by" +msgstr "telechargé par" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__validated +msgid "Validated" +msgstr "Validé" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__validate_id +msgid "Validated by" +msgstr "Validé par" + +#, python-format +#~ msgid "Can't delete default Area Kind" +#~ msgstr "Impossible de supprimer le type de zone par défaut" + +#, python-format +#~ msgid "Can't delete used Area Kind" +#~ msgstr "Impossible de supprimer un type de zone utilisé" + +#, python-format +#~ msgid "Can't edit default Area Kind" +#~ msgstr "Impossible de modifier le type de zone par défaut" diff --git a/spp_area_base/i18n/spp_area.pot b/spp_area_base/i18n/spp_area.pot new file mode 100644 index 000000000..0c8d291e0 --- /dev/null +++ b/spp_area_base/i18n/spp_area.pot @@ -0,0 +1,591 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * spp_area +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \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: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Warning: Operation in progress: " +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "AREA_SQKM should be numerical." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_code +msgid "Admin Code" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__admin_name +msgid "Admin Name" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__altnames +msgid "Alternate Name" +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_spparea +#: model:ir.model,name:spp_area.model_spp_area +#: model:ir.model.fields,field_description:spp_area.field_res_partner__area_id +#: model:ir.model.fields,field_description:spp_area.field_res_users__area_id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__area_id +#: model:ir.model.fields,field_description:spp_area.field_spp_attendance_subscriber__area_id +#: model:ir.ui.menu,name:spp_area.area_main_menu_root +#: model:ir.ui.menu,name:spp_area.menu_spparea +#: model_terms:ir.ui.view,arch_db:spp_area.view_area_groups_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_area_individuals_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Area" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__area_sqkm +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__area_sqkm +msgid "Area (sq/km)" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__excel_file +msgid "Area Excel File" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Area Excel File:" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__area_import_id +#: model:ir.ui.menu,name:spp_area.menu_spparea_import +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Area Import" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import_raw +msgid "Area Import Raw Data" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__area_level +msgid "Area Level" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Area Name..." +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_spparea_kind +#: model:ir.model,name:spp_area.model_spp_area_kind +#: model:ir.ui.menu,name:spp_area.menu_spparea_kind +msgid "Area Type" +msgstr "" + +#. module: spp_area +#: model:ir.actions.act_window,name:spp_area.action_view_spparea_import +msgid "Area Upload" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area.py:0 code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Area already exist!" +msgstr "" + +#. module: spp_area +#: model:ir.ui.menu,name:spp_area.area_main_top_menu +msgid "Areas" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_spp_area_import +msgid "Areas Import Table" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't delete default Area Type" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area.py:0 +#, python-format +msgid "Can't delete used Area Type" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Cancel" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__cancelled +msgid "Cancelled" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__child_ids +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Child" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea_kind +msgid "Click the create button to enter the information of the Area Type." +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea +msgid "Click the create button to enter the information of the Area." +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_view_spparea_import +msgid "Click the create button to upload a new excel file." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__code +msgid "Code" +msgstr "" + +#. module: spp_area +#: model:ir.model.constraint,message:spp_area.constraint_spp_area_code_unique +msgid "Code is already exists!" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__complete_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__complete_name +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_kind_tree +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_tree +msgid "Complete Name" +msgstr "" + +#. module: spp_area +#: model:ir.model,name:spp_area.model_res_partner +msgid "Contact" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea_kind +msgid "Create a new Area Type!" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_spparea +msgid "Create a new Area!" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__create_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__create_uid +msgid "Created by" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__create_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__create_date +msgid "Created on" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "DONE" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_imported +msgid "Date Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_uploaded +msgid "Date Uploaded" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__date_validated +msgid "Date Validated" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__display_name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__display_name +msgid "Display Name" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__done +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Done" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__draft_name +msgid "Draft Name" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "ERROR: {}" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__error +msgid "Error" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__name +msgid "File Name" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Fix Area Level and Kind" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "Fixing area level." +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Group By" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__id +msgid "ID" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "IMPORTED" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Import" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__imported +msgid "Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__import_id +msgid "Imported by" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "Importing data." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__kind +msgid "Kind" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "" +"Language with ISO Code %s is not active.\n" +"Please request the administrator to enable the desired language." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__write_uid +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__write_date +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__write_date +msgid "Last Updated on" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__level +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__level +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +msgid "Level" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "Level 0 area should not have a parent name and parent code." +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "Level 1 and above area should have a parent name and parent code." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__locked +msgid "Locked" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__locked_reason +msgid "Locked Reason" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "NEW" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__name +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__name +msgid "Name" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "Name and Code of area is required." +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Name:" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__new +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__new +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "New" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "No active language found." +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__parent_id +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__parent_id +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +msgid "Parent" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__parent_code +msgid "Parent Code" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__parent_name +msgid "Parent Name" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area__parent_path +#: model:ir.model.fields,field_description:spp_area.field_spp_area_kind__parent_path +msgid "Parent Path" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Parent:" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__posted +msgid "Posted" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__raw_data_ids +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Raw Data" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Refresh" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Refresh Page" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__remarks +msgid "Remarks/Errors" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Reset to Uploaded" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Save to Area" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_filter +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_kind_filter +msgid "Search Area" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Search Imported File" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_form +msgid "Select a Parent..." +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "State" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__state_order +msgid "State Order" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__state +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import_raw__state +msgid "Status" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,help:spp_area.field_spp_area__level +msgid "This is the area level for importing" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,help:spp_area.field_spp_area__area_level +msgid "This is the main area level" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__tot_rows_imported +msgid "Total Rows Imported" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__tot_rows_error +msgid "Total Rows with Error" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "UPLOADED" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__updated +msgid "Updated" +msgstr "" + +#. module: spp_area +#: model_terms:ir.actions.act_window,help:spp_area.action_view_spparea_import +msgid "Upload an excel file!" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__uploaded +#: model_terms:ir.ui.view,arch_db:spp_area.spparea_import_filter +msgid "Uploaded" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__upload_id +msgid "Uploaded by" +msgstr "" + +#. module: spp_area +#: model_terms:ir.ui.view,arch_db:spp_area.view_spparea_import_form +msgid "Validate Data" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import__state__validated +#: model:ir.model.fields.selection,name:spp_area.selection__spp_area_import_raw__state__validated +msgid "Validated" +msgstr "" + +#. module: spp_area +#: model:ir.model.fields,field_description:spp_area.field_spp_area_import__validate_id +msgid "Validated by" +msgstr "" + +#. module: spp_area +#. odoo-python +#: code:addons/spp_area/models/area_import.py:0 +#, python-format +msgid "Validating data." +msgstr "" diff --git a/spp_area_base/models/__init__.py b/spp_area_base/models/__init__.py new file mode 100644 index 000000000..840ddfc30 --- /dev/null +++ b/spp_area_base/models/__init__.py @@ -0,0 +1,5 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + + +from . import area +from . import area_import diff --git a/spp_area_base/models/area.py b/spp_area_base/models/area.py new file mode 100644 index 000000000..1a8d21037 --- /dev/null +++ b/spp_area_base/models/area.py @@ -0,0 +1,303 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +import logging +import textwrap + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class OpenSPPArea(models.Model): + """ + Represents an area in the OpenSPP system. + + This model defines the structure and behavior of geographical areas, + including their hierarchical relationships, names, codes, and other attributes. + """ + + _name = "spp.area" + _description = "Area" + _order = "id desc" + _parent_name = "parent_id" + _parent_store = True + _order = "parent_id,name" + + parent_id = fields.Many2one("spp.area", "Parent") + complete_name = fields.Char(compute="_compute_complete_name", recursive=True, translate=True) + name = fields.Char(translate=True, compute="_compute_name", store=True) + draft_name = fields.Char(required=True, translate=True) + parent_path = fields.Char(index=True, unaccent=False) + code = fields.Char() + altnames = fields.Char("Alternate Name") + level = fields.Integer(help="This is the area level for importing") + area_level = fields.Integer(compute="_compute_area_level", store=True, help="This is the main area level") + child_ids = fields.One2many("spp.area", "id", "Child", compute="_compute_get_childs") + kind = fields.Many2one("spp.area.kind") + area_sqkm = fields.Float("Area (sq/km)") + + _sql_constraints = [ + ( + "code_unique", + "unique (code)", + "Code is already exists!", + ) + ] + + @api.depends("draft_name", "code") + def _compute_name(self): + """ + Compute the name for the area to include the code. + + The name is set as a combination of the code (if present) and the draft name. + """ + for rec in self: + name = rec.draft_name or "" + + if rec.code: + name = f"{rec.code} - {name}" + + rec.name = name + + def _compute_get_childs(self): + """ + Compute the child areas of the current area. + + This method searches for all areas that have the current area as their parent + and assigns them to the child_ids field. + """ + for rec in self: + child_ids = self.env["spp.area"].search([("parent_id", "=", rec.id)]) + rec.child_ids = child_ids + + @api.depends("parent_id") + def _compute_area_level(self): + """ + Compute the area level based on the parent area. + + If the area has a parent, its level is set to the parent's level plus one. + If it doesn't have a parent, its level is set to 0. + """ + for rec in self: + if rec.parent_id: + rec.area_level = rec.parent_id.area_level + 1 + else: + rec.area_level = 0 + + @api.onchange("parent_id") + def _onchange_parent_id(self): + """ + Validate the area level when the parent area changes. + + Raises a ValidationError if the resulting area level would exceed 10. + """ + for rec in self: + if rec.area_level > 10: + raise ValidationError( + _( + textwrap.fill( + textwrap.dedent( + """Max level exceeded! Can't have area with level greater + than 10 and your current area is level %s.""" + % rec.area_level + ) + ) + ) + ) + + @api.depends("name", "parent_id.complete_name") + def _compute_complete_name(self): + """ + Compute the complete name of the area. + + The complete name includes the parent's complete name (if any) and the area's own name. + """ + for rec in self: + if rec.id: + if rec.parent_id: + rec.complete_name = f"{rec.parent_id.complete_name} > {rec.name}" + else: + rec.complete_name = rec.name + else: + rec.complete_name = None + + @api.model + def create(self, vals): + """ + Create a new area record. + + This method overrides the default create method to add additional validation + and translation handling. + + :param vals: The values for the new record. + :return: The newly created area record. + :raises ValidationError: If an area with the same name and code already exists. + """ + area_name = self.name + if "name" in vals: + area_name = vals["name"] + area_code = self.code + if "code" in vals: + area_code = vals["code"] + curr_area = self.env["spp.area"].search( + [ + ("name", "=", area_name), + ("code", "=", area_code), + ] + ) + + if curr_area: + raise ValidationError(_("Area already exist!")) + else: + Area = super().create(vals) + Languages = self.env["res.lang"].search([("active", "=", True)]) + vals_list = [] + for lang_code in Languages: + vals_list.append( + { + "name": "spp.area,draft_name", + "lang": lang_code.code, + "res_id": Area.id, + "src": Area.draft_name, + "value": None, + "state": "to_translate", + "type": "model", + } + ) + + return Area + + def write(self, vals): + """ + Update an existing area record. + + This method overrides the default write method to add additional validation. + + :param vals: The values to update. + :return: The result of the write operation. + :raises ValidationError: If an area with the same name and code already exists. + """ + for rec in self: + area_name = rec.name + if "name" in vals: + area_name = vals["name"] + area_code = rec.code + if "code" in vals: + area_code = vals["code"] + curr_area = self.env["spp.area"].search( + [ + ("name", "=", area_name), + ("code", "=", area_code), + ("id", "!=", rec.id), + ] + ) + if curr_area: + raise ValidationError(_("Area already exist!")) + else: + return super().write(vals) + + def open_area_form(self): + """ + Open the form view of the area. + + :return: A dictionary containing the action to open the area form view. + """ + for rec in self: + return { + "name": "Area", + "view_mode": "form", + "res_model": "spp.area", + "res_id": rec.id, + "view_id": self.env.ref("spp_area_base.view_spparea_form").id, + "type": "ir.actions.act_window", + "target": "new", + "flags": {"mode": "readonly"}, + } + + +class OpenSPPAreaKind(models.Model): + """ + Represents the type or kind of an area in the OpenSPP system. + + This model defines the structure and behavior of area types, including + their hierarchical relationships and names. + """ + + _name = "spp.area.kind" + _description = "Area Type" + _parent_name = "parent_id" + _parent_store = True + _rec_name = "complete_name" + _order = "parent_id,name" + + parent_id = fields.Many2one("spp.area.kind", "Parent") + parent_path = fields.Char(index=True) + name = fields.Char(required=True) + complete_name = fields.Char(compute="_compute_complete_name", recursive=True, translate=True) + + @api.depends("name", "parent_id.complete_name") + def _compute_complete_name(self): + """ + Compute the complete name of the area type. + + The complete name includes the parent's complete name (if any) and the area type's own name. + """ + for rec in self: + if rec.id: + if rec.parent_id: + rec.complete_name = f"{rec.parent_id.complete_name} > {rec.name}" + else: + rec.complete_name = rec.name + else: + rec.complete_name = None + + def unlink(self): + """ + Delete an area type record. + + This method overrides the default unlink method to add additional validation. + It prevents the deletion of default area types and area types that are in use. + + :raises ValidationError: If trying to delete a default area type or an area type in use. + """ + for rec in self: + external_identifier = self.env["ir.model.data"].search( + [("res_id", "=", rec.id), ("model", "=", "spp.area.kind")] + ) + if external_identifier and external_identifier.name: + raise ValidationError(_("Can't delete default Area Type")) + else: + areas = self.env["spp.area"].search([("kind", "=", rec.id)]) + if areas: + raise ValidationError(_("Can't delete used Area Type")) + else: + return super().unlink() + + def write(self, vals): + """ + Update an existing area type record. + + This method overrides the default write method to prevent editing of default area types. + + :param vals: The values to update. + :return: The result of the write operation. + """ + for rec in self: + external_identifier = self.env["ir.model.data"].search( + [("res_id", "=", rec.id), ("model", "=", "spp.area.kind")] + ) + if external_identifier and external_identifier.name: + vals = {} + return super().write(vals) + + def update_parent(self): + for rec in self: + if rec.parent_name and rec.parent_code: + parent_id = self.env["spp.area.kind"].search( + [ + ("code", "=", rec.parent_code), + ], + limit=1, + ) + if parent_id: + rec.parent_id = parent_id.id diff --git a/spp_area_base/models/area_import.py b/spp_area_base/models/area_import.py new file mode 100644 index 000000000..86b2889ef --- /dev/null +++ b/spp_area_base/models/area_import.py @@ -0,0 +1,620 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +import base64 +import logging +import math +from io import BytesIO + +from xlrd import open_workbook + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from odoo.addons.queue_job.delay import group + +_logger = logging.getLogger(__name__) + + +class OpenSPPAreaImport(models.Model): + _name = "spp.area.import" + _description = "Areas Import Table" + + MIN_ROW_JOB_QUEUE = 400 + + NEW = "New" + UPLOADED = "Uploaded" + IMPORTED = "Imported" + VALIDATED = "Validated" + DONE = "Done" + CANCELLED = "Cancelled" + + STATE_SELECTION = [ + (NEW, NEW), + (UPLOADED, UPLOADED), + (IMPORTED, IMPORTED), + (VALIDATED, VALIDATED), + (DONE, DONE), + (CANCELLED, CANCELLED), + ] + + name = fields.Char("File Name", required=True, translate=True) + excel_file = fields.Binary("Area Excel File") + date_uploaded = fields.Datetime() + + upload_id = fields.Many2one("res.users", "Uploaded by") + date_imported = fields.Datetime() + import_id = fields.Many2one("res.users", "Imported by") + date_validated = fields.Datetime() + validate_id = fields.Many2one("res.users", "Validated by") + raw_data_ids = fields.One2many("spp.area.import.raw", "area_import_id", "Raw Data") + tot_rows_imported = fields.Integer( + "Total Rows Imported", + compute="_compute_get_total_rows", + store=True, + readonly=True, + ) + tot_rows_error = fields.Integer( + "Total Rows with Error", + compute="_compute_get_total_rows", + store=True, + readonly=True, + ) + state = fields.Selection( + STATE_SELECTION, + "Status", + default=NEW, + ) + + locked = fields.Boolean(default=False) + locked_reason = fields.Char(readonly=True) + + @api.onchange("excel_file") + def excel_file_change(self): + """ + The above function is an onchange function in Python that updates the date_uploaded, upload_id, + and state fields based on the value of the excel_file field. + """ + + if self.name: + self.update( + { + "date_uploaded": fields.Datetime.now(), + "upload_id": self.env.user, + "state": self.UPLOADED, + } + ) + else: + self.update({"date_uploaded": None, "upload_id": None, "state": self.UPLOADED}) + + @api.depends("raw_data_ids", "raw_data_ids.state") + def _compute_get_total_rows(self): + """ + The function `_compute_get_total_rows` calculates the total number of imported rows and the + total number of rows with an error for a given record. + """ + for rec in self: + tot_rows_imported = len(rec.raw_data_ids) + tot_rows_error = self.env["spp.area.import.raw"].search( + [("id", "in", rec.raw_data_ids.ids), ("state", "=", "Error")] + ) + rec.update( + { + "tot_rows_imported": tot_rows_imported, + "tot_rows_error": len(tot_rows_error), + } + ) + + def cancel_import(self): + """ + The function cancels the import by updating the state of the record to "Cancelled". + """ + for rec in self: + rec.update({"state": self.CANCELLED}) + + def reset_to_uploaded(self): + """ + The function resets the state of a record to "Uploaded". + """ + for rec in self: + rec.update({"state": self.UPLOADED}) + + def get_column_indexes(self, columns, area_level): + self.ensure_one() + default_lang = self.env.context.get("lang", "en_US") + if default_lang not in columns: + default_lang = "en_US" + default_iso_code = default_lang.split("_")[0].upper() + + active_languages = self.env["res.lang"].search([("active", "=", True)]) + if not active_languages: + raise ValidationError(_("No active language found.")) + + # Get column prefix and the language iso code used in the name header + lang_codes = active_languages.read(fields=["code", "iso_code"]) + column_name_prefix = f"ADM{area_level}" + + # Get Column name to be used as name field in the area + name_headers = {code["code"]: f"{column_name_prefix}_{code['iso_code'].upper()}" for code in lang_codes} + + # Get Column name to be used as code field in the area + code_header = f"{column_name_prefix}_PCODE" + + # get name and code column indexes + name_indexes = {} + for name_header in name_headers: + try: + name_indexes.update({name_header: columns.index(name_headers[name_header])}) + except ValueError as e: + _logger.warning("Column header not found: %s", e) + code_index = columns.index(code_header) + + # Get index of the Parent header of the area if area level is not 0 + parent_name_index = None + parent_code_index = None + if area_level != 0: + parent_name_header = f"{column_name_prefix[:3]}{area_level - 1}_{default_iso_code}" + parent_code_header = f"{column_name_prefix[:3]}{area_level - 1}_PCODE" + + parent_name_index = columns.index(parent_name_header) + parent_code_index = columns.index(parent_code_header) + + # Get area_sqkm column index + area_sqkm_index = None + if "AREA_SQKM" in columns: + area_sqkm_index = columns.index("AREA_SQKM") + + return { + "name_indexes": name_indexes, + "code_index": code_index, + "parent_name_index": parent_name_index, + "parent_code_index": parent_code_index, + "area_sqkm_index": area_sqkm_index, + } + + def get_area_vals(self, column_indexes, row, sheet, area_level): + self.ensure_one() + default_lang = self.env.context.get("lang", "en_US") + if default_lang not in column_indexes["name_indexes"]: + default_lang = "en_US" + vals = { + "admin_name": sheet.cell(row, column_indexes["name_indexes"][default_lang]).value, + "admin_code": sheet.cell(row, column_indexes["code_index"]).value, + "parent_name": "", + "parent_code": "", + "level": area_level, + "area_import_id": self.id, + } + if column_indexes["area_sqkm_index"]: + vals["area_sqkm"] = sheet.cell(row, column_indexes["area_sqkm_index"]).value + + if column_indexes["parent_name_index"] is not None and column_indexes["parent_code_index"] is not None: + vals["parent_name"] = sheet.cell(row, column_indexes["parent_name_index"]).value + vals["parent_code"] = sheet.cell(row, column_indexes["parent_code_index"]).value + + return vals + + def create_import_raw(self, vals, column_indexes, row, sheet): + self.ensure_one() + import_raw_id = self.env["spp.area.import.raw"].create(vals) + for lang_code in column_indexes["name_indexes"]: + lang_name = sheet.cell(row, column_indexes["name_indexes"][lang_code]).value + import_raw_id.with_context(lang=lang_code).write( + { + "admin_name": lang_name, + } + ) + + def _get_book(self): + self.ensure_one() + try: + inputx = BytesIO() + inputx.write(base64.decodebytes(self.excel_file)) + except TypeError as e: + raise ValidationError(_("ERROR: {}").format(e)) from e + return open_workbook(file_contents=inputx.getvalue()) + + def check_all_languages_activated(self, columns, area_level): + """Check if all languages in the specified columns are activated. + + Args: + columns (list): The list of column names to check. + area_level (int): The administrative area level to check within the column names. + + Raises: + ValidationError: If any language is not active. + """ + self.ensure_one() + prefix = f"ADM{area_level}_" + active_langs = self.env["res.lang"].search([("active", "=", True)]).mapped("iso_code") + + for col in columns: + if col.startswith(prefix): + lang = col.split("_", 1)[1] + if len(lang) == 2 and lang.lower() not in active_langs: + raise ValidationError( + _( + "Language with ISO Code %s is not active.\n" + "Please request the administrator to enable the desired language." + ) + % lang.upper() + ) + + def import_data(self): + self.ensure_one() + + _logger.info("Area Import: Started: %s" % fields.Datetime.now()) + # Delete all existing import data for this record + # This can only be happen if the Area Upload record is reset back to Uploaded state + if self.raw_data_ids: + self.raw_data_ids.unlink() + + _logger.info("Area Import: Loading Excel File: %s" % fields.Datetime.now()) + # Wrap binary to BytesIO + + book = self._get_book() + + sheet_names = book.sheet_names() + sheet_names.sort() + self.locked = True + self.locked_reason = _("Importing data.") + + jobs = [] + + for area_level, sheet_name in enumerate(sheet_names): + sheet = book.sheet_by_name(sheet_name) + columns = sheet.row_values(0) + self.check_all_languages_activated(columns, area_level) + column_indexes = self.get_column_indexes(columns, area_level) + + batches = math.ceil(sheet.nrows / 1000) + for i in range(batches): + if i == 0: + start = 1 + else: + start = i * 1000 + end = min((i + 1) * 1000, sheet.nrows) + jobs.append( + self.delayable(channel="root.area_import")._import_data( + sheet_name, column_indexes, start, end, area_level + ) + ) + + main_job = group(*jobs) + + main_job.on_done(self.delayable(channel="root.area_import")._async_mark_done()) + main_job.delay() + + def _import_data(self, sheet_name, column_indexes, start, end, area_level): + """ + The `import_data` function imports data from an Excel file, processes it, and updates the record + with the imported data. + """ + self.ensure_one() + + book = self._get_book() + + sheet = book.sheet_by_name(sheet_name) + for row in range(start, end): + import_raw_vals = self.get_area_vals(column_indexes, row, sheet, area_level) + self.create_import_raw(import_raw_vals, column_indexes, row, sheet) + + self.update( + { + "date_imported": fields.Datetime.now(), + "import_id": self.env.user, + "date_validated": fields.Datetime.now(), + "validate_id": self.env.user, + "state": self.IMPORTED, + } + ) + + def validate_raw_data(self): + """ + The function iterates through a collection of records and checks if the count of raw data is + less than a minimum threshold, and if so, it calls a validation function, otherwise it calls an + """ + for rec in self: + rec.locked = True + rec.locked_reason = _("Validating data.") + batches = math.ceil(len(rec.raw_data_ids) / 1000) + jobs = [] + for i in range(batches): + start = i * 1000 + end = min((i + 1) * 1000, len(rec.raw_data_ids)) + jobs.append(rec.delayable(channel="root.area_import")._validate_raw_data(rec.raw_data_ids[start:end])) + main_job = group(*jobs) + main_job.on_done(rec.delayable(channel="root.area_import")._validate_mark_done()) + main_job.delay() + + def _validate_raw_data(self, raw_data_ids): + """ + The function validates raw data and updates the state if there are no errors. + """ + self.ensure_one() + raw_data_ids.validate_raw_data() + + def _validate_mark_done(self): + self.locked = False + self.locked_reason = None + self.ensure_one() + if not self.env["spp.area.import.raw"].search([("id", "in", self.raw_data_ids.ids), ("state", "=", "Error")]): + self.update( + { + "state": self.VALIDATED, + } + ) + + def fix_area_level_and_kind(self): + for rec in self: + rec.locked = True + rec.locked_reason = _("Fixing area level.") + batches = math.ceil(len(rec.raw_data_ids) / 1000) + jobs = [] + for i in range(batches): + start = i * 1000 + end = min((i + 1) * 1000, len(rec.raw_data_ids)) + jobs.append( + rec.delayable(channel="root.area_import")._fix_area_level_and_kind(rec.raw_data_ids[start:end]) + ) + main_job = group(*jobs) + main_job.on_done(rec.delayable(channel="root.area_import")._async_mark_done()) + main_job.delay() + + def _fix_area_level_and_kind(self, raw_data_ids): + """ + The function `fix_area_level_and_kind` fixes the area level of the raw data. + """ + self.ensure_one() + raw_data_ids.fix_area_level_and_kind() + + def _async_mark_done(self, function_mark_done=None): + """ + The function `_async_mark_done` unlocks a resource by setting the `locked` attribute to `False` + and clearing the `locked_reason` attribute. + """ + self.ensure_one() + + self.locked = False + self.locked_reason = None + + if function_mark_done: + getattr(self, function_mark_done)() + + def save_to_area(self): + """ + The function saves data to an area, either synchronously or asynchronously depending on the + number of raw data records. + """ + for rec in self: + rec.locked = True + rec.locked_reason = _("Importing data.") + rec._async_recursive_save_to_area(rec.raw_data_ids) + + def _async_recursive_save_to_area(self, raw_data_ids): + """ + This is to ensure that the function `_save_to_area` is called recursively and in order until all raw data + is saved to the area. + """ + self.ensure_one() + jobs = [] + jobs.append(self.delayable(channel="root.area_import")._save_to_area(raw_data_ids[:1000])) + main_job = group(*jobs) + count = len(raw_data_ids) + if count <= 1000: + main_job.on_done(self.delayable(channel="root.area_import")._save_to_area_mark_done()) + else: + main_job.on_done( + self.delayable(channel="root.area_import")._async_recursive_save_to_area(raw_data_ids[1000:]) + ) + main_job.delay() + + def _save_to_area(self, raw_data_ids): + """ + The function saves raw data to an area and updates the state to "DONE". + """ + self.ensure_one() + + raw_data_ids.save_to_area() + + def _save_to_area_mark_done(self): + self.ensure_one() + self.locked = False + self.locked_reason = None + if not self.env["spp.area.import.raw"].search( + [("id", "in", self.raw_data_ids.ids), ("state", "=", "Validated")] + ): + self.update( + { + "state": self.DONE, + } + ) + + def refresh_page(self): + """ + The function `refresh_page` returns a dictionary with the type and tag values to reload the + page. + :return: The code is returning a dictionary with two key-value pairs. The "type" key has the + value "ir.actions.client" and the "tag" key has the value "reload". + """ + return { + "type": "ir.actions.client", + "tag": "reload", + } + + +# Assets Import Raw Data +class OpenSPPAreaImportActivities(models.Model): + _name = "spp.area.import.raw" + _description = "Area Import Raw Data" + _order = "level" + + NEW = "New" + VALIDATED = "Validated" + ERROR = "Error" + UPDATED = "Updated" + POSTED = "Posted" + + STATE_CHOICES = [ + (NEW, NEW), + (VALIDATED, VALIDATED), + (ERROR, ERROR), + (UPDATED, UPDATED), + (POSTED, POSTED), + ] + + STATE_ORDER = { + ERROR: 0, + NEW: 1, + VALIDATED: 2, + UPDATED: 3, + POSTED: 4, + } + + area_import_id = fields.Many2one("spp.area.import", "Area Import", required=True) + admin_name = fields.Char(translate=True) + admin_code = fields.Char() + + parent_name = fields.Char() + parent_code = fields.Char() + + level = fields.Integer() + + area_sqkm = fields.Char("Area (sq/km)") + + remarks = fields.Text("Remarks/Errors") + state = fields.Selection( + STATE_CHOICES, + "Status", + default="New", + ) + state_order = fields.Integer( + compute="_compute_state_order", + store=True, + ) + area_id = fields.Many2one("spp.area", "Area", readonly=True) + + @api.depends("state") + def _compute_state_order(self): + for rec in self: + rec.state_order = self.STATE_ORDER[rec.state] + + def check_errors(self): + self.ensure_one() + errors = [] + if not self.admin_name or not self.admin_code: + errors.append(_("Name and Code of area is required.")) + + if self.area_sqkm: + try: + float(self.area_sqkm) + except ValueError: + errors.append(_("AREA_SQKM should be numerical.")) + + if self.level == 0 and (self.parent_name or self.parent_code): + errors.append(_("Level 0 area should not have a parent name and parent code.")) + + if self.level != 0 and (not self.parent_name or not self.parent_code): + errors.append(_("Level 1 and above area should have a parent name and parent code.")) + return errors + + def validate_raw_data(self): + for rec in self: + errors = rec.check_errors() + + if errors: + state = self.ERROR + remarks = "\n".join(errors) + else: + state = self.VALIDATED + remarks = "No Error" + + rec.write( + { + "remarks": remarks, + "state": state, + } + ) + + def get_area_vals(self): + self.ensure_one() + + parent_id = None + if self.parent_name and self.parent_code: + parent_id = ( + self.env["spp.area"] + .search( + [ + ("code", "=", self.parent_code), + ], + limit=1, + ) + .id + ) + + area_sqkm = self.area_sqkm + + try: + area_sqkm = float(area_sqkm) + except ValueError: + area_sqkm = 0.0 + + return { + "parent_id": parent_id, + "draft_name": self.admin_name, + "code": self.admin_code, + "area_sqkm": area_sqkm, + "kind": self.env.ref("spp_area_base.admin_area_kind").id, + } + + def save_to_area(self): + """ + The function saves data to the "spp.area" model in the database, updating existing records if + they exist and creating new records if they don't. + """ + active_languages = self.env["res.lang"].search([("active", "=", True)]) + for rec in self: + area_vals = rec.get_area_vals() + if area_id := self.env["spp.area"].search([("code", "=", rec.admin_code)]): + state = self.UPDATED + area_id.update(area_vals) + else: + state = self.POSTED + area_id = self.env["spp.area"].create(area_vals) + + for lang in active_languages: + area_id.with_context(lang=lang.code).write( + { + "draft_name": rec.with_context(lang=lang.code).admin_name, + } + ) + area_id.with_context(lang=lang.code)._compute_name() + area_id.with_context(lang=lang.code)._compute_complete_name() + + rec.update( + { + "state": state, + "remarks": "Successfully save to Area", + "area_id": area_id.id, + } + ) + + def fix_area_level_and_kind(self): + for rec in self: + if rec.area_id and (rec.area_id.area_level != rec.level or not rec.area_id.kind): + parent_id = None + if rec.parent_name and rec.parent_code: + parent_id = ( + self.env["spp.area"] + .search( + [ + ("code", "=", rec.parent_code), + ], + limit=1, + ) + .id + ) + rec.area_id.update( + { + "kind": rec.env.ref("spp_area_base.admin_area_kind").id, + "parent_id": parent_id, + } + ) diff --git a/spp_area_base/pyproject.toml b/spp_area_base/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_area_base/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_area_base/readme/DESCRIPTION.md b/spp_area_base/readme/DESCRIPTION.md new file mode 100644 index 000000000..538064547 --- /dev/null +++ b/spp_area_base/readme/DESCRIPTION.md @@ -0,0 +1,42 @@ +# OpenSPP Area + +This document describes the **OpenSPP Area** module, which extends the OpenSPP framework by providing features to manage and organize geographical areas within the system. It integrates with the core registry modules to allow associating registrants and other data with specific locations. + +## Purpose + +The **OpenSPP Area** module is designed to: + +* **Define and Structure Geographical Areas**: Establish a hierarchical structure for representing administrative regions, from the highest level (e.g., country) down to the most granular level (e.g., village). +* **Manage Area Information**: Store key details about each area, including its name, code, alternate names, geographical size, and parent-child relationships within the hierarchy. +* **Associate Registrants with Areas**: Enable the linking of individual and group registrants to specific areas, facilitating location-based targeting, analysis, and program implementation. + +## Dependencies and Integration + +1. **G2P Registry: Base ([g2p_registry_base](g2p_registry_base))**: The Area module utilizes the **Districts (g2p.district)** feature from the **G2P Registry: Base** module as a foundation. It extends this concept to create a more comprehensive and flexible system for managing area data. + +2. **G2P Registry: Individual ([g2p_registry_individual](g2p_registry_individual))**: Integrates with the Individual module by adding a dedicated "Area" field to the individual registrant form. This field allows users to assign a specific area to each individual, linking registrant data to geographical locations. + +3. **G2P Registry: Group ([g2p_registry_group](g2p_registry_group))**: Similar to the Individual module integration, this module incorporates an "Area" field into the group registrant form, enabling the association of groups with specific areas. + +4. **Queue Job ([queue_job](queue_job))**: Leverages the **Queue Job** module for background processing of large data imports, improving performance and user experience. This is particularly beneficial when importing extensive area hierarchies from external sources. + +## Additional Functionality + +* **Hierarchical Area Structure ([spp.area](spp.area))**: + * Introduces a dedicated model for managing areas, allowing for the creation of multi-level administrative boundaries with parent-child relationships. + * Computes and displays the complete area path (e.g., "Country > Province > District > Village") to provide clear context within the hierarchy. + * Enforces unique codes for each area to ensure proper identification and prevent duplicates. + +* **Area Types ([spp.area.kind](spp.area.kind))**: + * Includes a model for defining and managing different types of areas (e.g., administrative regions, ecological zones, project implementation areas). + * Allows for the creation of a hierarchy of area types, providing further categorization and flexibility. + +* **Area Import Functionality**: + * Provides tools for importing area data in bulk from Excel files, streamlining the process of populating the area hierarchy. + * Implements validation rules during the import process to ensure data integrity, such as checking for required fields, data types, and hierarchical consistency. + * Utilizes the Queue Job module to perform data validation and import operations in the background, preventing performance issues and providing a smoother user experience. + * Ability to localize the name of the imported area. + +## Conclusion + +The **OpenSPP Area** module enhances the OpenSPP platform by providing a robust and flexible system for managing geographical areas and linking them to registrant data. Its integration with the core registry modules ensures that location information is seamlessly incorporated into the overall system, supporting location-based targeting, analysis, and program management for social protection programs and farmer registries. diff --git a/spp_area_base/security/ir.model.access.csv b/spp_area_base/security/ir.model.access.csv new file mode 100644 index 000000000..561118591 --- /dev/null +++ b/spp_area_base/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +spp_area_admin,Area Admin Access,spp_area.model_spp_area,base.group_system,1,1,1,1 +spp_area_import_sysadmin,Area Import Admin Access,spp_area.model_spp_area_import,base.group_system,1,1,1,1 +spp_area_import_raw_sysadmin,Area Import Raw Admin Access,spp_area.model_spp_area_import_raw,base.group_system,1,1,1,1 +spp_area_kind_sysadmin,Area Kind Admin Access,spp_area.model_spp_area_kind,base.group_system,1,1,1,1 diff --git a/spp_area_base/static/description/icon.png b/spp_area_base/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..35f8fec263588314689c920efc4b6728b407daaa GIT binary patch literal 12567 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®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 + + + + +OpenSPP Area Management + + + +
+

OpenSPP Area Management

+ + +

Production/Stable License: LGPL-3 OpenSPP/openspp-modules

+
+

OpenSPP Area

+

This document describes the OpenSPP Area module, which extends the +OpenSPP framework by providing features to manage and organize +geographical areas within the system. It integrates with the core +registry modules to allow associating registrants and other data with +specific locations.

+
+

Purpose

+

The OpenSPP Area module is designed to:

+
    +
  • Define and Structure Geographical Areas: Establish a hierarchical +structure for representing administrative regions, from the highest +level (e.g., country) down to the most granular level (e.g., +village).
  • +
  • Manage Area Information: Store key details about each area, +including its name, code, alternate names, geographical size, and +parent-child relationships within the hierarchy.
  • +
  • Associate Registrants with Areas: Enable the linking of +individual and group registrants to specific areas, facilitating +location-based targeting, analysis, and program implementation.
  • +
+
+
+

Dependencies and Integration

+
    +
  1. G2P Registry: Base +(g2p_registry_base): The Area +module utilizes the Districts (g2p.district) feature from the +G2P Registry: Base module as a foundation. It extends this +concept to create a more comprehensive and flexible system for +managing area data.
  2. +
  3. G2P Registry: Individual +(g2p_registry_individual): +Integrates with the Individual module by adding a dedicated “Area” +field to the individual registrant form. This field allows users to +assign a specific area to each individual, linking registrant data to +geographical locations.
  4. +
  5. G2P Registry: Group +(g2p_registry_group): Similar to +the Individual module integration, this module incorporates an “Area” +field into the group registrant form, enabling the association of +groups with specific areas.
  6. +
  7. Queue Job (queue_job): Leverages the +Queue Job module for background processing of large data imports, +improving performance and user experience. This is particularly +beneficial when importing extensive area hierarchies from external +sources.
  8. +
+
+
+

Additional Functionality

+
    +
  • Hierarchical Area Structure (spp.area):
      +
    • Introduces a dedicated model for managing areas, allowing for the +creation of multi-level administrative boundaries with +parent-child relationships.
    • +
    • Computes and displays the complete area path (e.g., “Country > +Province > District > Village”) to provide clear context within +the hierarchy.
    • +
    • Enforces unique codes for each area to ensure proper +identification and prevent duplicates.
    • +
    +
  • +
  • Area Types (spp.area.kind):
      +
    • Includes a model for defining and managing different types of +areas (e.g., administrative regions, ecological zones, project +implementation areas).
    • +
    • Allows for the creation of a hierarchy of area types, providing +further categorization and flexibility.
    • +
    +
  • +
  • Area Import Functionality:
      +
    • Provides tools for importing area data in bulk from Excel files, +streamlining the process of populating the area hierarchy.
    • +
    • Implements validation rules during the import process to ensure +data integrity, such as checking for required fields, data types, +and hierarchical consistency.
    • +
    • Utilizes the Queue Job module to perform data validation and +import operations in the background, preventing performance issues +and providing a smoother user experience.
    • +
    • Ability to localize the name of the imported area.
    • +
    +
  • +
+
+
+

Conclusion

+

The OpenSPP Area module enhances the OpenSPP platform by providing a +robust and flexible system for managing geographical areas and linking +them to registrant data. Its integration with the core registry modules +ensures that location information is seamlessly incorporated into the +overall system, supporting location-based targeting, analysis, and +program management for social protection programs and farmer registries.

+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 reichie020212

+

This module is part of the OpenSPP/openspp-modules project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/spp_area_base/static/src/img/icons/contacts.png b/spp_area_base/static/src/img/icons/contacts.png new file mode 100644 index 0000000000000000000000000000000000000000..8582a80b512c4a483f1c062faa764ea4374b8e6e GIT binary patch literal 591 zcmV-V0QIvv$g9hz94FwCb!9II^_NB>4Ti83EH-7v4cI}XZq>m(bopWCS ztfCuSWm)#6YSQH|LI^*cbK4ySenpmLuAT*ep{i+-cU$Q zo!gF3sEH~Ae*vuYeV_d;Y>*?|F$tJB6*jp-*pit_mMUR z&H%v7CX$AdMrJn2&$ULwA{@^s!nL{dn;3{Dh9nP2-oNdq(6Cixh-?=mru zfeVr{v)c2bne9ZK^DYhC$M#!$QkJwDb*4@9n-<|G0C3LzsUzk%OHG7JPefJdqenE& zz*P(F%j75icR~R7!1HGFuM8CS`b8Kh{Fq1@o7pv<&wRZh|6@|ffOGD~d%q6g5Wu@e d(h&+@#}k~#Ae6(71VI1*002ovPDHLkV1nWE`quye literal 0 HcmV?d00001 diff --git a/spp_area_base/tests/__init__.py b/spp_area_base/tests/__init__.py new file mode 100644 index 000000000..2bb125bfe --- /dev/null +++ b/spp_area_base/tests/__init__.py @@ -0,0 +1,6 @@ +# Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. + +from . import common +from . import test_area +from . import test_area_import +from . import test_area_import_raw diff --git a/spp_area_base/tests/common.py b/spp_area_base/tests/common.py new file mode 100644 index 000000000..d371412d4 --- /dev/null +++ b/spp_area_base/tests/common.py @@ -0,0 +1,51 @@ +import base64 +import os + +from odoo.tests.common import TransactionCase + + +class AreaImportTestMixin(TransactionCase): + @staticmethod + def get_file_path_1(): + return f"{os.path.dirname(os.path.abspath(__file__))}/irq_adminboundaries_tabulardata.xlsx" + + @staticmethod + def get_file_path_2(): + return f"{os.path.dirname(os.path.abspath(__file__))}/pse_adminboundaries_tabulardata.xlsx" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Greater than or equal to 400 rows + xls_file = None + xls_file_name = None + + file_path = cls.get_file_path_1() + with open(file_path, "rb") as f: + xls_file_name = f.name + xls_file = base64.b64encode(f.read()) + + cls.area_import_id = cls.env["spp.area.import"].create( + { + "excel_file": xls_file, + "name": xls_file_name, + "state": "Uploaded", + } + ) + + # Less than 400 rows + xls_file_2 = None + xls_file_name_2 = None + + file_path_2 = cls.get_file_path_2() + with open(file_path_2, "rb") as f: + xls_file_name_2 = f.name + xls_file_2 = base64.b64encode(f.read()) + + cls.area_import_id_2 = cls.env["spp.area.import"].create( + { + "excel_file": xls_file_2, + "name": xls_file_name_2, + "state": "Uploaded", + } + ) diff --git a/spp_area_base/tests/irq_adminboundaries_tabulardata.xlsx b/spp_area_base/tests/irq_adminboundaries_tabulardata.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7e65998362405ab3ae6257e72cbed25dae37d7d1 GIT binary patch literal 42057 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 z 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 onbj3#XmrKWvftdTw3+~TG?rqZZ?x=Sh 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 literal 0 HcmV?d00001 diff --git a/spp_area_base/tests/test_area.py b/spp_area_base/tests/test_area.py new file mode 100644 index 000000000..8039e1d26 --- /dev/null +++ b/spp_area_base/tests/test_area.py @@ -0,0 +1,33 @@ +# Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. + +import logging + +# from odoo.tests import tagged +from odoo.tests.common import TransactionCase + +_logger = logging.getLogger(__name__) + + +# @tagged("post_install", "-at_install") +class AreaTest(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Initial Setup of Variables + cls.area_1 = cls.env["spp.area"].create( + { + "draft_name": "Testing Area", + } + ) + cls.area_1_child = cls.env["spp.area"].create( + { + "draft_name": "Testing Area Child", + "parent_id": cls.area_1.id, + } + ) + + def test_01_check_childs(self): + self.area_1._compute_get_childs() + + self.assertEqual(len(self.area_1.child_ids), 1) diff --git a/spp_area_base/tests/test_area_import.py b/spp_area_base/tests/test_area_import.py new file mode 100644 index 000000000..fb442501f --- /dev/null +++ b/spp_area_base/tests/test_area_import.py @@ -0,0 +1,140 @@ +import logging + +from odoo.exceptions import ValidationError + +from .common import AreaImportTestMixin + +_logger = logging.getLogger(__name__) + + +class AreaImportTest(AreaImportTestMixin): + def test_01_cancel_import(self): + self.area_import_id.cancel_import() + + self.assertEqual(self.area_import_id.state, "Cancelled") + + def test_02_reset_to_uploaded(self): + self.area_import_id.reset_to_uploaded() + + self.assertEqual(self.area_import_id.state, "Uploaded") + + def test_03_import_data(self): + with self.assertRaises(ValidationError): + self.area_import_id.import_data() + + lang = self.env["res.lang"].with_context(active_test=False).search([("iso_code", "=", "ar")]) + lang.active = True + + self.area_import_id.import_data() + raw_data_ids = self.area_import_id.raw_data_ids + + self.assertEqual(len(raw_data_ids.ids), self.area_import_id.tot_rows_imported) + self.assertEqual(0, self.area_import_id.tot_rows_error) + self.assertEqual(self.area_import_id.state, "Uploaded") + self.assertEqual( + len(self.env["spp.area.import.raw"].search([("id", "in", raw_data_ids.ids), ("state", "=", "New")])), + self.area_import_id.tot_rows_imported, + ) + + # NOTE: validate_raw_data and save_to_area tests are not working + # since both of them are now fully asynchronous. + # update test case. + + # def test_04_validate_raw_data(self): + # # Greater than or equal to 400 rows + # with self.assertRaises(ValidationError): + # self.area_import_id.import_data() + + # lang = self.env["res.lang"].with_context(active_test=False).search([("iso_code", "=", "ar")]) + # lang.active = True + + # self.area_import_id.import_data() + # self.area_import_id.validate_raw_data() + + # raw_data_ids = self.area_import_id.raw_data_ids + + # self.assertEqual(len(raw_data_ids.ids), self.area_import_id.tot_rows_imported) + # self.assertEqual(0, self.area_import_id.tot_rows_error) + # self.assertEqual(self.area_import_id.state, "Imported") + # self.assertTrue(self.area_import_id.locked) + # self.assertEqual(self.area_import_id.locked_reason, "Validating data.") + + # # Less than 400 rows + # self.area_import_id_2.import_data() + # self.area_import_id_2.validate_raw_data() + + # raw_data_ids = self.area_import_id_2.raw_data_ids + # self.assertEqual(len(raw_data_ids.ids), self.area_import_id_2.tot_rows_imported) + # self.assertEqual(0, self.area_import_id_2.tot_rows_error) + # self.assertEqual(self.area_import_id_2.state, "Validated") + # self.assertFalse(self.area_import_id_2.locked) + # self.assertFalse(self.area_import_id_2.locked_reason) + # self.assertEqual( + # len( + # self.env["spp.area.import.raw"].search( + # [("id", "in", raw_data_ids.ids), ("state", "=", "Validated")], + # ) + # ), + # self.area_import_id_2.tot_rows_imported, + # ) + + # def test_05_save_to_area(self): + # # Greater than or equal to 400 rows + # with self.assertRaises(ValidationError): + # self.area_import_id.import_data() + # lang = self.env["res.lang"].with_context(active_test=False).search([("iso_code", "=", "ar")]) + # lang.active = True + + # self.area_import_id.import_data() + # self.area_import_id.save_to_area() + + # raw_data_ids = self.area_import_id.raw_data_ids + + # self.assertEqual(len(raw_data_ids.ids), self.area_import_id.tot_rows_imported) + # self.assertEqual(0, self.area_import_id.tot_rows_error) + # self.assertEqual(self.area_import_id.state, "Imported") + # self.assertTrue(self.area_import_id.locked) + # self.assertEqual(self.area_import_id.locked_reason, "Saving to Area.") + + # # Less than 400 rows + # self.area_import_id_2.import_data() + # self.area_import_id_2.validate_raw_data() + # self.area_import_id_2.save_to_area() + + # raw_data_ids = self.area_import_id_2.raw_data_ids + + # self.assertEqual(len(raw_data_ids.ids), self.area_import_id_2.tot_rows_imported) + # self.assertEqual(0, self.area_import_id_2.tot_rows_error) + # self.assertEqual(self.area_import_id_2.state, "Done") + # self.assertFalse(self.area_import_id_2.locked) + # self.assertFalse(self.area_import_id_2.locked_reason) + + # for raw_data_id in raw_data_ids: + # self.assertTrue( + # bool( + # self.env["spp.area"].search( + # [ + # ("draft_name", "=", raw_data_id.admin_name), + # ("code", "=", raw_data_id.admin_code), + # ], + # limit=1, + # ) + # ) + # ) + + def test_06_refresh_page(self): + action = self.area_import_id.refresh_page() + + self.assertEqual( + action, + { + "type": "ir.actions.client", + "tag": "reload", + }, + ) + + def test_07_async_mark_done(self): + self.area_import_id._async_mark_done() + + self.assertFalse(self.area_import_id.locked) + self.assertFalse(self.area_import_id.locked_reason) diff --git a/spp_area_base/tests/test_area_import_raw.py b/spp_area_base/tests/test_area_import_raw.py new file mode 100644 index 000000000..5019cdfbe --- /dev/null +++ b/spp_area_base/tests/test_area_import_raw.py @@ -0,0 +1,75 @@ +from .common import AreaImportTestMixin + + +class AreaImportRawTest(AreaImportTestMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.area_import_raw_id = cls.env["spp.area.import.raw"].create( + { + "area_import_id": cls.area_import_id.id, + "admin_name": "Philippines", + "admin_code": "PH", + "parent_name": "", + "parent_code": "", + "level": 0, + "area_sqkm": "194000.23", + } + ) + + cls.area_import_raw_child_id = cls.env["spp.area.import.raw"].create( + { + "area_import_id": cls.area_import_id.id, + "admin_name": "Manila", + "admin_code": "MNL", + "parent_name": "Philippines", + "parent_code": "PH", + "level": 1, + "area_sqkm": "200.23", + } + ) + + def test_01_validate_raw_data_no_error(self): + result = self.area_import_raw_id.validate_raw_data() + result_child = self.area_import_raw_child_id.validate_raw_data() + + self.assertFalse(result) + self.assertEqual(self.area_import_raw_id.state, "Validated") + self.assertEqual(self.area_import_raw_id.remarks, "No Error") + + self.assertFalse(result_child) + self.assertEqual(self.area_import_raw_child_id.state, "Validated") + self.assertEqual(self.area_import_raw_child_id.remarks, "No Error") + + def test_02_validate_raw_data_with_error(self): + self.area_import_raw_id.admin_name = "" + self.area_import_raw_id.area_sqkm = "text" + self.area_import_raw_id.parent_name = "MNL" + self.area_import_raw_child_id.parent_name = "" + + self.area_import_raw_id.validate_raw_data() + self.area_import_raw_child_id.validate_raw_data() + + self.assertEqual(self.area_import_raw_id.state, "Error") + self.assertIn("Name and Code of area is required.", self.area_import_raw_id.remarks) + self.assertIn("AREA_SQKM should be numerical.", self.area_import_raw_id.remarks) + self.assertIn( + "Level 0 area should not have a parent name and parent code.", + self.area_import_raw_id.remarks, + ) + + self.assertEqual(self.area_import_raw_child_id.state, "Error") + self.assertIn( + "Level 1 and above area should have a parent name and parent code.", + self.area_import_raw_child_id.remarks, + ) + + def test_03_save_to_area(self): + self.area_import_raw_id.area_sqkm = "" + + self.area_import_raw_id.save_to_area() + self.assertEqual(self.area_import_raw_id.state, "Posted") + + self.area_import_raw_id.save_to_area() + self.assertEqual(self.area_import_raw_id.state, "Updated") diff --git a/spp_area_base/views/area.xml b/spp_area_base/views/area.xml new file mode 100644 index 000000000..8cab508b4 --- /dev/null +++ b/spp_area_base/views/area.xml @@ -0,0 +1,196 @@ + + + view_spparea_tree + spp.area + 1 + + + + + + + + + + + + + + + view_spparea_form + spp.area + 100 + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + +
+
+
+
+ + + spparea_filter + spp.area + + + + + + + + + + + + + + + + Area + ir.actions.act_window + spp.area + tree,form + {} + [] + +

+ Create a new Area! +

+ Click the create button to enter the information of the Area. +

+
+
+ + + + tree + + + + + + + form + + + + + + + + + +
diff --git a/spp_area_base/views/area_import_views.xml b/spp_area_base/views/area_import_views.xml new file mode 100644 index 000000000..eb4245506 --- /dev/null +++ b/spp_area_base/views/area_import_views.xml @@ -0,0 +1,258 @@ + + + + + view_spparea_import_form + spp.area.import + 1 + +
+ +
+
+
+ Warning: Operation in progress: + +
+ +
+
+ + + + +
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+
+ +
+
+ + + spparea_import_filter + spp.area.import + + + + + + + + + + + + + + + + + + + + view_spparea_import_tree + spp.area.import + 1 + + + + + + + + + + + + + + + + Area Upload + ir.actions.act_window + spp.area.import + tree,form + + {} + [] + +

+ Upload an excel file! +

+ Click the create button to upload a new excel file. +

+
+
+ + + + tree + + + + + + + form + + + + + + +
diff --git a/spp_area_base/views/area_kind.xml b/spp_area_base/views/area_kind.xml new file mode 100644 index 000000000..88a5030e3 --- /dev/null +++ b/spp_area_base/views/area_kind.xml @@ -0,0 +1,58 @@ + + + view_spparea_kind_tree + spp.area.kind + 1 + + + + + + + + + + + spparea_kind_filter + spp.area.kind + + + + + + + + + + + Area Type + ir.actions.act_window + spp.area.kind + tree + {} + [] + +

+ Create a new Area Type! +

+ Click the create button to enter the information of the Area Type. +

+
+
+ + + + tree + + + + + +
From ea01aa99d4a436f528bed3f1a8d769df81fce463 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 10:29:36 +0800 Subject: [PATCH 02/22] [FIX] spp_area_base: Fix access rights issues. --- spp_area_base/readme/DESCRIPTION.md | 6 +++--- spp_area_base/security/ir.model.access.csv | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spp_area_base/readme/DESCRIPTION.md b/spp_area_base/readme/DESCRIPTION.md index 538064547..c09717721 100644 --- a/spp_area_base/readme/DESCRIPTION.md +++ b/spp_area_base/readme/DESCRIPTION.md @@ -1,10 +1,10 @@ -# OpenSPP Area +# OpenSPP Area (Base) -This document describes the **OpenSPP Area** module, which extends the OpenSPP framework by providing features to manage and organize geographical areas within the system. It integrates with the core registry modules to allow associating registrants and other data with specific locations. +This document describes the **OpenSPP Area (Base)** module, which extends the OpenSPP framework by providing features to manage and organize geographical areas within the system. It integrates with the core registry modules to allow associating registrants and other data with specific locations. ## Purpose -The **OpenSPP Area** module is designed to: +The **OpenSPP Area (Base)** module is designed to: * **Define and Structure Geographical Areas**: Establish a hierarchical structure for representing administrative regions, from the highest level (e.g., country) down to the most granular level (e.g., village). * **Manage Area Information**: Store key details about each area, including its name, code, alternate names, geographical size, and parent-child relationships within the hierarchy. diff --git a/spp_area_base/security/ir.model.access.csv b/spp_area_base/security/ir.model.access.csv index 561118591..d7e4c78ec 100644 --- a/spp_area_base/security/ir.model.access.csv +++ b/spp_area_base/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -spp_area_admin,Area Admin Access,spp_area.model_spp_area,base.group_system,1,1,1,1 -spp_area_import_sysadmin,Area Import Admin Access,spp_area.model_spp_area_import,base.group_system,1,1,1,1 -spp_area_import_raw_sysadmin,Area Import Raw Admin Access,spp_area.model_spp_area_import_raw,base.group_system,1,1,1,1 -spp_area_kind_sysadmin,Area Kind Admin Access,spp_area.model_spp_area_kind,base.group_system,1,1,1,1 +spp_area_admin,Area Admin Access,spp_area_base.model_spp_area,base.group_system,1,1,1,1 +spp_area_import_sysadmin,Area Import Admin Access,spp_area_base.model_spp_area_import,base.group_system,1,1,1,1 +spp_area_import_raw_sysadmin,Area Import Raw Admin Access,spp_area_base.model_spp_area_import_raw,base.group_system,1,1,1,1 +spp_area_kind_sysadmin,Area Kind Admin Access,spp_area_base.model_spp_area_kind,base.group_system,1,1,1,1 From 3d1bef9d439c5c48bb25683535826b246af437be Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 11:30:19 +0800 Subject: [PATCH 03/22] [FIX] spp_area_base: Modify the spp_area module to depend on the spp_area_base module. --- spp_area/__manifest__.py | 14 +- spp_area/data/area_kind_data.xml | 5 - spp_area/data/queue_job_channel.xml | 6 - spp_area/models/__init__.py | 2 - spp_area/models/area.py | 303 ---------- spp_area/models/area_import.py | 620 --------------------- spp_area/models/registrant.py | 2 +- spp_area/readme/DESCRIPTION.md | 2 +- spp_area/views/area.xml | 196 ------- spp_area/views/area_import_views.xml | 258 --------- spp_area/views/area_kind.xml | 58 -- spp_area_base/security/ir.model.access.csv | 2 +- 12 files changed, 6 insertions(+), 1462 deletions(-) delete mode 100644 spp_area/data/area_kind_data.xml delete mode 100644 spp_area/data/queue_job_channel.xml delete mode 100644 spp_area/models/area.py delete mode 100644 spp_area/models/area_import.py delete mode 100644 spp_area/views/area.xml delete mode 100644 spp_area/views/area_import_views.xml delete mode 100644 spp_area/views/area_kind.xml diff --git a/spp_area/__manifest__.py b/spp_area/__manifest__.py index c20a4a300..1796d61a8 100644 --- a/spp_area/__manifest__.py +++ b/spp_area/__manifest__.py @@ -3,7 +3,7 @@ { "name": "OpenSPP Area Management", - "summary": "This module enables management of geographical areas, linking them to registrants for targeted interventions and analysis in social protection programs.", + "summary": "This module extends the OpenSPP Area (Base) module to include additional features for managing and organizing geographical areas within the system.", "category": "OpenSPP", "version": "17.0.1.3.0", "sequence": 1, @@ -14,25 +14,17 @@ "maintainers": ["jeremi", "gonzalesedwin1123", "reichie020212"], "depends": [ "base", + "spp_area_base", "g2p_registry_base", "g2p_registry_individual", "g2p_registry_group", "queue_job", ], - "external_dependencies": { - "python": [ - "xlrd", - ] - }, + "external_dependencies": {}, "data": [ - "data/area_kind_data.xml", - "data/queue_job_channel.xml", "security/ir.model.access.csv", "views/individual_views.xml", "views/group_views.xml", - "views/area.xml", - "views/area_kind.xml", - "views/area_import_views.xml", ], "assets": {}, "demo": [], diff --git a/spp_area/data/area_kind_data.xml b/spp_area/data/area_kind_data.xml deleted file mode 100644 index 897a528a6..000000000 --- a/spp_area/data/area_kind_data.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - Admin Area - - diff --git a/spp_area/data/queue_job_channel.xml b/spp_area/data/queue_job_channel.xml deleted file mode 100644 index 3ac763ab9..000000000 --- a/spp_area/data/queue_job_channel.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - area_import - - - diff --git a/spp_area/models/__init__.py b/spp_area/models/__init__.py index 4a350927f..e182f176f 100644 --- a/spp_area/models/__init__.py +++ b/spp_area/models/__init__.py @@ -1,6 +1,4 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. -from . import area from . import registrant -from . import area_import diff --git a/spp_area/models/area.py b/spp_area/models/area.py deleted file mode 100644 index 8afe5c5d2..000000000 --- a/spp_area/models/area.py +++ /dev/null @@ -1,303 +0,0 @@ -# Part of OpenSPP. See LICENSE file for full copyright and licensing details. -import logging -import textwrap - -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError - -_logger = logging.getLogger(__name__) - - -class OpenSPPArea(models.Model): - """ - Represents an area in the OpenSPP system. - - This model defines the structure and behavior of geographical areas, - including their hierarchical relationships, names, codes, and other attributes. - """ - - _name = "spp.area" - _description = "Area" - _order = "id desc" - _parent_name = "parent_id" - _parent_store = True - _order = "parent_id,name" - - parent_id = fields.Many2one("spp.area", "Parent") - complete_name = fields.Char(compute="_compute_complete_name", recursive=True, translate=True) - name = fields.Char(translate=True, compute="_compute_name", store=True) - draft_name = fields.Char(required=True, translate=True) - parent_path = fields.Char(index=True, unaccent=False) - code = fields.Char() - altnames = fields.Char("Alternate Name") - level = fields.Integer(help="This is the area level for importing") - area_level = fields.Integer(compute="_compute_area_level", store=True, help="This is the main area level") - child_ids = fields.One2many("spp.area", "id", "Child", compute="_compute_get_childs") - kind = fields.Many2one("spp.area.kind") - area_sqkm = fields.Float("Area (sq/km)") - - _sql_constraints = [ - ( - "code_unique", - "unique (code)", - "Code is already exists!", - ) - ] - - @api.depends("draft_name", "code") - def _compute_name(self): - """ - Compute the name for the area to include the code. - - The name is set as a combination of the code (if present) and the draft name. - """ - for rec in self: - name = rec.draft_name or "" - - if rec.code: - name = f"{rec.code} - {name}" - - rec.name = name - - def _compute_get_childs(self): - """ - Compute the child areas of the current area. - - This method searches for all areas that have the current area as their parent - and assigns them to the child_ids field. - """ - for rec in self: - child_ids = self.env["spp.area"].search([("parent_id", "=", rec.id)]) - rec.child_ids = child_ids - - @api.depends("parent_id") - def _compute_area_level(self): - """ - Compute the area level based on the parent area. - - If the area has a parent, its level is set to the parent's level plus one. - If it doesn't have a parent, its level is set to 0. - """ - for rec in self: - if rec.parent_id: - rec.area_level = rec.parent_id.area_level + 1 - else: - rec.area_level = 0 - - @api.onchange("parent_id") - def _onchange_parent_id(self): - """ - Validate the area level when the parent area changes. - - Raises a ValidationError if the resulting area level would exceed 10. - """ - for rec in self: - if rec.area_level > 10: - raise ValidationError( - _( - textwrap.fill( - textwrap.dedent( - """Max level exceeded! Can't have area with level greater - than 10 and your current area is level %s.""" - % rec.area_level - ) - ) - ) - ) - - @api.depends("name", "parent_id.complete_name") - def _compute_complete_name(self): - """ - Compute the complete name of the area. - - The complete name includes the parent's complete name (if any) and the area's own name. - """ - for rec in self: - if rec.id: - if rec.parent_id: - rec.complete_name = f"{rec.parent_id.complete_name} > {rec.name}" - else: - rec.complete_name = rec.name - else: - rec.complete_name = None - - @api.model - def create(self, vals): - """ - Create a new area record. - - This method overrides the default create method to add additional validation - and translation handling. - - :param vals: The values for the new record. - :return: The newly created area record. - :raises ValidationError: If an area with the same name and code already exists. - """ - area_name = self.name - if "name" in vals: - area_name = vals["name"] - area_code = self.code - if "code" in vals: - area_code = vals["code"] - curr_area = self.env["spp.area"].search( - [ - ("name", "=", area_name), - ("code", "=", area_code), - ] - ) - - if curr_area: - raise ValidationError(_("Area already exist!")) - else: - Area = super().create(vals) - Languages = self.env["res.lang"].search([("active", "=", True)]) - vals_list = [] - for lang_code in Languages: - vals_list.append( - { - "name": "spp.area,draft_name", - "lang": lang_code.code, - "res_id": Area.id, - "src": Area.draft_name, - "value": None, - "state": "to_translate", - "type": "model", - } - ) - - return Area - - def write(self, vals): - """ - Update an existing area record. - - This method overrides the default write method to add additional validation. - - :param vals: The values to update. - :return: The result of the write operation. - :raises ValidationError: If an area with the same name and code already exists. - """ - for rec in self: - area_name = rec.name - if "name" in vals: - area_name = vals["name"] - area_code = rec.code - if "code" in vals: - area_code = vals["code"] - curr_area = self.env["spp.area"].search( - [ - ("name", "=", area_name), - ("code", "=", area_code), - ("id", "!=", rec.id), - ] - ) - if curr_area: - raise ValidationError(_("Area already exist!")) - else: - return super().write(vals) - - def open_area_form(self): - """ - Open the form view of the area. - - :return: A dictionary containing the action to open the area form view. - """ - for rec in self: - return { - "name": "Area", - "view_mode": "form", - "res_model": "spp.area", - "res_id": rec.id, - "view_id": self.env.ref("spp_area.view_spparea_form").id, - "type": "ir.actions.act_window", - "target": "new", - "flags": {"mode": "readonly"}, - } - - -class OpenSPPAreaKind(models.Model): - """ - Represents the type or kind of an area in the OpenSPP system. - - This model defines the structure and behavior of area types, including - their hierarchical relationships and names. - """ - - _name = "spp.area.kind" - _description = "Area Type" - _parent_name = "parent_id" - _parent_store = True - _rec_name = "complete_name" - _order = "parent_id,name" - - parent_id = fields.Many2one("spp.area.kind", "Parent") - parent_path = fields.Char(index=True) - name = fields.Char(required=True) - complete_name = fields.Char(compute="_compute_complete_name", recursive=True, translate=True) - - @api.depends("name", "parent_id.complete_name") - def _compute_complete_name(self): - """ - Compute the complete name of the area type. - - The complete name includes the parent's complete name (if any) and the area type's own name. - """ - for rec in self: - if rec.id: - if rec.parent_id: - rec.complete_name = f"{rec.parent_id.complete_name} > {rec.name}" - else: - rec.complete_name = rec.name - else: - rec.complete_name = None - - def unlink(self): - """ - Delete an area type record. - - This method overrides the default unlink method to add additional validation. - It prevents the deletion of default area types and area types that are in use. - - :raises ValidationError: If trying to delete a default area type or an area type in use. - """ - for rec in self: - external_identifier = self.env["ir.model.data"].search( - [("res_id", "=", rec.id), ("model", "=", "spp.area.kind")] - ) - if external_identifier and external_identifier.name: - raise ValidationError(_("Can't delete default Area Type")) - else: - areas = self.env["spp.area"].search([("kind", "=", rec.id)]) - if areas: - raise ValidationError(_("Can't delete used Area Type")) - else: - return super().unlink() - - def write(self, vals): - """ - Update an existing area type record. - - This method overrides the default write method to prevent editing of default area types. - - :param vals: The values to update. - :return: The result of the write operation. - """ - for rec in self: - external_identifier = self.env["ir.model.data"].search( - [("res_id", "=", rec.id), ("model", "=", "spp.area.kind")] - ) - if external_identifier and external_identifier.name: - vals = {} - return super().write(vals) - - def update_parent(self): - for rec in self: - if rec.parent_name and rec.parent_code: - parent_id = self.env["spp.area.kind"].search( - [ - ("code", "=", rec.parent_code), - ], - limit=1, - ) - if parent_id: - rec.parent_id = parent_id.id diff --git a/spp_area/models/area_import.py b/spp_area/models/area_import.py deleted file mode 100644 index 4641ce13c..000000000 --- a/spp_area/models/area_import.py +++ /dev/null @@ -1,620 +0,0 @@ -# Part of OpenSPP. See LICENSE file for full copyright and licensing details. -import base64 -import logging -import math -from io import BytesIO - -from xlrd import open_workbook - -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError - -from odoo.addons.queue_job.delay import group - -_logger = logging.getLogger(__name__) - - -class OpenSPPAreaImport(models.Model): - _name = "spp.area.import" - _description = "Areas Import Table" - - MIN_ROW_JOB_QUEUE = 400 - - NEW = "New" - UPLOADED = "Uploaded" - IMPORTED = "Imported" - VALIDATED = "Validated" - DONE = "Done" - CANCELLED = "Cancelled" - - STATE_SELECTION = [ - (NEW, NEW), - (UPLOADED, UPLOADED), - (IMPORTED, IMPORTED), - (VALIDATED, VALIDATED), - (DONE, DONE), - (CANCELLED, CANCELLED), - ] - - name = fields.Char("File Name", required=True, translate=True) - excel_file = fields.Binary("Area Excel File") - date_uploaded = fields.Datetime() - - upload_id = fields.Many2one("res.users", "Uploaded by") - date_imported = fields.Datetime() - import_id = fields.Many2one("res.users", "Imported by") - date_validated = fields.Datetime() - validate_id = fields.Many2one("res.users", "Validated by") - raw_data_ids = fields.One2many("spp.area.import.raw", "area_import_id", "Raw Data") - tot_rows_imported = fields.Integer( - "Total Rows Imported", - compute="_compute_get_total_rows", - store=True, - readonly=True, - ) - tot_rows_error = fields.Integer( - "Total Rows with Error", - compute="_compute_get_total_rows", - store=True, - readonly=True, - ) - state = fields.Selection( - STATE_SELECTION, - "Status", - default=NEW, - ) - - locked = fields.Boolean(default=False) - locked_reason = fields.Char(readonly=True) - - @api.onchange("excel_file") - def excel_file_change(self): - """ - The above function is an onchange function in Python that updates the date_uploaded, upload_id, - and state fields based on the value of the excel_file field. - """ - - if self.name: - self.update( - { - "date_uploaded": fields.Datetime.now(), - "upload_id": self.env.user, - "state": self.UPLOADED, - } - ) - else: - self.update({"date_uploaded": None, "upload_id": None, "state": self.UPLOADED}) - - @api.depends("raw_data_ids", "raw_data_ids.state") - def _compute_get_total_rows(self): - """ - The function `_compute_get_total_rows` calculates the total number of imported rows and the - total number of rows with an error for a given record. - """ - for rec in self: - tot_rows_imported = len(rec.raw_data_ids) - tot_rows_error = self.env["spp.area.import.raw"].search( - [("id", "in", rec.raw_data_ids.ids), ("state", "=", "Error")] - ) - rec.update( - { - "tot_rows_imported": tot_rows_imported, - "tot_rows_error": len(tot_rows_error), - } - ) - - def cancel_import(self): - """ - The function cancels the import by updating the state of the record to "Cancelled". - """ - for rec in self: - rec.update({"state": self.CANCELLED}) - - def reset_to_uploaded(self): - """ - The function resets the state of a record to "Uploaded". - """ - for rec in self: - rec.update({"state": self.UPLOADED}) - - def get_column_indexes(self, columns, area_level): - self.ensure_one() - default_lang = self.env.context.get("lang", "en_US") - if default_lang not in columns: - default_lang = "en_US" - default_iso_code = default_lang.split("_")[0].upper() - - active_languages = self.env["res.lang"].search([("active", "=", True)]) - if not active_languages: - raise ValidationError(_("No active language found.")) - - # Get column prefix and the language iso code used in the name header - lang_codes = active_languages.read(fields=["code", "iso_code"]) - column_name_prefix = f"ADM{area_level}" - - # Get Column name to be used as name field in the area - name_headers = {code["code"]: f"{column_name_prefix}_{code['iso_code'].upper()}" for code in lang_codes} - - # Get Column name to be used as code field in the area - code_header = f"{column_name_prefix}_PCODE" - - # get name and code column indexes - name_indexes = {} - for name_header in name_headers: - try: - name_indexes.update({name_header: columns.index(name_headers[name_header])}) - except ValueError as e: - _logger.warning("Column header not found: %s", e) - code_index = columns.index(code_header) - - # Get index of the Parent header of the area if area level is not 0 - parent_name_index = None - parent_code_index = None - if area_level != 0: - parent_name_header = f"{column_name_prefix[:3]}{area_level - 1}_{default_iso_code}" - parent_code_header = f"{column_name_prefix[:3]}{area_level - 1}_PCODE" - - parent_name_index = columns.index(parent_name_header) - parent_code_index = columns.index(parent_code_header) - - # Get area_sqkm column index - area_sqkm_index = None - if "AREA_SQKM" in columns: - area_sqkm_index = columns.index("AREA_SQKM") - - return { - "name_indexes": name_indexes, - "code_index": code_index, - "parent_name_index": parent_name_index, - "parent_code_index": parent_code_index, - "area_sqkm_index": area_sqkm_index, - } - - def get_area_vals(self, column_indexes, row, sheet, area_level): - self.ensure_one() - default_lang = self.env.context.get("lang", "en_US") - if default_lang not in column_indexes["name_indexes"]: - default_lang = "en_US" - vals = { - "admin_name": sheet.cell(row, column_indexes["name_indexes"][default_lang]).value, - "admin_code": sheet.cell(row, column_indexes["code_index"]).value, - "parent_name": "", - "parent_code": "", - "level": area_level, - "area_import_id": self.id, - } - if column_indexes["area_sqkm_index"]: - vals["area_sqkm"] = sheet.cell(row, column_indexes["area_sqkm_index"]).value - - if column_indexes["parent_name_index"] is not None and column_indexes["parent_code_index"] is not None: - vals["parent_name"] = sheet.cell(row, column_indexes["parent_name_index"]).value - vals["parent_code"] = sheet.cell(row, column_indexes["parent_code_index"]).value - - return vals - - def create_import_raw(self, vals, column_indexes, row, sheet): - self.ensure_one() - import_raw_id = self.env["spp.area.import.raw"].create(vals) - for lang_code in column_indexes["name_indexes"]: - lang_name = sheet.cell(row, column_indexes["name_indexes"][lang_code]).value - import_raw_id.with_context(lang=lang_code).write( - { - "admin_name": lang_name, - } - ) - - def _get_book(self): - self.ensure_one() - try: - inputx = BytesIO() - inputx.write(base64.decodebytes(self.excel_file)) - except TypeError as e: - raise ValidationError(_("ERROR: {}").format(e)) from e - return open_workbook(file_contents=inputx.getvalue()) - - def check_all_languages_activated(self, columns, area_level): - """Check if all languages in the specified columns are activated. - - Args: - columns (list): The list of column names to check. - area_level (int): The administrative area level to check within the column names. - - Raises: - ValidationError: If any language is not active. - """ - self.ensure_one() - prefix = f"ADM{area_level}_" - active_langs = self.env["res.lang"].search([("active", "=", True)]).mapped("iso_code") - - for col in columns: - if col.startswith(prefix): - lang = col.split("_", 1)[1] - if len(lang) == 2 and lang.lower() not in active_langs: - raise ValidationError( - _( - "Language with ISO Code %s is not active.\n" - "Please request the administrator to enable the desired language." - ) - % lang.upper() - ) - - def import_data(self): - self.ensure_one() - - _logger.info("Area Import: Started: %s" % fields.Datetime.now()) - # Delete all existing import data for this record - # This can only be happen if the Area Upload record is reset back to Uploaded state - if self.raw_data_ids: - self.raw_data_ids.unlink() - - _logger.info("Area Import: Loading Excel File: %s" % fields.Datetime.now()) - # Wrap binary to BytesIO - - book = self._get_book() - - sheet_names = book.sheet_names() - sheet_names.sort() - self.locked = True - self.locked_reason = _("Importing data.") - - jobs = [] - - for area_level, sheet_name in enumerate(sheet_names): - sheet = book.sheet_by_name(sheet_name) - columns = sheet.row_values(0) - self.check_all_languages_activated(columns, area_level) - column_indexes = self.get_column_indexes(columns, area_level) - - batches = math.ceil(sheet.nrows / 1000) - for i in range(batches): - if i == 0: - start = 1 - else: - start = i * 1000 - end = min((i + 1) * 1000, sheet.nrows) - jobs.append( - self.delayable(channel="root.area_import")._import_data( - sheet_name, column_indexes, start, end, area_level - ) - ) - - main_job = group(*jobs) - - main_job.on_done(self.delayable(channel="root.area_import")._async_mark_done()) - main_job.delay() - - def _import_data(self, sheet_name, column_indexes, start, end, area_level): - """ - The `import_data` function imports data from an Excel file, processes it, and updates the record - with the imported data. - """ - self.ensure_one() - - book = self._get_book() - - sheet = book.sheet_by_name(sheet_name) - for row in range(start, end): - import_raw_vals = self.get_area_vals(column_indexes, row, sheet, area_level) - self.create_import_raw(import_raw_vals, column_indexes, row, sheet) - - self.update( - { - "date_imported": fields.Datetime.now(), - "import_id": self.env.user, - "date_validated": fields.Datetime.now(), - "validate_id": self.env.user, - "state": self.IMPORTED, - } - ) - - def validate_raw_data(self): - """ - The function iterates through a collection of records and checks if the count of raw data is - less than a minimum threshold, and if so, it calls a validation function, otherwise it calls an - """ - for rec in self: - rec.locked = True - rec.locked_reason = _("Validating data.") - batches = math.ceil(len(rec.raw_data_ids) / 1000) - jobs = [] - for i in range(batches): - start = i * 1000 - end = min((i + 1) * 1000, len(rec.raw_data_ids)) - jobs.append(rec.delayable(channel="root.area_import")._validate_raw_data(rec.raw_data_ids[start:end])) - main_job = group(*jobs) - main_job.on_done(rec.delayable(channel="root.area_import")._validate_mark_done()) - main_job.delay() - - def _validate_raw_data(self, raw_data_ids): - """ - The function validates raw data and updates the state if there are no errors. - """ - self.ensure_one() - raw_data_ids.validate_raw_data() - - def _validate_mark_done(self): - self.locked = False - self.locked_reason = None - self.ensure_one() - if not self.env["spp.area.import.raw"].search([("id", "in", self.raw_data_ids.ids), ("state", "=", "Error")]): - self.update( - { - "state": self.VALIDATED, - } - ) - - def fix_area_level_and_kind(self): - for rec in self: - rec.locked = True - rec.locked_reason = _("Fixing area level.") - batches = math.ceil(len(rec.raw_data_ids) / 1000) - jobs = [] - for i in range(batches): - start = i * 1000 - end = min((i + 1) * 1000, len(rec.raw_data_ids)) - jobs.append( - rec.delayable(channel="root.area_import")._fix_area_level_and_kind(rec.raw_data_ids[start:end]) - ) - main_job = group(*jobs) - main_job.on_done(rec.delayable(channel="root.area_import")._async_mark_done()) - main_job.delay() - - def _fix_area_level_and_kind(self, raw_data_ids): - """ - The function `fix_area_level_and_kind` fixes the area level of the raw data. - """ - self.ensure_one() - raw_data_ids.fix_area_level_and_kind() - - def _async_mark_done(self, function_mark_done=None): - """ - The function `_async_mark_done` unlocks a resource by setting the `locked` attribute to `False` - and clearing the `locked_reason` attribute. - """ - self.ensure_one() - - self.locked = False - self.locked_reason = None - - if function_mark_done: - getattr(self, function_mark_done)() - - def save_to_area(self): - """ - The function saves data to an area, either synchronously or asynchronously depending on the - number of raw data records. - """ - for rec in self: - rec.locked = True - rec.locked_reason = _("Importing data.") - rec._async_recursive_save_to_area(rec.raw_data_ids) - - def _async_recursive_save_to_area(self, raw_data_ids): - """ - This is to ensure that the function `_save_to_area` is called recursively and in order until all raw data - is saved to the area. - """ - self.ensure_one() - jobs = [] - jobs.append(self.delayable(channel="root.area_import")._save_to_area(raw_data_ids[:1000])) - main_job = group(*jobs) - count = len(raw_data_ids) - if count <= 1000: - main_job.on_done(self.delayable(channel="root.area_import")._save_to_area_mark_done()) - else: - main_job.on_done( - self.delayable(channel="root.area_import")._async_recursive_save_to_area(raw_data_ids[1000:]) - ) - main_job.delay() - - def _save_to_area(self, raw_data_ids): - """ - The function saves raw data to an area and updates the state to "DONE". - """ - self.ensure_one() - - raw_data_ids.save_to_area() - - def _save_to_area_mark_done(self): - self.ensure_one() - self.locked = False - self.locked_reason = None - if not self.env["spp.area.import.raw"].search( - [("id", "in", self.raw_data_ids.ids), ("state", "=", "Validated")] - ): - self.update( - { - "state": self.DONE, - } - ) - - def refresh_page(self): - """ - The function `refresh_page` returns a dictionary with the type and tag values to reload the - page. - :return: The code is returning a dictionary with two key-value pairs. The "type" key has the - value "ir.actions.client" and the "tag" key has the value "reload". - """ - return { - "type": "ir.actions.client", - "tag": "reload", - } - - -# Assets Import Raw Data -class OpenSPPAreaImportActivities(models.Model): - _name = "spp.area.import.raw" - _description = "Area Import Raw Data" - _order = "level" - - NEW = "New" - VALIDATED = "Validated" - ERROR = "Error" - UPDATED = "Updated" - POSTED = "Posted" - - STATE_CHOICES = [ - (NEW, NEW), - (VALIDATED, VALIDATED), - (ERROR, ERROR), - (UPDATED, UPDATED), - (POSTED, POSTED), - ] - - STATE_ORDER = { - ERROR: 0, - NEW: 1, - VALIDATED: 2, - UPDATED: 3, - POSTED: 4, - } - - area_import_id = fields.Many2one("spp.area.import", "Area Import", required=True) - admin_name = fields.Char(translate=True) - admin_code = fields.Char() - - parent_name = fields.Char() - parent_code = fields.Char() - - level = fields.Integer() - - area_sqkm = fields.Char("Area (sq/km)") - - remarks = fields.Text("Remarks/Errors") - state = fields.Selection( - STATE_CHOICES, - "Status", - default="New", - ) - state_order = fields.Integer( - compute="_compute_state_order", - store=True, - ) - area_id = fields.Many2one("spp.area", "Area", readonly=True) - - @api.depends("state") - def _compute_state_order(self): - for rec in self: - rec.state_order = self.STATE_ORDER[rec.state] - - def check_errors(self): - self.ensure_one() - errors = [] - if not self.admin_name or not self.admin_code: - errors.append(_("Name and Code of area is required.")) - - if self.area_sqkm: - try: - float(self.area_sqkm) - except ValueError: - errors.append(_("AREA_SQKM should be numerical.")) - - if self.level == 0 and (self.parent_name or self.parent_code): - errors.append(_("Level 0 area should not have a parent name and parent code.")) - - if self.level != 0 and (not self.parent_name or not self.parent_code): - errors.append(_("Level 1 and above area should have a parent name and parent code.")) - return errors - - def validate_raw_data(self): - for rec in self: - errors = rec.check_errors() - - if errors: - state = self.ERROR - remarks = "\n".join(errors) - else: - state = self.VALIDATED - remarks = "No Error" - - rec.write( - { - "remarks": remarks, - "state": state, - } - ) - - def get_area_vals(self): - self.ensure_one() - - parent_id = None - if self.parent_name and self.parent_code: - parent_id = ( - self.env["spp.area"] - .search( - [ - ("code", "=", self.parent_code), - ], - limit=1, - ) - .id - ) - - area_sqkm = self.area_sqkm - - try: - area_sqkm = float(area_sqkm) - except ValueError: - area_sqkm = 0.0 - - return { - "parent_id": parent_id, - "draft_name": self.admin_name, - "code": self.admin_code, - "area_sqkm": area_sqkm, - "kind": self.env.ref("spp_area.admin_area_kind").id, - } - - def save_to_area(self): - """ - The function saves data to the "spp.area" model in the database, updating existing records if - they exist and creating new records if they don't. - """ - active_languages = self.env["res.lang"].search([("active", "=", True)]) - for rec in self: - area_vals = rec.get_area_vals() - if area_id := self.env["spp.area"].search([("code", "=", rec.admin_code)]): - state = self.UPDATED - area_id.update(area_vals) - else: - state = self.POSTED - area_id = self.env["spp.area"].create(area_vals) - - for lang in active_languages: - area_id.with_context(lang=lang.code).write( - { - "draft_name": rec.with_context(lang=lang.code).admin_name, - } - ) - area_id.with_context(lang=lang.code)._compute_name() - area_id.with_context(lang=lang.code)._compute_complete_name() - - rec.update( - { - "state": state, - "remarks": "Successfully save to Area", - "area_id": area_id.id, - } - ) - - def fix_area_level_and_kind(self): - for rec in self: - if rec.area_id and (rec.area_id.area_level != rec.level or not rec.area_id.kind): - parent_id = None - if rec.parent_name and rec.parent_code: - parent_id = ( - self.env["spp.area"] - .search( - [ - ("code", "=", rec.parent_code), - ], - limit=1, - ) - .id - ) - rec.area_id.update( - { - "kind": rec.env.ref("spp_area.admin_area_kind").id, - "parent_id": parent_id, - } - ) diff --git a/spp_area/models/registrant.py b/spp_area/models/registrant.py index 654097a87..648326548 100644 --- a/spp_area/models/registrant.py +++ b/spp_area/models/registrant.py @@ -10,7 +10,7 @@ def _get_area_domain(self): """ This set up the domain of the area base on its kind """ - area_id = self.env.ref("spp_area.admin_area_kind").id + area_id = self.env.ref("spp_area_base.admin_area_kind").id return [("kind", "=", area_id)] # Custom Fields diff --git a/spp_area/readme/DESCRIPTION.md b/spp_area/readme/DESCRIPTION.md index 538064547..e3dc4d7a2 100644 --- a/spp_area/readme/DESCRIPTION.md +++ b/spp_area/readme/DESCRIPTION.md @@ -1,6 +1,6 @@ # OpenSPP Area -This document describes the **OpenSPP Area** module, which extends the OpenSPP framework by providing features to manage and organize geographical areas within the system. It integrates with the core registry modules to allow associating registrants and other data with specific locations. +This document describes the **OpenSPP Area** module, which extends the OpenSPP Area Base module. ## Purpose diff --git a/spp_area/views/area.xml b/spp_area/views/area.xml deleted file mode 100644 index a2518bfd3..000000000 --- a/spp_area/views/area.xml +++ /dev/null @@ -1,196 +0,0 @@ - - - view_spparea_tree - spp.area - 1 - - - - - - - - - - - - - - - view_spparea_form - spp.area - 100 - -
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - -
-
-
-
- - - spparea_filter - spp.area - - - - - - - - - - - - - - - - Area - ir.actions.act_window - spp.area - tree,form - {} - [] - -

- Create a new Area! -

- Click the create button to enter the information of the Area. -

-
-
- - - - tree - - - - - - - form - - - - - - - - - -
diff --git a/spp_area/views/area_import_views.xml b/spp_area/views/area_import_views.xml deleted file mode 100644 index 7e3992e80..000000000 --- a/spp_area/views/area_import_views.xml +++ /dev/null @@ -1,258 +0,0 @@ - - - - - view_spparea_import_form - spp.area.import - 1 - -
- -
-
-
- Warning: Operation in progress: - -
- -
-
- - - - -
-

- -

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - - - - -
-
- -
-
- - - spparea_import_filter - spp.area.import - - - - - - - - - - - - - - - - - - - - view_spparea_import_tree - spp.area.import - 1 - - - - - - - - - - - - - - - - Area Upload - ir.actions.act_window - spp.area.import - tree,form - - {} - [] - -

- Upload an excel file! -

- Click the create button to upload a new excel file. -

-
-
- - - - tree - - - - - - - form - - - - - - -
diff --git a/spp_area/views/area_kind.xml b/spp_area/views/area_kind.xml deleted file mode 100644 index 0ea689c16..000000000 --- a/spp_area/views/area_kind.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - view_spparea_kind_tree - spp.area.kind - 1 - - - - - - - - - - - spparea_kind_filter - spp.area.kind - - - - - - - - - - - Area Type - ir.actions.act_window - spp.area.kind - tree - {} - [] - -

- Create a new Area Type! -

- Click the create button to enter the information of the Area Type. -

-
-
- - - - tree - - - - - -
diff --git a/spp_area_base/security/ir.model.access.csv b/spp_area_base/security/ir.model.access.csv index d7e4c78ec..9c5494fc8 100644 --- a/spp_area_base/security/ir.model.access.csv +++ b/spp_area_base/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -spp_area_admin,Area Admin Access,spp_area_base.model_spp_area,base.group_system,1,1,1,1 +spp_area_sysadmin,Area Admin Access,spp_area_base.model_spp_area,base.group_system,1,1,1,1 spp_area_import_sysadmin,Area Import Admin Access,spp_area_base.model_spp_area_import,base.group_system,1,1,1,1 spp_area_import_raw_sysadmin,Area Import Raw Admin Access,spp_area_base.model_spp_area_import_raw,base.group_system,1,1,1,1 spp_area_kind_sysadmin,Area Kind Admin Access,spp_area_base.model_spp_area_kind,base.group_system,1,1,1,1 From 2ffb4cf3676348744cd82b2fa6fbd797db860ba7 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 11:42:05 +0800 Subject: [PATCH 04/22] [FIX] spp_area: Fix access rights. --- spp_area/security/ir.model.access.csv | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spp_area/security/ir.model.access.csv b/spp_area/security/ir.model.access.csv index 0e8ecdf95..ed816fcc3 100644 --- a/spp_area/security/ir.model.access.csv +++ b/spp_area/security/ir.model.access.csv @@ -1,7 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -spp_area_admin,Area Admin Access,spp_area.model_spp_area,g2p_registry_base.group_g2p_admin,1,1,1,1 -spp_area_import_admin,Area Import Admin Access,spp_area.model_spp_area_import,g2p_registry_base.group_g2p_admin,1,1,1,1 -spp_area_import_raw_admin,Area Import Raw Admin Access,spp_area.model_spp_area_import_raw,g2p_registry_base.group_g2p_admin,1,1,1,1 -spp_area_kind_admin,Area Kind Admin Access,spp_area.model_spp_area_kind,g2p_registry_base.group_g2p_admin,1,1,1,1 +spp_area_admin,Area Admin Access,spp_area_base.model_spp_area,g2p_registry_base.group_g2p_admin,1,1,1,1 +spp_area_import_admin,Area Import Admin Access,spp_area_base.model_spp_area_import,g2p_registry_base.group_g2p_admin,1,1,1,1 +spp_area_import_raw_admin,Area Import Raw Admin Access,spp_area_base.model_spp_area_import_raw,g2p_registry_base.group_g2p_admin,1,1,1,1 +spp_area_kind_admin,Area Kind Admin Access,spp_area_base.model_spp_area_kind,g2p_registry_base.group_g2p_admin,1,1,1,1 -spp_area_registrar,Area Registrar Access,spp_area.model_spp_area,g2p_registry_base.group_g2p_registrar,1,1,1,0 +spp_area_registrar,Area Registrar Access,spp_area_base.model_spp_area,g2p_registry_base.group_g2p_registrar,1,1,1,0 From 84e49ec7ea6d277ea5d721ae84f6db3ecf269783 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 12:21:35 +0800 Subject: [PATCH 05/22] [FIX] spp_area_gis: Fix references to spp_area_base views. --- spp_area_gis/views/area.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spp_area_gis/views/area.xml b/spp_area_gis/views/area.xml index 964aa86ad..04e86f61c 100644 --- a/spp_area_gis/views/area.xml +++ b/spp_area_gis/views/area.xml @@ -3,7 +3,7 @@ custom_view_spparea_form spp.area - + @@ -60,7 +60,7 @@ - + tree,form,gis From 15da9b5a6ca5ed6a10748c968be1b61c7bc32114 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 12:43:37 +0800 Subject: [PATCH 06/22] [FIX] spp_area_base: Modify the DESCRIPTION.md. --- spp_area_base/readme/DESCRIPTION.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spp_area_base/readme/DESCRIPTION.md b/spp_area_base/readme/DESCRIPTION.md index c09717721..4e61ab52f 100644 --- a/spp_area_base/readme/DESCRIPTION.md +++ b/spp_area_base/readme/DESCRIPTION.md @@ -12,13 +12,7 @@ The **OpenSPP Area (Base)** module is designed to: ## Dependencies and Integration -1. **G2P Registry: Base ([g2p_registry_base](g2p_registry_base))**: The Area module utilizes the **Districts (g2p.district)** feature from the **G2P Registry: Base** module as a foundation. It extends this concept to create a more comprehensive and flexible system for managing area data. - -2. **G2P Registry: Individual ([g2p_registry_individual](g2p_registry_individual))**: Integrates with the Individual module by adding a dedicated "Area" field to the individual registrant form. This field allows users to assign a specific area to each individual, linking registrant data to geographical locations. - -3. **G2P Registry: Group ([g2p_registry_group](g2p_registry_group))**: Similar to the Individual module integration, this module incorporates an "Area" field into the group registrant form, enabling the association of groups with specific areas. - -4. **Queue Job ([queue_job](queue_job))**: Leverages the **Queue Job** module for background processing of large data imports, improving performance and user experience. This is particularly beneficial when importing extensive area hierarchies from external sources. +1. **Queue Job ([queue_job](queue_job))**: Leverages the **Queue Job** module for background processing of large data imports, improving performance and user experience. This is particularly beneficial when importing extensive area hierarchies from external sources. ## Additional Functionality From d473e5ab1a9ba19fa794d4a3b28f92c69e3c29eb Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 12:57:35 +0800 Subject: [PATCH 07/22] [FIX] spp_change_request: Update the reference to the spp.area model in the ir.model.access.csv --- spp_change_request/security/ir.model.access.csv | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spp_change_request/security/ir.model.access.csv b/spp_change_request/security/ir.model.access.csv index 19719064a..bf690a71d 100644 --- a/spp_change_request/security/ir.model.access.csv +++ b/spp_change_request/security/ir.model.access.csv @@ -26,7 +26,7 @@ spp_change_request_validation_stage_validator,Change Request Validation Stage Va spp_change_request_group_members_validator,Change Request Group Membership Validator Access,spp_change_request.model_spp_change_request_group_members,spp_change_request.group_spp_change_request_validator,1,0,0,0 spp_change_request_user_assign_wizard_validator,Change Request User Assignment Validator Access,spp_change_request.model_spp_change_request_user_assign_wizard,spp_change_request.group_spp_change_request_validator,1,1,1,1 spp_change_request_reject_wizard_validator,Change Request Reject Validator Access,spp_change_request.model_spp_change_request_reject_wizard,spp_change_request.group_spp_change_request_validator,1,1,1,1 -spp_change_request_area_validator,SPP-CR Area Validator Access,spp_area.model_spp_area,spp_change_request.group_spp_change_request_validator,1,0,0,0 +spp_change_request_area_validator,SPP-CR Area Validator Access,spp_area_base.model_spp_area,spp_change_request.group_spp_change_request_validator,1,0,0,0 spp_change_request_service_point_validator,SPP-CR Service Point Validator Access,spp_service_points.model_spp_service_point,spp_change_request.group_spp_change_request_validator,1,0,0,0 spp_change_request_g2p_group_membership_validator,G2P Group Membership Validator Access,g2p_registry_membership.model_g2p_group_membership,spp_change_request.group_spp_change_request_validator,1,1,1,0 spp_change_request_g2p_group_membership_kind_validator,G2P Group Membership Kind Validator Access,g2p_registry_membership.model_g2p_group_membership_kind,spp_change_request.group_spp_change_request_validator,1,0,0,0 @@ -52,7 +52,7 @@ spp_change_request_validation_stage_hq_validator,Change Request HQ Validator Sta spp_change_request_group_members_hq_validator,Change Request Group Membership HQ Validator Access,spp_change_request.model_spp_change_request_group_members,spp_change_request.group_spp_change_request_hq_validator,1,0,0,0 spp_change_request_user_assign_wizard_hq_validator,Change Request User Assignment HQ Validator Access,spp_change_request.model_spp_change_request_user_assign_wizard,spp_change_request.group_spp_change_request_hq_validator,1,1,1,1 spp_change_request_reject_wizard_hq_validator,Change Request Reject HQ Validator Access,spp_change_request.model_spp_change_request_reject_wizard,spp_change_request.group_spp_change_request_hq_validator,1,1,1,1 -spp_change_request_area_hq_validator,SPP-CR Area HQ Validator Access,spp_area.model_spp_area,spp_change_request.group_spp_change_request_hq_validator,1,1,0,0 +spp_change_request_area_hq_validator,SPP-CR Area HQ Validator Access,spp_area_base.model_spp_area,spp_change_request.group_spp_change_request_hq_validator,1,1,0,0 spp_change_request_service_point_hq_validator,SPP-CR Service Point HQ Validator Access,spp_service_points.model_spp_service_point,spp_change_request.group_spp_change_request_hq_validator,1,0,0,0 spp_change_request_g2p_group_membership_hq_validator,G2P Group Membership HQ Validator Access,g2p_registry_membership.model_g2p_group_membership,spp_change_request.group_spp_change_request_hq_validator,1,1,1,0 spp_change_request_g2p_group_membership_kind_hq_validator,G2P Group Membership Kind HQ Validator Access,g2p_registry_membership.model_g2p_group_membership_kind,spp_change_request.group_spp_change_request_hq_validator,1,0,0,0 @@ -78,7 +78,7 @@ spp_change_request_validation_stage_administrator,Change Request Validation Stag spp_change_request_group_members_administrator,Change Request Group Membership Administrator Access,spp_change_request.model_spp_change_request_group_members,spp_change_request.group_spp_change_request_administrator,1,1,1,1 spp_change_request_user_assign_wizard_administrator,Change Request User Assignment Administrator Access,spp_change_request.model_spp_change_request_user_assign_wizard,spp_change_request.group_spp_change_request_administrator,1,1,1,1 spp_change_request_reject_wizard_administrator,Change Request Reject Administrator Access,spp_change_request.model_spp_change_request_reject_wizard,spp_change_request.group_spp_change_request_administrator,1,1,1,1 -spp_change_request_area_administrator,SPP-CR Area Administrator Access,spp_area.model_spp_area,spp_change_request.group_spp_change_request_administrator,1,0,0,0 +spp_change_request_area_administrator,SPP-CR Area Administrator Access,spp_area_base.model_spp_area,spp_change_request.group_spp_change_request_administrator,1,0,0,0 spp_change_request_service_point_administrator,SPP-CR Service Point Administrator Access,spp_service_points.model_spp_service_point,spp_change_request.group_spp_change_request_administrator,1,0,0,0 spp_change_request_g2p_group_membership_administrator,G2P Group Membership Administrator Access,g2p_registry_membership.model_g2p_group_membership,spp_change_request.group_spp_change_request_administrator,1,1,1,0 spp_change_request_g2p_group_membership_kind_administrator,G2P Group Membership Kind Administrator Access,g2p_registry_membership.model_g2p_group_membership_kind,spp_change_request.group_spp_change_request_administrator,1,0,0,0 @@ -104,7 +104,7 @@ spp_change_request_validation_stage_applicator,Change Request Validation Stage A spp_change_request_group_members_applicator,Change Request Group Membership Applicator Access,spp_change_request.model_spp_change_request_group_members,spp_change_request.group_spp_change_request_applicator,1,0,0,0 spp_change_request_user_assign_wizard_applicator,Change Request User Assignment Applicator Access,spp_change_request.model_spp_change_request_user_assign_wizard,spp_change_request.group_spp_change_request_applicator,1,1,1,1 spp_change_request_reject_wizard_applicator,Change Request Reject Applicator Access,spp_change_request.model_spp_change_request_reject_wizard,spp_change_request.group_spp_change_request_applicator,1,1,1,1 -spp_change_request_area_applicator,SPP-CR Area Applicator Access,spp_area.model_spp_area,spp_change_request.group_spp_change_request_applicator,1,1,0,0 +spp_change_request_area_applicator,SPP-CR Area Applicator Access,spp_area_base.model_spp_area,spp_change_request.group_spp_change_request_applicator,1,1,0,0 spp_change_request_service_point_applicator,SPP-CR Service Point Applicator Access,spp_service_points.model_spp_service_point,spp_change_request.group_spp_change_request_applicator,1,0,0,0 spp_change_request_g2p_group_membership_applicator,G2P Group Membership Applicator Access,g2p_registry_membership.model_g2p_group_membership,spp_change_request.group_spp_change_request_applicator,1,1,1,0 spp_change_request_g2p_group_membership_kind_applicator,G2P Group Membership Kind Applicator Access,g2p_registry_membership.model_g2p_group_membership_kind,spp_change_request.group_spp_change_request_applicator,1,0,0,0 @@ -128,7 +128,7 @@ spp_change_request_validators_agent,Change Request Validators Agent Access,spp_c spp_change_request_validation_sequence_agent,Change Request Validation Sequence Agent Access,spp_change_request.model_spp_change_request_validation_sequence,spp_change_request.group_spp_change_request_agent,1,1,1,0 spp_change_request_validation_stage_agent,Change Request Validation Stage Agent Access,spp_change_request.model_spp_change_request_validation_stage,spp_change_request.group_spp_change_request_agent,1,0,0,0 spp_change_request_group_members_agent,Change Request Group Membership Agent Access,spp_change_request.model_spp_change_request_group_members,spp_change_request.group_spp_change_request_agent,1,1,1,1 -spp_change_request_area_agent,SPP-CR Area Agent Access,spp_area.model_spp_area,spp_change_request.group_spp_change_request_agent,1,0,0,0 +spp_change_request_area_agent,SPP-CR Area Agent Access,spp_area_base.model_spp_area,spp_change_request.group_spp_change_request_agent,1,0,0,0 spp_change_request_service_point_agent,SPP-CR Service Point Agent Access,spp_service_points.model_spp_service_point,spp_change_request.group_spp_change_request_agent,1,0,0,0 spp_change_request_g2p_group_membership_agent,G2P Group Membership Agent Access,g2p_registry_membership.model_g2p_group_membership,spp_change_request.group_spp_change_request_agent,1,0,0,0 spp_change_request_g2p_group_membership_kind_agent,G2P Group Membership Kind Agent Access,g2p_registry_membership.model_g2p_group_membership_kind,spp_change_request.group_spp_change_request_agent,1,0,0,0 From ba594a87a7640e636b1fc774cd8d3b25d0536f49 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 13:47:42 +0800 Subject: [PATCH 08/22] [FIX] spp_user_roles: Update the reference to the spp.area model in the ir.model.access.csv --- spp_user_roles/security/ir.model.access.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp_user_roles/security/ir.model.access.csv b/spp_user_roles/security/ir.model.access.csv index 0628ef75a..b6f7a91ff 100644 --- a/spp_user_roles/security/ir.model.access.csv +++ b/spp_user_roles/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -spp_area_local_registrar,SPP Area Local Registrar Access,spp_area.model_spp_area,spp_user_roles.group_local_registrar,1,0,0,0 +spp_area_local_registrar,SPP Area Local Registrar Access,spp_area_base.model_spp_area,spp_user_roles.group_local_registrar,1,0,0,0 spp_res_users_local_registrar,SPP Users Local Registrar Access,base.model_res_users,spp_user_roles.group_local_registrar,1,0,0,0 spp_res_users_role_local_registrar,SPP Users Role Local Registrar Access,base_user_role.model_res_users_role,spp_user_roles.group_local_registrar,1,0,0,0 spp_res_users_role_line_local_registrar,SPP Users Role Lines Local Registrar Access,base_user_role.model_res_users_role_line,spp_user_roles.group_local_registrar,1,0,0,0 From 0869600b5270f72aecd2f4a97b4d35cddba8a04e Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 14:09:11 +0800 Subject: [PATCH 09/22] [FIX] spp_programs: Update the reference to the spp.area model in the ir.model.access.csv --- spp_programs/security/ir.model.access.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp_programs/security/ir.model.access.csv b/spp_programs/security/ir.model.access.csv index bab0fbd0b..fd9fd29e6 100644 --- a/spp_programs/security/ir.model.access.csv +++ b/spp_programs/security/ir.model.access.csv @@ -29,7 +29,7 @@ spp_programs_stock_location_program_manager,SPP Entitlement Stock Location Progr spp_programs_stock_route_program_manager,SPP Entitlement Stock Location Route Program Manager Access,stock.model_stock_route,g2p_programs.g2p_program_manager,1,0,0,0 spp_programs_stock_move_program_manager,SPP Entitlement Stock Move Program Manager Access,stock.model_stock_move,g2p_programs.g2p_program_manager,1,1,1,0 spp_programs_stock_rule_program_manager,SPP Entitlement Stock Rule Program Manager Access,stock.model_stock_rule,g2p_programs.g2p_program_manager,1,0,0,0 -spp_programs_spp_area_program_manager,SPP Area Program Manager Access,spp_area.model_spp_area,g2p_programs.g2p_program_manager,1,0,0,0 +spp_programs_spp_area_program_manager,SPP Area Program Manager Access,spp_area_base.model_spp_area,g2p_programs.g2p_program_manager,1,0,0,0 spp_programs_entitlement_inkind_program_validator,SPP Entitlement In-Kind Program Validator Access,spp_programs.model_g2p_entitlement_inkind,g2p_programs.g2p_program_validator,1,1,0,0 g2p_entitlement_inkind_report_wizard_program_validator,SPP Entitlement In-Kind Report Wizard Program Validator Access,spp_programs.model_g2p_entitlement_inkind_report_wizard,g2p_programs.g2p_program_validator,1,1,1,1 From c1f9442a69fec7df28489a3b150381b7e555f070 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 14:28:22 +0800 Subject: [PATCH 10/22] [FIX] spp_programs: Update the reference to the spp.area model in eligibility_manager.py --- spp_programs/models/managers/eligibility_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp_programs/models/managers/eligibility_manager.py b/spp_programs/models/managers/eligibility_manager.py index dffa0b669..a4e9d13d8 100644 --- a/spp_programs/models/managers/eligibility_manager.py +++ b/spp_programs/models/managers/eligibility_manager.py @@ -11,7 +11,7 @@ class SPPDefaultEligibilityManager(models.Model): @api.model def _get_admin_area_domain(self): - return [("kind", "=", self.env.ref("spp_area.admin_area_kind").id)] + return [("kind", "=", self.env.ref("spp_area_base.admin_area_kind").id)] admin_area_ids = fields.Many2many("spp.area", domain=_get_admin_area_domain) target_type = fields.Selection(related="program_id.target_type") From fafd9d36c4237b2f48475446659f1cf0dcf838aa Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 14:37:55 +0800 Subject: [PATCH 11/22] [FIX] spp_programs: Update the reference to the spp.area model in wizard/create_program_wizard.py --- spp_programs/wizard/create_program_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp_programs/wizard/create_program_wizard.py b/spp_programs/wizard/create_program_wizard.py index bbe16353b..c00e91674 100644 --- a/spp_programs/wizard/create_program_wizard.py +++ b/spp_programs/wizard/create_program_wizard.py @@ -15,7 +15,7 @@ class SPPCreateNewProgramWiz(models.TransientModel): @api.model def _get_admin_area_domain(self): - return [("kind", "=", self.env.ref("spp_area.admin_area_kind").id)] + return [("kind", "=", self.env.ref("spp_area_base.admin_area_kind").id)] admin_area_ids = fields.Many2many("spp.area", domain=_get_admin_area_domain) From a7acc96d3823314539d325529e719d3ee1f840e4 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 14:51:26 +0800 Subject: [PATCH 12/22] [FIX] spp_eligibility_tags: Update the reference to the spp.area model in wizard/create_program_wizard.py --- spp_eligibility_tags/wizard/create_program_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp_eligibility_tags/wizard/create_program_wizard.py b/spp_eligibility_tags/wizard/create_program_wizard.py index 5066133f6..b9daf2077 100644 --- a/spp_eligibility_tags/wizard/create_program_wizard.py +++ b/spp_eligibility_tags/wizard/create_program_wizard.py @@ -19,7 +19,7 @@ class SPPCreateNewProgramWiz(models.TransientModel): tags_id = fields.Many2one("g2p.registrant.tags", string="Tags") area_id = fields.Many2one( "spp.area", - domain=lambda self: [("kind", "=", self.env.ref("spp_area.admin_area_kind").id)], + domain=lambda self: [("kind", "=", self.env.ref("spp_area_base.admin_area_kind").id)], ) custom_domain = fields.Text(string="Tags Domain", default="[]", readonly=True) From d3b04209b64c93e6a1019885d84e5feebc0cc6e2 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 15:02:19 +0800 Subject: [PATCH 13/22] [FIX] spp_eligibility_tags: Update the reference to the spp.area model in models/eligibility_manager.py --- spp_eligibility_tags/models/eligibility_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp_eligibility_tags/models/eligibility_manager.py b/spp_eligibility_tags/models/eligibility_manager.py index bb8ead823..e8224678c 100644 --- a/spp_eligibility_tags/models/eligibility_manager.py +++ b/spp_eligibility_tags/models/eligibility_manager.py @@ -29,7 +29,7 @@ class TagBasedEligibilityManager(models.Model): tags_id = fields.Many2one("g2p.registrant.tags", string="Tags") area_id = fields.Many2one( "spp.area", - domain=lambda self: [("kind", "=", self.env.ref("spp_area.admin_area_kind").id)], + domain=lambda self: [("kind", "=", self.env.ref("spp_area_base.admin_area_kind").id)], ) custom_domain = fields.Text(string="Tags Domain", default="[]", readonly=True, compute="_compute_custom_domain") From 5ae1697d3ba79caa693b224b1cfcb841c86bcac1 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 15:29:45 +0800 Subject: [PATCH 14/22] [FIX] spp_eligibility_tags: Update the reference to the spp.area model in the tests. --- spp_eligibility_tags/tests/test_eligibility_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp_eligibility_tags/tests/test_eligibility_manager.py b/spp_eligibility_tags/tests/test_eligibility_manager.py index c5c7eb72c..cac5380d8 100644 --- a/spp_eligibility_tags/tests/test_eligibility_manager.py +++ b/spp_eligibility_tags/tests/test_eligibility_manager.py @@ -34,7 +34,7 @@ def setUpClass(cls): cls.area_id = cls.env["spp.area"].create( { "code": "101-1", - "kind": cls.env.ref("spp_area.admin_area_kind").id, + "kind": cls.env.ref("spp_area_base.admin_area_kind").id, "draft_name": "1", } ) From 75b2f07cab0d9d152975a99cfedf863ffc359ea3 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 17:52:50 +0800 Subject: [PATCH 15/22] [FIX] spp_area_base: Fix issues with the tests --- spp_area_base/tests/common.py | 2 +- spp_area_base/tests/test_area.py | 2 +- spp_area_base/tests/test_area_import.py | 88 +-------------------- spp_area_base/tests/test_area_import_raw.py | 2 +- 4 files changed, 4 insertions(+), 90 deletions(-) diff --git a/spp_area_base/tests/common.py b/spp_area_base/tests/common.py index d371412d4..92281b2e7 100644 --- a/spp_area_base/tests/common.py +++ b/spp_area_base/tests/common.py @@ -4,7 +4,7 @@ from odoo.tests.common import TransactionCase -class AreaImportTestMixin(TransactionCase): +class AreaImportBaseTestMixin(TransactionCase): @staticmethod def get_file_path_1(): return f"{os.path.dirname(os.path.abspath(__file__))}/irq_adminboundaries_tabulardata.xlsx" diff --git a/spp_area_base/tests/test_area.py b/spp_area_base/tests/test_area.py index 8039e1d26..ab66fce34 100644 --- a/spp_area_base/tests/test_area.py +++ b/spp_area_base/tests/test_area.py @@ -9,7 +9,7 @@ # @tagged("post_install", "-at_install") -class AreaTest(TransactionCase): +class BaseAreaTest(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() diff --git a/spp_area_base/tests/test_area_import.py b/spp_area_base/tests/test_area_import.py index fb442501f..36972b36f 100644 --- a/spp_area_base/tests/test_area_import.py +++ b/spp_area_base/tests/test_area_import.py @@ -7,7 +7,7 @@ _logger = logging.getLogger(__name__) -class AreaImportTest(AreaImportTestMixin): +class BaseAreaImportTest(AreaImportTestMixin): def test_01_cancel_import(self): self.area_import_id.cancel_import() @@ -36,92 +36,6 @@ def test_03_import_data(self): self.area_import_id.tot_rows_imported, ) - # NOTE: validate_raw_data and save_to_area tests are not working - # since both of them are now fully asynchronous. - # update test case. - - # def test_04_validate_raw_data(self): - # # Greater than or equal to 400 rows - # with self.assertRaises(ValidationError): - # self.area_import_id.import_data() - - # lang = self.env["res.lang"].with_context(active_test=False).search([("iso_code", "=", "ar")]) - # lang.active = True - - # self.area_import_id.import_data() - # self.area_import_id.validate_raw_data() - - # raw_data_ids = self.area_import_id.raw_data_ids - - # self.assertEqual(len(raw_data_ids.ids), self.area_import_id.tot_rows_imported) - # self.assertEqual(0, self.area_import_id.tot_rows_error) - # self.assertEqual(self.area_import_id.state, "Imported") - # self.assertTrue(self.area_import_id.locked) - # self.assertEqual(self.area_import_id.locked_reason, "Validating data.") - - # # Less than 400 rows - # self.area_import_id_2.import_data() - # self.area_import_id_2.validate_raw_data() - - # raw_data_ids = self.area_import_id_2.raw_data_ids - # self.assertEqual(len(raw_data_ids.ids), self.area_import_id_2.tot_rows_imported) - # self.assertEqual(0, self.area_import_id_2.tot_rows_error) - # self.assertEqual(self.area_import_id_2.state, "Validated") - # self.assertFalse(self.area_import_id_2.locked) - # self.assertFalse(self.area_import_id_2.locked_reason) - # self.assertEqual( - # len( - # self.env["spp.area.import.raw"].search( - # [("id", "in", raw_data_ids.ids), ("state", "=", "Validated")], - # ) - # ), - # self.area_import_id_2.tot_rows_imported, - # ) - - # def test_05_save_to_area(self): - # # Greater than or equal to 400 rows - # with self.assertRaises(ValidationError): - # self.area_import_id.import_data() - # lang = self.env["res.lang"].with_context(active_test=False).search([("iso_code", "=", "ar")]) - # lang.active = True - - # self.area_import_id.import_data() - # self.area_import_id.save_to_area() - - # raw_data_ids = self.area_import_id.raw_data_ids - - # self.assertEqual(len(raw_data_ids.ids), self.area_import_id.tot_rows_imported) - # self.assertEqual(0, self.area_import_id.tot_rows_error) - # self.assertEqual(self.area_import_id.state, "Imported") - # self.assertTrue(self.area_import_id.locked) - # self.assertEqual(self.area_import_id.locked_reason, "Saving to Area.") - - # # Less than 400 rows - # self.area_import_id_2.import_data() - # self.area_import_id_2.validate_raw_data() - # self.area_import_id_2.save_to_area() - - # raw_data_ids = self.area_import_id_2.raw_data_ids - - # self.assertEqual(len(raw_data_ids.ids), self.area_import_id_2.tot_rows_imported) - # self.assertEqual(0, self.area_import_id_2.tot_rows_error) - # self.assertEqual(self.area_import_id_2.state, "Done") - # self.assertFalse(self.area_import_id_2.locked) - # self.assertFalse(self.area_import_id_2.locked_reason) - - # for raw_data_id in raw_data_ids: - # self.assertTrue( - # bool( - # self.env["spp.area"].search( - # [ - # ("draft_name", "=", raw_data_id.admin_name), - # ("code", "=", raw_data_id.admin_code), - # ], - # limit=1, - # ) - # ) - # ) - def test_06_refresh_page(self): action = self.area_import_id.refresh_page() diff --git a/spp_area_base/tests/test_area_import_raw.py b/spp_area_base/tests/test_area_import_raw.py index 5019cdfbe..e8b2126ee 100644 --- a/spp_area_base/tests/test_area_import_raw.py +++ b/spp_area_base/tests/test_area_import_raw.py @@ -1,7 +1,7 @@ from .common import AreaImportTestMixin -class AreaImportRawTest(AreaImportTestMixin): +class BaseAreaImportRawTest(AreaImportTestMixin): @classmethod def setUpClass(cls): super().setUpClass() From b06f35ed597b711db2e3fcbf3e9d54045fb98047 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 18:00:55 +0800 Subject: [PATCH 16/22] [FIX] spp_area_base: Fix issues with the tests --- spp_area_base/tests/common.py | 8 ++++---- spp_area_base/tests/test_area_import.py | 4 ++-- spp_area_base/tests/test_area_import_raw.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spp_area_base/tests/common.py b/spp_area_base/tests/common.py index 92281b2e7..3213bb138 100644 --- a/spp_area_base/tests/common.py +++ b/spp_area_base/tests/common.py @@ -6,11 +6,11 @@ class AreaImportBaseTestMixin(TransactionCase): @staticmethod - def get_file_path_1(): + def get_file_path_irq(): return f"{os.path.dirname(os.path.abspath(__file__))}/irq_adminboundaries_tabulardata.xlsx" @staticmethod - def get_file_path_2(): + def get_file_path_pse(): return f"{os.path.dirname(os.path.abspath(__file__))}/pse_adminboundaries_tabulardata.xlsx" @classmethod @@ -20,7 +20,7 @@ def setUpClass(cls): xls_file = None xls_file_name = None - file_path = cls.get_file_path_1() + file_path = cls.get_file_path_irq() with open(file_path, "rb") as f: xls_file_name = f.name xls_file = base64.b64encode(f.read()) @@ -37,7 +37,7 @@ def setUpClass(cls): xls_file_2 = None xls_file_name_2 = None - file_path_2 = cls.get_file_path_2() + file_path_2 = cls.get_file_path_pse() with open(file_path_2, "rb") as f: xls_file_name_2 = f.name xls_file_2 = base64.b64encode(f.read()) diff --git a/spp_area_base/tests/test_area_import.py b/spp_area_base/tests/test_area_import.py index 36972b36f..8cfc0460d 100644 --- a/spp_area_base/tests/test_area_import.py +++ b/spp_area_base/tests/test_area_import.py @@ -2,12 +2,12 @@ from odoo.exceptions import ValidationError -from .common import AreaImportTestMixin +from .common import AreaImportBaseTestMixin _logger = logging.getLogger(__name__) -class BaseAreaImportTest(AreaImportTestMixin): +class BaseAreaImportTest(AreaImportBaseTestMixin): def test_01_cancel_import(self): self.area_import_id.cancel_import() diff --git a/spp_area_base/tests/test_area_import_raw.py b/spp_area_base/tests/test_area_import_raw.py index e8b2126ee..57737eb33 100644 --- a/spp_area_base/tests/test_area_import_raw.py +++ b/spp_area_base/tests/test_area_import_raw.py @@ -1,7 +1,7 @@ -from .common import AreaImportTestMixin +from .common import AreaImportBaseTestMixin -class BaseAreaImportRawTest(AreaImportTestMixin): +class BaseAreaImportRawTest(AreaImportBaseTestMixin): @classmethod def setUpClass(cls): super().setUpClass() From 642ec0b3eca461f90527d0f6266ccc66fee4f531 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 18:12:20 +0800 Subject: [PATCH 17/22] [FIX] spp_area_base: Fix issues with the tests --- spp_area_base/tests/test_area_import.py | 10 +++++----- spp_area_base/tests/test_area_import_raw.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spp_area_base/tests/test_area_import.py b/spp_area_base/tests/test_area_import.py index 8cfc0460d..1dc41c744 100644 --- a/spp_area_base/tests/test_area_import.py +++ b/spp_area_base/tests/test_area_import.py @@ -8,17 +8,17 @@ class BaseAreaImportTest(AreaImportBaseTestMixin): - def test_01_cancel_import(self): + def test_01_cancel_area_import(self): self.area_import_id.cancel_import() self.assertEqual(self.area_import_id.state, "Cancelled") - def test_02_reset_to_uploaded(self): + def test_02_reset_to_uploaded_area(self): self.area_import_id.reset_to_uploaded() self.assertEqual(self.area_import_id.state, "Uploaded") - def test_03_import_data(self): + def test_03_import_area_data(self): with self.assertRaises(ValidationError): self.area_import_id.import_data() @@ -36,7 +36,7 @@ def test_03_import_data(self): self.area_import_id.tot_rows_imported, ) - def test_06_refresh_page(self): + def test_06_reload_page(self): action = self.area_import_id.refresh_page() self.assertEqual( @@ -47,7 +47,7 @@ def test_06_refresh_page(self): }, ) - def test_07_async_mark_done(self): + def test_07_async_mark_import_done(self): self.area_import_id._async_mark_done() self.assertFalse(self.area_import_id.locked) diff --git a/spp_area_base/tests/test_area_import_raw.py b/spp_area_base/tests/test_area_import_raw.py index 57737eb33..50c6de367 100644 --- a/spp_area_base/tests/test_area_import_raw.py +++ b/spp_area_base/tests/test_area_import_raw.py @@ -30,7 +30,7 @@ def setUpClass(cls): } ) - def test_01_validate_raw_data_no_error(self): + def test_01_validate_import_raw_data_no_error(self): result = self.area_import_raw_id.validate_raw_data() result_child = self.area_import_raw_child_id.validate_raw_data() @@ -42,7 +42,7 @@ def test_01_validate_raw_data_no_error(self): self.assertEqual(self.area_import_raw_child_id.state, "Validated") self.assertEqual(self.area_import_raw_child_id.remarks, "No Error") - def test_02_validate_raw_data_with_error(self): + def test_02_validate_import_raw_data_with_error(self): self.area_import_raw_id.admin_name = "" self.area_import_raw_id.area_sqkm = "text" self.area_import_raw_id.parent_name = "MNL" @@ -65,7 +65,7 @@ def test_02_validate_raw_data_with_error(self): self.area_import_raw_child_id.remarks, ) - def test_03_save_to_area(self): + def test_03_save_import_to_area(self): self.area_import_raw_id.area_sqkm = "" self.area_import_raw_id.save_to_area() From c3090e77dc6c613918ae8d52dc53f4a1a29bf29a Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 18:20:25 +0800 Subject: [PATCH 18/22] [FIX] spp_area_base: Fix SonarQube reported errors. --- spp_area_base/__init__.py | 2 -- spp_area_base/models/area.py | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/spp_area_base/__init__.py b/spp_area_base/__init__.py index 23b379697..d22455abf 100644 --- a/spp_area_base/__init__.py +++ b/spp_area_base/__init__.py @@ -2,5 +2,3 @@ from . import models - -# from . import controllers diff --git a/spp_area_base/models/area.py b/spp_area_base/models/area.py index 1a8d21037..0398d9605 100644 --- a/spp_area_base/models/area.py +++ b/spp_area_base/models/area.py @@ -23,7 +23,7 @@ class OpenSPPArea(models.Model): _parent_store = True _order = "parent_id,name" - parent_id = fields.Many2one("spp.area", "Parent") + parent_id = fields.Many2one(_name, "Parent") complete_name = fields.Char(compute="_compute_complete_name", recursive=True, translate=True) name = fields.Char(translate=True, compute="_compute_name", store=True) draft_name = fields.Char(required=True, translate=True) @@ -32,7 +32,7 @@ class OpenSPPArea(models.Model): altnames = fields.Char("Alternate Name") level = fields.Integer(help="This is the area level for importing") area_level = fields.Integer(compute="_compute_area_level", store=True, help="This is the main area level") - child_ids = fields.One2many("spp.area", "id", "Child", compute="_compute_get_childs") + child_ids = fields.One2many(_name, "id", "Child", compute="_compute_get_childs") kind = fields.Many2one("spp.area.kind") area_sqkm = fields.Float("Area (sq/km)") @@ -67,7 +67,7 @@ def _compute_get_childs(self): and assigns them to the child_ids field. """ for rec in self: - child_ids = self.env["spp.area"].search([("parent_id", "=", rec.id)]) + child_ids = self.env[self._name].search([("parent_id", "=", rec.id)]) rec.child_ids = child_ids @api.depends("parent_id") @@ -139,7 +139,7 @@ def create(self, vals): area_code = self.code if "code" in vals: area_code = vals["code"] - curr_area = self.env["spp.area"].search( + curr_area = self.env[self._name].search( [ ("name", "=", area_name), ("code", "=", area_code), @@ -184,7 +184,7 @@ def write(self, vals): area_code = rec.code if "code" in vals: area_code = vals["code"] - curr_area = self.env["spp.area"].search( + curr_area = self.env[self._name].search( [ ("name", "=", area_name), ("code", "=", area_code), @@ -206,7 +206,7 @@ def open_area_form(self): return { "name": "Area", "view_mode": "form", - "res_model": "spp.area", + "res_model": self._name, "res_id": rec.id, "view_id": self.env.ref("spp_area_base.view_spparea_form").id, "type": "ir.actions.act_window", @@ -267,7 +267,7 @@ def unlink(self): if external_identifier and external_identifier.name: raise ValidationError(_("Can't delete default Area Type")) else: - areas = self.env["spp.area"].search([("kind", "=", rec.id)]) + areas = self.env[self._name].search([("kind", "=", rec.id)]) if areas: raise ValidationError(_("Can't delete used Area Type")) else: From 5b7a3725a26bea6d672e226bddf30992f731e72d Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 18:28:03 +0800 Subject: [PATCH 19/22] [FIX] spp_area_base: Fix SonarQube reported errors. --- spp_area_base/models/area.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spp_area_base/models/area.py b/spp_area_base/models/area.py index 0398d9605..d8bdb7723 100644 --- a/spp_area_base/models/area.py +++ b/spp_area_base/models/area.py @@ -6,6 +6,7 @@ from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) +_kind_model = "spp.area.kind" class OpenSPPArea(models.Model): @@ -33,7 +34,7 @@ class OpenSPPArea(models.Model): level = fields.Integer(help="This is the area level for importing") area_level = fields.Integer(compute="_compute_area_level", store=True, help="This is the main area level") child_ids = fields.One2many(_name, "id", "Child", compute="_compute_get_childs") - kind = fields.Many2one("spp.area.kind") + kind = fields.Many2one(_kind_model) area_sqkm = fields.Float("Area (sq/km)") _sql_constraints = [ @@ -223,14 +224,14 @@ class OpenSPPAreaKind(models.Model): their hierarchical relationships and names. """ - _name = "spp.area.kind" + _name = _kind_model _description = "Area Type" _parent_name = "parent_id" _parent_store = True _rec_name = "complete_name" _order = "parent_id,name" - parent_id = fields.Many2one("spp.area.kind", "Parent") + parent_id = fields.Many2one(_kind_model, "Parent") parent_path = fields.Char(index=True) name = fields.Char(required=True) complete_name = fields.Char(compute="_compute_complete_name", recursive=True, translate=True) @@ -262,7 +263,7 @@ def unlink(self): """ for rec in self: external_identifier = self.env["ir.model.data"].search( - [("res_id", "=", rec.id), ("model", "=", "spp.area.kind")] + [("res_id", "=", rec.id), ("model", "=", _kind_model)] ) if external_identifier and external_identifier.name: raise ValidationError(_("Can't delete default Area Type")) @@ -284,7 +285,7 @@ def write(self, vals): """ for rec in self: external_identifier = self.env["ir.model.data"].search( - [("res_id", "=", rec.id), ("model", "=", "spp.area.kind")] + [("res_id", "=", rec.id), ("model", "=", _kind_model)] ) if external_identifier and external_identifier.name: vals = {} @@ -293,7 +294,7 @@ def write(self, vals): def update_parent(self): for rec in self: if rec.parent_name and rec.parent_code: - parent_id = self.env["spp.area.kind"].search( + parent_id = self.env[_kind_model].search( [ ("code", "=", rec.parent_code), ], From 0b0d14fa7f426ecc9f8781672bd824f59201ee26 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 18:41:23 +0800 Subject: [PATCH 20/22] [FIX] spp_area_base: Fix SonarQube reported errors. --- spp_area_base/models/area.py | 12 ++++++------ spp_area_base/models/area_import.py | 20 +++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/spp_area_base/models/area.py b/spp_area_base/models/area.py index d8bdb7723..807ad5986 100644 --- a/spp_area_base/models/area.py +++ b/spp_area_base/models/area.py @@ -150,23 +150,23 @@ def create(self, vals): if curr_area: raise ValidationError(_("Area already exist!")) else: - Area = super().create(vals) - Languages = self.env["res.lang"].search([("active", "=", True)]) + area_val = super().create(vals) + languages = self.env["res.lang"].search([("active", "=", True)]) vals_list = [] - for lang_code in Languages: + for lang_code in languages: vals_list.append( { "name": "spp.area,draft_name", "lang": lang_code.code, - "res_id": Area.id, - "src": Area.draft_name, + "res_id": area_val.id, + "src": area_val.draft_name, "value": None, "state": "to_translate", "type": "model", } ) - return Area + return area_val def write(self, vals): """ diff --git a/spp_area_base/models/area_import.py b/spp_area_base/models/area_import.py index 86b2889ef..5742546bb 100644 --- a/spp_area_base/models/area_import.py +++ b/spp_area_base/models/area_import.py @@ -12,11 +12,13 @@ from odoo.addons.queue_job.delay import group _logger = logging.getLogger(__name__) +_area_import_raw_model = "spp.area.import.raw" class OpenSPPAreaImport(models.Model): _name = "spp.area.import" _description = "Areas Import Table" + _users_model = "res.users" MIN_ROW_JOB_QUEUE = 400 @@ -40,12 +42,12 @@ class OpenSPPAreaImport(models.Model): excel_file = fields.Binary("Area Excel File") date_uploaded = fields.Datetime() - upload_id = fields.Many2one("res.users", "Uploaded by") + upload_id = fields.Many2one(_users_model, "Uploaded by") date_imported = fields.Datetime() - import_id = fields.Many2one("res.users", "Imported by") + import_id = fields.Many2one(_users_model, "Imported by") date_validated = fields.Datetime() - validate_id = fields.Many2one("res.users", "Validated by") - raw_data_ids = fields.One2many("spp.area.import.raw", "area_import_id", "Raw Data") + validate_id = fields.Many2one(_users_model, "Validated by") + raw_data_ids = fields.One2many(_area_import_raw_model, "area_import_id", "Raw Data") tot_rows_imported = fields.Integer( "Total Rows Imported", compute="_compute_get_total_rows", @@ -93,7 +95,7 @@ def _compute_get_total_rows(self): """ for rec in self: tot_rows_imported = len(rec.raw_data_ids) - tot_rows_error = self.env["spp.area.import.raw"].search( + tot_rows_error = self.env[_area_import_raw_model].search( [("id", "in", rec.raw_data_ids.ids), ("state", "=", "Error")] ) rec.update( @@ -194,7 +196,7 @@ def get_area_vals(self, column_indexes, row, sheet, area_level): def create_import_raw(self, vals, column_indexes, row, sheet): self.ensure_one() - import_raw_id = self.env["spp.area.import.raw"].create(vals) + import_raw_id = self.env[_area_import_raw_model].create(vals) for lang_code in column_indexes["name_indexes"]: lang_name = sheet.cell(row, column_indexes["name_indexes"][lang_code]).value import_raw_id.with_context(lang=lang_code).write( @@ -336,7 +338,7 @@ def _validate_mark_done(self): self.locked = False self.locked_reason = None self.ensure_one() - if not self.env["spp.area.import.raw"].search([("id", "in", self.raw_data_ids.ids), ("state", "=", "Error")]): + if not self.env[_area_import_raw_model].search([("id", "in", self.raw_data_ids.ids), ("state", "=", "Error")]): self.update( { "state": self.VALIDATED, @@ -419,7 +421,7 @@ def _save_to_area_mark_done(self): self.ensure_one() self.locked = False self.locked_reason = None - if not self.env["spp.area.import.raw"].search( + if not self.env[_area_import_raw_model].search( [("id", "in", self.raw_data_ids.ids), ("state", "=", "Validated")] ): self.update( @@ -443,7 +445,7 @@ def refresh_page(self): # Assets Import Raw Data class OpenSPPAreaImportActivities(models.Model): - _name = "spp.area.import.raw" + _name = _area_import_raw_model _description = "Area Import Raw Data" _order = "level" From c654ffb1b9a671bd52564f7d5ebe8921b488a674 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 19:00:28 +0800 Subject: [PATCH 21/22] [FIX] spp_area_base: Fix SonarQube reported errors. --- spp_area_base/models/area_import.py | 41 +++++++++++---------- spp_area_base/static/description/index.html | 14 ------- spp_area_base/tests/test_area.py | 1 - 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/spp_area_base/models/area_import.py b/spp_area_base/models/area_import.py index 5742546bb..14f7485cb 100644 --- a/spp_area_base/models/area_import.py +++ b/spp_area_base/models/area_import.py @@ -13,6 +13,8 @@ _logger = logging.getLogger(__name__) _area_import_raw_model = "spp.area.import.raw" +_res_lang_model = "res.lang" +_area_import_channel = "root.area_import" class OpenSPPAreaImport(models.Model): @@ -126,7 +128,7 @@ def get_column_indexes(self, columns, area_level): default_lang = "en_US" default_iso_code = default_lang.split("_")[0].upper() - active_languages = self.env["res.lang"].search([("active", "=", True)]) + active_languages = self.env[_res_lang_model].search([("active", "=", True)]) if not active_languages: raise ValidationError(_("No active language found.")) @@ -226,7 +228,7 @@ def check_all_languages_activated(self, columns, area_level): """ self.ensure_one() prefix = f"ADM{area_level}_" - active_langs = self.env["res.lang"].search([("active", "=", True)]).mapped("iso_code") + active_langs = self.env[_res_lang_model].search([("active", "=", True)]).mapped("iso_code") for col in columns: if col.startswith(prefix): @@ -275,14 +277,14 @@ def import_data(self): start = i * 1000 end = min((i + 1) * 1000, sheet.nrows) jobs.append( - self.delayable(channel="root.area_import")._import_data( + self.delayable(channel=_area_import_channel)._import_data( sheet_name, column_indexes, start, end, area_level ) ) main_job = group(*jobs) - main_job.on_done(self.delayable(channel="root.area_import")._async_mark_done()) + main_job.on_done(self.delayable(channel=_area_import_channel)._async_mark_done()) main_job.delay() def _import_data(self, sheet_name, column_indexes, start, end, area_level): @@ -322,9 +324,9 @@ def validate_raw_data(self): for i in range(batches): start = i * 1000 end = min((i + 1) * 1000, len(rec.raw_data_ids)) - jobs.append(rec.delayable(channel="root.area_import")._validate_raw_data(rec.raw_data_ids[start:end])) + jobs.append(rec.delayable(channel=_area_import_channel)._validate_raw_data(rec.raw_data_ids[start:end])) main_job = group(*jobs) - main_job.on_done(rec.delayable(channel="root.area_import")._validate_mark_done()) + main_job.on_done(rec.delayable(channel=_area_import_channel)._validate_mark_done()) main_job.delay() def _validate_raw_data(self, raw_data_ids): @@ -355,10 +357,10 @@ def fix_area_level_and_kind(self): start = i * 1000 end = min((i + 1) * 1000, len(rec.raw_data_ids)) jobs.append( - rec.delayable(channel="root.area_import")._fix_area_level_and_kind(rec.raw_data_ids[start:end]) + rec.delayable(channel=_area_import_channel)._fix_area_level_and_kind(rec.raw_data_ids[start:end]) ) main_job = group(*jobs) - main_job.on_done(rec.delayable(channel="root.area_import")._async_mark_done()) + main_job.on_done(rec.delayable(channel=_area_import_channel)._async_mark_done()) main_job.delay() def _fix_area_level_and_kind(self, raw_data_ids): @@ -398,14 +400,14 @@ def _async_recursive_save_to_area(self, raw_data_ids): """ self.ensure_one() jobs = [] - jobs.append(self.delayable(channel="root.area_import")._save_to_area(raw_data_ids[:1000])) + jobs.append(self.delayable(channel=_area_import_channel)._save_to_area(raw_data_ids[:1000])) main_job = group(*jobs) count = len(raw_data_ids) if count <= 1000: - main_job.on_done(self.delayable(channel="root.area_import")._save_to_area_mark_done()) + main_job.on_done(self.delayable(channel=_area_import_channel)._save_to_area_mark_done()) else: main_job.on_done( - self.delayable(channel="root.area_import")._async_recursive_save_to_area(raw_data_ids[1000:]) + self.delayable(channel=_area_import_channel)._async_recursive_save_to_area(raw_data_ids[1000:]) ) main_job.delay() @@ -448,6 +450,7 @@ class OpenSPPAreaImportActivities(models.Model): _name = _area_import_raw_model _description = "Area Import Raw Data" _order = "level" + _area_model = "spp.area" NEW = "New" VALIDATED = "Validated" @@ -463,7 +466,7 @@ class OpenSPPAreaImportActivities(models.Model): (POSTED, POSTED), ] - STATE_ORDER = { + STATE_ORDER_STATE = { ERROR: 0, NEW: 1, VALIDATED: 2, @@ -492,12 +495,12 @@ class OpenSPPAreaImportActivities(models.Model): compute="_compute_state_order", store=True, ) - area_id = fields.Many2one("spp.area", "Area", readonly=True) + area_id = fields.Many2one(_area_model, "Area", readonly=True) @api.depends("state") def _compute_state_order(self): for rec in self: - rec.state_order = self.STATE_ORDER[rec.state] + rec.state_order = self.STATE_ORDER_STATE[rec.state] def check_errors(self): self.ensure_one() @@ -542,7 +545,7 @@ def get_area_vals(self): parent_id = None if self.parent_name and self.parent_code: parent_id = ( - self.env["spp.area"] + self.env[self._area_model] .search( [ ("code", "=", self.parent_code), @@ -572,15 +575,15 @@ def save_to_area(self): The function saves data to the "spp.area" model in the database, updating existing records if they exist and creating new records if they don't. """ - active_languages = self.env["res.lang"].search([("active", "=", True)]) + active_languages = self.env[_res_lang_model].search([("active", "=", True)]) for rec in self: area_vals = rec.get_area_vals() - if area_id := self.env["spp.area"].search([("code", "=", rec.admin_code)]): + if area_id := self.env[self._area_model].search([("code", "=", rec.admin_code)]): state = self.UPDATED area_id.update(area_vals) else: state = self.POSTED - area_id = self.env["spp.area"].create(area_vals) + area_id = self.env[self._area_model].create(area_vals) for lang in active_languages: area_id.with_context(lang=lang.code).write( @@ -605,7 +608,7 @@ def fix_area_level_and_kind(self): parent_id = None if rec.parent_name and rec.parent_code: parent_id = ( - self.env["spp.area"] + self.env[self._area_model] .search( [ ("code", "=", rec.parent_code), diff --git a/spp_area_base/static/description/index.html b/spp_area_base/static/description/index.html index 70baee1df..6465181a1 100644 --- a/spp_area_base/static/description/index.html +++ b/spp_area_base/static/description/index.html @@ -59,11 +59,6 @@ overflow: hidden; } -/* Uncomment (and remove this text!) to get bold-faced definition list terms -dl.docutils dt { - font-weight: bold } -*/ - div.abstract { margin: 2em 5em } @@ -90,15 +85,6 @@ font-weight: bold ; font-family: sans-serif } -/* Uncomment (and remove this text!) to get reduced vertical space in - compound paragraphs. -div.compound .compound-first, div.compound .compound-middle { - margin-bottom: 0.5em } - -div.compound .compound-last, div.compound .compound-middle { - margin-top: 0.5em } -*/ - div.dedication { margin: 2em 5em ; text-align: center ; diff --git a/spp_area_base/tests/test_area.py b/spp_area_base/tests/test_area.py index ab66fce34..4b3e40f08 100644 --- a/spp_area_base/tests/test_area.py +++ b/spp_area_base/tests/test_area.py @@ -2,7 +2,6 @@ import logging -# from odoo.tests import tagged from odoo.tests.common import TransactionCase _logger = logging.getLogger(__name__) From ea209f16fa620b80aae5f07e98fca8d9ce135db0 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 13 Feb 2025 19:11:58 +0800 Subject: [PATCH 22/22] [FIX] spp_area_base: Fix SonarQube cloud duplicate codes issues. --- spp_area_base/tests/test_area_import.py | 6 +-- spp_area_base/tests/test_area_import_raw.py | 50 ++++++++++----------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/spp_area_base/tests/test_area_import.py b/spp_area_base/tests/test_area_import.py index 1dc41c744..04dc481d1 100644 --- a/spp_area_base/tests/test_area_import.py +++ b/spp_area_base/tests/test_area_import.py @@ -26,13 +26,13 @@ def test_03_import_area_data(self): lang.active = True self.area_import_id.import_data() - raw_data_ids = self.area_import_id.raw_data_ids + raw_area_data_ids = self.area_import_id.raw_data_ids - self.assertEqual(len(raw_data_ids.ids), self.area_import_id.tot_rows_imported) + self.assertEqual(len(raw_area_data_ids.ids), self.area_import_id.tot_rows_imported) self.assertEqual(0, self.area_import_id.tot_rows_error) self.assertEqual(self.area_import_id.state, "Uploaded") self.assertEqual( - len(self.env["spp.area.import.raw"].search([("id", "in", raw_data_ids.ids), ("state", "=", "New")])), + len(self.env["spp.area.import.raw"].search([("id", "in", raw_area_data_ids.ids), ("state", "=", "New")])), self.area_import_id.tot_rows_imported, ) diff --git a/spp_area_base/tests/test_area_import_raw.py b/spp_area_base/tests/test_area_import_raw.py index 50c6de367..8a4d29d9e 100644 --- a/spp_area_base/tests/test_area_import_raw.py +++ b/spp_area_base/tests/test_area_import_raw.py @@ -6,7 +6,7 @@ class BaseAreaImportRawTest(AreaImportBaseTestMixin): def setUpClass(cls): super().setUpClass() - cls.area_import_raw_id = cls.env["spp.area.import.raw"].create( + cls.area_import_raw_data_id = cls.env["spp.area.import.raw"].create( { "area_import_id": cls.area_import_id.id, "admin_name": "Philippines", @@ -18,7 +18,7 @@ def setUpClass(cls): } ) - cls.area_import_raw_child_id = cls.env["spp.area.import.raw"].create( + cls.area_import_raw_data_child_id = cls.env["spp.area.import.raw"].create( { "area_import_id": cls.area_import_id.id, "admin_name": "Manila", @@ -31,45 +31,45 @@ def setUpClass(cls): ) def test_01_validate_import_raw_data_no_error(self): - result = self.area_import_raw_id.validate_raw_data() - result_child = self.area_import_raw_child_id.validate_raw_data() + result = self.area_import_raw_data_id.validate_raw_data() + result_child = self.area_import_raw_data_child_id.validate_raw_data() self.assertFalse(result) - self.assertEqual(self.area_import_raw_id.state, "Validated") - self.assertEqual(self.area_import_raw_id.remarks, "No Error") + self.assertEqual(self.area_import_raw_data_id.state, "Validated") + self.assertEqual(self.area_import_raw_data_id.remarks, "No Error") self.assertFalse(result_child) - self.assertEqual(self.area_import_raw_child_id.state, "Validated") - self.assertEqual(self.area_import_raw_child_id.remarks, "No Error") + self.assertEqual(self.area_import_raw_data_child_id.state, "Validated") + self.assertEqual(self.area_import_raw_data_child_id.remarks, "No Error") def test_02_validate_import_raw_data_with_error(self): - self.area_import_raw_id.admin_name = "" - self.area_import_raw_id.area_sqkm = "text" - self.area_import_raw_id.parent_name = "MNL" - self.area_import_raw_child_id.parent_name = "" + self.area_import_raw_data_id.admin_name = "" + self.area_import_raw_data_id.area_sqkm = "text" + self.area_import_raw_data_id.parent_name = "MNL" + self.area_import_raw_data_child_id.parent_name = "" - self.area_import_raw_id.validate_raw_data() - self.area_import_raw_child_id.validate_raw_data() + self.area_import_raw_data_id.validate_raw_data() + self.area_import_raw_data_child_id.validate_raw_data() - self.assertEqual(self.area_import_raw_id.state, "Error") - self.assertIn("Name and Code of area is required.", self.area_import_raw_id.remarks) - self.assertIn("AREA_SQKM should be numerical.", self.area_import_raw_id.remarks) + self.assertEqual(self.area_import_raw_data_id.state, "Error") + self.assertIn("Name and Code of area is required.", self.area_import_raw_data_id.remarks) + self.assertIn("AREA_SQKM should be numerical.", self.area_import_raw_data_id.remarks) self.assertIn( "Level 0 area should not have a parent name and parent code.", - self.area_import_raw_id.remarks, + self.area_import_raw_data_id.remarks, ) - self.assertEqual(self.area_import_raw_child_id.state, "Error") + self.assertEqual(self.area_import_raw_data_child_id.state, "Error") self.assertIn( "Level 1 and above area should have a parent name and parent code.", - self.area_import_raw_child_id.remarks, + self.area_import_raw_data_child_id.remarks, ) def test_03_save_import_to_area(self): - self.area_import_raw_id.area_sqkm = "" + self.area_import_raw_data_id.area_sqkm = "" - self.area_import_raw_id.save_to_area() - self.assertEqual(self.area_import_raw_id.state, "Posted") + self.area_import_raw_data_id.save_to_area() + self.assertEqual(self.area_import_raw_data_id.state, "Posted") - self.area_import_raw_id.save_to_area() - self.assertEqual(self.area_import_raw_id.state, "Updated") + self.area_import_raw_data_id.save_to_area() + self.assertEqual(self.area_import_raw_data_id.state, "Updated")