From 8ef5f314928c65842f226a94e7012bf1824c0146 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 23 Oct 2025 11:21:05 +0800 Subject: [PATCH 1/8] Initial commit --- spp_code_generator/README.rst | 93 +++++++++++++++++++ spp_code_generator/__init__.py | 1 + spp_code_generator/__manifest__.py | 31 +++++++ spp_code_generator/models/__init__.py | 1 + spp_code_generator/models/code_generator.py | 37 ++++++++ spp_code_generator/pyproject.toml | 3 + .../security/ir.model.access.csv | 4 + .../views/code_generator_views.xml | 74 +++++++++++++++ 8 files changed, 244 insertions(+) create mode 100644 spp_code_generator/README.rst create mode 100644 spp_code_generator/__init__.py create mode 100644 spp_code_generator/__manifest__.py create mode 100644 spp_code_generator/models/__init__.py create mode 100644 spp_code_generator/models/code_generator.py create mode 100644 spp_code_generator/pyproject.toml create mode 100644 spp_code_generator/security/ir.model.access.csv create mode 100644 spp_code_generator/views/code_generator_views.xml diff --git a/spp_code_generator/README.rst b/spp_code_generator/README.rst new file mode 100644 index 000000000..c5a3484c3 --- /dev/null +++ b/spp_code_generator/README.rst @@ -0,0 +1,93 @@ +======================= +OpenSPP Code Generator +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:... + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |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_code_generator + :alt: OpenSPP/openspp-modules + +|badge1| |badge2| |badge3| + +This module provides a YAML-based code generator for OpenSPP program registrants and groups. + +**Table of contents** + +.. contents:: + :local: + +Features +======== + +* Upload and validate YAML configuration files +* Automatic YAML syntax validation +* Unique filename constraint +* Integration with OpenSPP registry system + +Configuration +============= + +To configure this module: + +#. Go to Code Generator > Code Generators +#. Click Create +#. Enter a filename (must end with .yaml or .yml) +#. Upload your YAML configuration file +#. The system will automatically validate the YAML syntax + +Usage +===== + +To use this module: + +#. Navigate to Code Generator menu +#. Create a new code generator record +#. Upload your YAML file - the system will validate it automatically +#. The filename must be unique across all code generators + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Contributors +------------ + +* Jeremi Joslin +* Edwin Gonzales + +Maintainers +----------- + +This module is maintained by OpenSPP.org. + +.. image:: https://openspp.org/logo.png + :alt: OpenSPP + :target: https://openspp.org + +OpenSPP is a digital platform for social protection programs. + diff --git a/spp_code_generator/__init__.py b/spp_code_generator/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/spp_code_generator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/spp_code_generator/__manifest__.py b/spp_code_generator/__manifest__.py new file mode 100644 index 000000000..beb768b3a --- /dev/null +++ b/spp_code_generator/__manifest__.py @@ -0,0 +1,31 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ + "name": "OpenSPP Code Generator", + "category": "OpenSPP", + "version": "17.0.1.3.1", + "summary": "This module generates codes for program registrants and groups based on specifications written in a YAML file.", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/openspp-modules", + "license": "LGPL-3", + "development_status": "Alpha", + "maintainers": ["jeremi", "gonzalesedwin1123"], + "depends": [ + "base", + "g2p_registry_base", + "g2p_registry_individual", + "g2p_registry_group", + "g2p_registry_membership", + "g2p_programs", + ], + "data": [ + "security/ir.model.access.csv", + "views/code_generator_views.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": False, +} diff --git a/spp_code_generator/models/__init__.py b/spp_code_generator/models/__init__.py new file mode 100644 index 000000000..f3b9d81e2 --- /dev/null +++ b/spp_code_generator/models/__init__.py @@ -0,0 +1 @@ +from . import code_generator diff --git a/spp_code_generator/models/code_generator.py b/spp_code_generator/models/code_generator.py new file mode 100644 index 000000000..1295568f2 --- /dev/null +++ b/spp_code_generator/models/code_generator.py @@ -0,0 +1,37 @@ +import base64 + +import yaml + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class CodeGenerator(models.Model): + _name = "spp.code.generator" + _description = "Code Generator" + + name = fields.Char(string="YAML Filename", required=True, index=True) + description = fields.Text(string="Description") + yaml_file = fields.Binary(string="YAML File", attachment=True) + + _sql_constraints = [ + ("name_unique", "UNIQUE(name)", "The YAML filename must be unique!"), + ] + + @api.constrains("yaml_file", "name") + def _validate_yaml_file(self): + """Validate that the uploaded file is a valid YAML file""" + for record in self: + if record.yaml_file and record.name: + # Check file extension + if not record.name.lower().endswith((".yaml", ".yml")): + raise ValidationError(_("Invalid file format. Filename must end with .yaml or .yml extension.")) + + # Validate YAML content + try: + yaml_content = base64.b64decode(record.yaml_file) + yaml.safe_load(yaml_content) + except yaml.YAMLError as e: + raise ValidationError(_("Invalid YAML file. Error: %s") % str(e)) from e + except Exception as e: + raise ValidationError(_("Failed to process YAML file. Error: %s") % str(e)) from e diff --git a/spp_code_generator/pyproject.toml b/spp_code_generator/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_code_generator/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_code_generator/security/ir.model.access.csv b/spp_code_generator/security/ir.model.access.csv new file mode 100644 index 000000000..5a2892ebf --- /dev/null +++ b/spp_code_generator/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_spp_code_generator_system,Code Generator System Access,model_spp_code_generator,base.group_system,1,1,1,1 +access_spp_code_generator_admin,Code Generator Admin Access,model_spp_code_generator,g2p_registry_base.group_g2p_admin,1,1,1,1 +access_spp_code_generator_user,Code Generator User Access,model_spp_code_generator,base.group_user,1,0,0,0 diff --git a/spp_code_generator/views/code_generator_views.xml b/spp_code_generator/views/code_generator_views.xml new file mode 100644 index 000000000..c358d9d54 --- /dev/null +++ b/spp_code_generator/views/code_generator_views.xml @@ -0,0 +1,74 @@ + + + + + spp.code.generator.tree + spp.code.generator + + + + + + + + + + + spp.code.generator.form + spp.code.generator + +
+ + + + + + + + + + + +
+
+
+ + + + spp.code.generator.search + spp.code.generator + + + + + + + + + + + Code Generators + spp.code.generator + tree,form + + +

+ Create your first YAML Configuration +

+

+ Upload YAML files to configure code generation. The system will validate the YAML syntax automatically. +

+
+
+ + + + + +
From 2e194219009d1a8b745cf5531d49243c070cf3f0 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 23 Oct 2025 11:37:43 +0800 Subject: [PATCH 2/8] [FIX] spp_code_generator: Fix the form view --- .../views/code_generator_views.xml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/spp_code_generator/views/code_generator_views.xml b/spp_code_generator/views/code_generator_views.xml index c358d9d54..75394f4ec 100644 --- a/spp_code_generator/views/code_generator_views.xml +++ b/spp_code_generator/views/code_generator_views.xml @@ -19,14 +19,25 @@
+
+
- - - - + + + +
From e69440dc39ea9a5d4026d200f9e3d64fd0c3abde Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 23 Oct 2025 11:53:55 +0800 Subject: [PATCH 3/8] [FIX] spp_code_generator: Added chatter in form UI --- spp_code_generator/__manifest__.py | 1 + spp_code_generator/models/code_generator.py | 3 ++- spp_code_generator/views/code_generator_views.xml | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/spp_code_generator/__manifest__.py b/spp_code_generator/__manifest__.py index beb768b3a..f488d38c2 100644 --- a/spp_code_generator/__manifest__.py +++ b/spp_code_generator/__manifest__.py @@ -12,6 +12,7 @@ "maintainers": ["jeremi", "gonzalesedwin1123"], "depends": [ "base", + "mail", "g2p_registry_base", "g2p_registry_individual", "g2p_registry_group", diff --git a/spp_code_generator/models/code_generator.py b/spp_code_generator/models/code_generator.py index 1295568f2..5475f5338 100644 --- a/spp_code_generator/models/code_generator.py +++ b/spp_code_generator/models/code_generator.py @@ -9,10 +9,11 @@ class CodeGenerator(models.Model): _name = "spp.code.generator" _description = "Code Generator" + _inherit = ["mail.thread", "mail.activity.mixin"] name = fields.Char(string="YAML Filename", required=True, index=True) description = fields.Text(string="Description") - yaml_file = fields.Binary(string="YAML File", attachment=True) + yaml_file = fields.Binary(string="YAML File", attachment=True, tracking=True) _sql_constraints = [ ("name_unique", "UNIQUE(name)", "The YAML filename must be unique!"), diff --git a/spp_code_generator/views/code_generator_views.xml b/spp_code_generator/views/code_generator_views.xml index 75394f4ec..4b060e6bf 100644 --- a/spp_code_generator/views/code_generator_views.xml +++ b/spp_code_generator/views/code_generator_views.xml @@ -40,6 +40,11 @@ +
+ + + +
From 0610e2a24dfb3c7bd8360fb9ba49fd1d2d12048e Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 23 Oct 2025 12:22:36 +0800 Subject: [PATCH 4/8] [UPD] spp_code_generator: Added the action_process_entities function --- spp_code_generator/IMPLEMENTATION_GUIDE.md | 439 ++++++++++++++++++ spp_code_generator/models/code_generator.py | 258 +++++++++- .../views/code_generator_views.xml | 27 ++ 3 files changed, 723 insertions(+), 1 deletion(-) create mode 100644 spp_code_generator/IMPLEMENTATION_GUIDE.md diff --git a/spp_code_generator/IMPLEMENTATION_GUIDE.md b/spp_code_generator/IMPLEMENTATION_GUIDE.md new file mode 100644 index 000000000..df066a6f0 --- /dev/null +++ b/spp_code_generator/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,439 @@ +# Code Generator Module - Entity Processing Implementation Guide + +## Overview + +This document explains the implementation of the YAML entity processing feature in the `spp_code_generator` +module. The feature reads YAML specification files and automatically creates custom fields in `res.partner` +based on the entity definitions. + +## Architecture + +### Data Flow + +``` +YAML File Upload → Validation → Parse → Extract Entities → Create Fields → Log Results +``` + +### Components + +1. **Model**: `spp.code.generator` (`models/code_generator.py`) +2. **View**: Form with "Process Entities" button (`views/code_generator_views.xml`) +3. **Target**: `res.partner` model (where fields are created) + +--- + +## Model Fields + +### Core Fields + +| Field | Type | Description | +| ---------------- | --------- | ----------------------------------- | +| `name` | Char | YAML filename (auto-populated) | +| `yaml_file` | Binary | The uploaded YAML file | +| `description` | Text | User-provided description | +| `state` | Selection | Processing status (draft/processed) | +| `processing_log` | Text | Detailed log of processing results | + +### States + +- **draft**: Initial state when YAML is uploaded +- **processed**: After successful entity processing + +--- + +## Key Methods + +### 1. `_load_yaml_content()` + +**Purpose**: Load and parse the YAML file content + +**Process**: + +1. Check if YAML file exists +2. Decode binary content (base64) +3. Parse YAML using `yaml.safe_load()` +4. Return Python dictionary + +**Returns**: `dict` - Parsed YAML content + +**Raises**: `UserError` if file missing or parsing fails + +--- + +### 2. `_get_odoo_field_type(yaml_field_type)` + +**Purpose**: Map YAML field types to Odoo field types + +**Type Mappings**: + +```python +"string" → "char" +"date" → "date" +"datetime" → "datetime" +"enum" → "selection" +"boolean" → "boolean" +"integer" → "integer" +"float" → "float" +"text" → "text" +"admin_code"→ "char" +``` + +**Args**: + +- `yaml_field_type` (str): Type from YAML spec + +**Returns**: `str` - Odoo field type + +--- + +### 3. `_create_field_from_spec(entity_name, field_spec)` + +**Purpose**: Create a single field in `res.partner` from YAML specification + +**Process**: + +1. Extract field specification (id, label, type, required, etc.) +2. Generate field name with OpenSPP prefix: `z_cst_{field_id}` +3. Check if field already exists (skip if yes) +4. Map YAML type to Odoo type +5. Prepare field values dict +6. Handle special cases (enum/selection fields) +7. Create field using `ir.model.fields` + +**Field Naming Convention**: + +``` +YAML: "hh_id" +Odoo: "z_cst_hh_id" + +Prefix breakdown: +- z_ : OpenSPP custom field prefix +- cst_ : Custom (not indicator) +- hh_id : Original field ID from YAML +``` + +**Args**: + +- `entity_name` (str): Entity name (e.g., "Household") +- `field_spec` (dict): Field specification from YAML + +**Field Spec Structure**: + +```yaml +{ + "id": "field_name", # Required: Technical name + "label": "Field Label", # Required: Display label + "type": "string", # Required: Field type + "required": True/False, # Optional: Is required? + "values": [...], # Optional: For enums + "description": "...", # Optional: Help text +} +``` + +**Returns**: `ir.model.fields` record or `None` if exists + +--- + +### 4. `action_process_entities()` + +**Purpose**: Main processing method - button action + +**Process**: + +1. Load YAML content +2. Extract "entities" section +3. Initialize processing log +4. For each entity: + - Get entity details (name, label, fields) + - For each field: + - Call `_create_field_from_spec()` + - Log result (created/skipped/error) +5. Generate summary +6. Update `state` to "processed" +7. Post message in chatter +8. Show success notification + +**Returns**: `dict` - Client action (notification) + +**Logging**: Creates detailed processing log showing: + +- Each entity processed +- Each field created/skipped +- Any errors encountered +- Summary statistics + +--- + +## YAML Structure Example + +Based on `4ps_best_practice_example_v7.yaml`: + +```yaml +entities: + - name: "Household" + label: "4Ps Household" + fields: + - id: "hh_id" + label: "Household ID" + type: "string" + required: true + - id: "province" + label: "Province" + type: "admin_code" + + - name: "Individual" + label: "Member" + fields: + - id: "person_id" + label: "Member ID" + type: "string" + required: true + - id: "birthdate" + label: "Date of Birth" + type: "date" + required: true + - id: "gender" + label: "Gender" + type: "enum" + values: ["Female", "Male"] + required: true +``` + +--- + +## Field Creation Results + +### Example Output + +For the above YAML, the following fields would be created in `res.partner`: + +| YAML Field ID | Odoo Field Name | Type | Required | +| ------------- | --------------- | --------- | -------- | +| hh_id | z_cst_hh_id | Char | Yes | +| province | z_cst_province | Char | No | +| person_id | z_cst_person_id | Char | Yes | +| birthdate | z_cst_birthdate | Date | Yes | +| gender | z_cst_gender | Selection | Yes | + +### Selection Field Example + +For `gender` enum field: + +```python +Field: z_cst_gender +Type: Selection +Values: [('Female', 'Female'), ('Male', 'Male')] +``` + +--- + +## User Interface + +### Form View Features + +1. **Header**: + + - Status bar (Draft → Processed) + - "Process Entities" button (visible only in draft state) + +2. **Main Section**: + + - YAML filename (large title, readonly) + - File upload widget + - Description field + +3. **Processing Log Tab**: + + - Shows detailed processing results + - Only visible after processing + +4. **Chatter**: + - Posts summary message after processing + - Shows fields created/skipped counts + +### Tree View + +- Displays filename, state (with colored badge), and description +- State badge colors: + - Draft: Blue + - Processed: Green + +### Search/Filters + +- Filter by state (Draft/Processed) +- Filter by has YAML file +- Group by status + +--- + +## Processing Log Example + +``` +================================================================================ +Processing YAML File: 4ps_best_practice_example_v7.yaml +Started at: 2025-10-23 10:30:00 +================================================================================ + +Entity: Household (4Ps Household) +---------------------------------------- + ✓ Created: hh_id (Household ID) - Type: string + ✓ Created: province (Province) - Type: admin_code + +Entity: Individual (Member) +---------------------------------------- + ✓ Created: person_id (Member ID) - Type: string + ✓ Created: birthdate (Date of Birth) - Type: date + ✓ Created: gender (Gender) - Type: enum + ⊗ Skipped: education_enrollment_status (Education Status) - Already exists + +================================================================================ +SUMMARY +================================================================================ +Total fields created: 5 +Total fields skipped: 1 +Completed at: 2025-10-23 10:30:15 +================================================================================ +``` + +--- + +## Error Handling + +### Validation Errors + +- File extension validation (must be .yaml or .yml) +- YAML syntax validation +- Missing required fields in spec + +### Processing Errors + +- No YAML file uploaded → `UserError` +- Invalid YAML syntax → `UserError` with details +- No "entities" section → `UserError` +- Field creation failure → Logged, processing continues + +### Duplicate Fields + +- Existing fields are skipped (not recreated) +- Logged as "Skipped" in processing log +- No error raised + +--- + +## OpenSPP Conventions + +### Field Naming + +Follows OpenSPP field naming patterns: + +- `z_`: General prefix for custom fields +- `cst_`: Custom/arbitrary fields (not indicators) +- Example: `z_cst_hh_id` + +### Why This Pattern? + +1. **z\_**: Groups all custom fields together +2. **cst\_**: Distinguishes from indicators (`ind_`) +3. **Prevents conflicts**: With standard Odoo fields + +--- + +## Best Practices + +### 1. YAML File Preparation + +- Ensure all required fields (id, label, type) are present +- Use consistent naming conventions +- Provide descriptions for clarity +- Validate YAML syntax before upload + +### 2. Field Types + +- Use appropriate types (date for dates, enum for choices) +- Provide enum values for selection fields +- Mark fields as required when appropriate + +### 3. Processing + +- Review the processing log after processing +- Check for skipped fields (may indicate duplicates) +- Check chatter for summary message + +### 4. Testing + +- Test with small YAML files first +- Verify fields are created correctly in res.partner +- Check field properties (type, required, etc.) + +--- + +## Troubleshooting + +### Issue: "No YAML file uploaded" + +**Solution**: Upload a YAML file before clicking "Process Entities" + +### Issue: "Invalid YAML file" + +**Solution**: Validate YAML syntax using online validator or IDE + +### Issue: "No 'entities' section found" + +**Solution**: Ensure YAML has an `entities` key at the root level + +### Issue: Fields not appearing + +**Solution**: + +1. Check processing log for errors +2. Verify field names don't conflict with existing fields +3. Restart Odoo server to reload model definitions + +--- + +## Future Enhancements + +Potential additions for future versions: + +1. **Derived Fields Processing**: Handle computed/indicator fields +2. **View Generation**: Auto-create form/tree views for entities +3. **Relationship Handling**: Process one-to-many, many-to-many relationships +4. **Rollback Feature**: Ability to delete created fields +5. **Preview Mode**: Show what would be created before actually creating +6. **Batch Processing**: Process multiple YAML files at once +7. **Field Updates**: Update existing fields instead of skipping + +--- + +## Technical Notes + +### Performance + +- Fields are created one at a time (not batched) +- Processing is synchronous (no queue jobs) +- Suitable for <100 fields per YAML file + +### Database Impact + +- Creates records in `ir.model.fields` +- Modifies PostgreSQL schema (adds columns to `res_partner` table) +- Changes are permanent (manual deletion required to remove) + +### Security + +- Requires System Administrator access to create fields +- Follows Odoo's standard security model +- Field creation logged in processing log and chatter + +--- + +## References + +- OpenSPP Field Naming Conventions: See `openspp-fields-naming` cursor rule +- YAML Specification: `4ps_best_practice_example_v7.yaml` +- Odoo Fields Documentation: + https://www.odoo.com/documentation/17.0/developer/reference/backend/orm.html#fields + +--- + +**Last Updated**: 2025-10-23 **Module Version**: 17.0.1.3.1 **Author**: OpenSPP.org diff --git a/spp_code_generator/models/code_generator.py b/spp_code_generator/models/code_generator.py index 5475f5338..cae9640d1 100644 --- a/spp_code_generator/models/code_generator.py +++ b/spp_code_generator/models/code_generator.py @@ -1,9 +1,12 @@ import base64 +import logging import yaml from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError + +_logger = logging.getLogger(__name__) class CodeGenerator(models.Model): @@ -14,6 +17,16 @@ class CodeGenerator(models.Model): name = fields.Char(string="YAML Filename", required=True, index=True) description = fields.Text(string="Description") yaml_file = fields.Binary(string="YAML File", attachment=True, tracking=True) + state = fields.Selection( + [ + ("draft", "Draft"), + ("processed", "Processed"), + ], + string="Status", + default="draft", + tracking=True, + ) + processing_log = fields.Text(string="Processing Log", readonly=True) _sql_constraints = [ ("name_unique", "UNIQUE(name)", "The YAML filename must be unique!"), @@ -36,3 +49,246 @@ def _validate_yaml_file(self): raise ValidationError(_("Invalid YAML file. Error: %s") % str(e)) from e except Exception as e: raise ValidationError(_("Failed to process YAML file. Error: %s") % str(e)) from e + + def _load_yaml_content(self): + """ + Load and parse the YAML file content. + + Returns: + dict: Parsed YAML content as a Python dictionary + + Raises: + UserError: If YAML file is not uploaded or cannot be parsed + """ + self.ensure_one() + + if not self.yaml_file: + raise UserError(_("No YAML file uploaded. Please upload a YAML file first.")) + + try: + # Decode the binary content + yaml_content = base64.b64decode(self.yaml_file) + # Parse YAML + yaml_data = yaml.safe_load(yaml_content) + _logger.info("Successfully loaded YAML file: %s", self.name) + return yaml_data + except yaml.YAMLError as e: + raise UserError(_("Failed to parse YAML file: %s") % str(e)) from e + except Exception as e: + raise UserError(_("Error loading YAML file: %s") % str(e)) from e + + def _get_odoo_field_type(self, yaml_field_type): + """ + Map YAML field type to Odoo field type. + + Args: + yaml_field_type (str): Field type from YAML (e.g., 'string', 'date', 'enum') + + Returns: + str: Corresponding Odoo field type (e.g., 'char', 'date', 'selection') + """ + type_mapping = { + "string": "char", + "date": "date", + "datetime": "datetime", + "enum": "selection", + "boolean": "boolean", + "integer": "integer", + "float": "float", + "text": "text", + "admin_code": "char", # Administrative code, treated as char + } + return type_mapping.get(yaml_field_type, "char") + + def _create_field_from_spec(self, entity_name, field_spec): + """ + Create an Odoo field in res.partner based on field specification from YAML. + + Args: + entity_name (str): Name of the entity (e.g., 'Household', 'Individual') + field_spec (dict): Field specification containing id, label, type, etc. + + Returns: + ir.model.fields: Created field record or None if field already exists + + Field Specification Structure: + { + 'id': 'field_name', # Required: Field technical name + 'label': 'Field Label', # Required: Field display label + 'type': 'string', # Required: Field type + 'required': True/False, # Optional: Whether field is required + 'values': [...], # Optional: For enum types, list of allowed values + 'description': '...' # Optional: Help text for the field + } + """ + self.ensure_one() + + # Extract field specifications + field_id = field_spec.get("id") + field_label = field_spec.get("label") + field_type = field_spec.get("type") + is_required = field_spec.get("required", False) + enum_values = field_spec.get("values", []) + field_description = field_spec.get("description", "") + + if not all([field_id, field_label, field_type]): + _logger.warning("Skipping field with incomplete specification: %s", field_spec) + return None + + # Prepare field name with prefix based on OpenSPP conventions + # z_cst_ prefix for custom fields + field_name = f"z_cst_{field_id}" + + # Check if field already exists + IrModelFields = self.env["ir.model.fields"] + existing_field = IrModelFields.search([("model", "=", "res.partner"), ("name", "=", field_name)], limit=1) + + if existing_field: + _logger.info("Field %s already exists in res.partner, skipping creation", field_name) + return existing_field + + # Get the res.partner model ID + partner_model = self.env["ir.model"].search([("model", "=", "res.partner")], limit=1) + if not partner_model: + raise UserError(_("res.partner model not found in the system")) + + # Map YAML type to Odoo type + odoo_field_type = self._get_odoo_field_type(field_type) + + # Prepare field values + field_values = { + "name": field_name, + "field_description": field_label, + "model_id": partner_model.id, + "model": "res.partner", + "ttype": odoo_field_type, + "required": is_required, + "help": field_description or f"Custom field for {entity_name}: {field_label}", + "state": "manual", # Manual fields can be deleted + } + + # Handle selection/enum fields + if odoo_field_type == "selection" and enum_values: + # Convert list of values to Odoo selection format + # Format for selection_ids: [(0, 0, {'value': 'key', 'name': 'Label'}), ...] + field_values["selection_ids"] = [(0, 0, {"value": val, "name": val}) for val in enum_values] + + try: + # Create the field + new_field = IrModelFields.create(field_values) + _logger.info("Created field %s in res.partner for entity %s", field_name, entity_name) + return new_field + except Exception as e: + _logger.error("Failed to create field %s: %s", field_name, str(e)) + raise UserError(_("Failed to create field %s: %s") % (field_name, str(e))) from e + + def action_process_entities(self): + """ + Process the 'entities' section from the YAML file and create fields in res.partner. + + This method: + 1. Loads and parses the YAML file + 2. Extracts the 'entities' section + 3. For each entity, creates corresponding fields in res.partner + 4. Logs the processing results + 5. Updates the record state to 'processed' + + Returns: + dict: Action to reload the form view with a success message + """ + self.ensure_one() + + # Load YAML content + yaml_data = self._load_yaml_content() + + # Extract entities section + entities = yaml_data.get("entities", []) + if not entities: + raise UserError(_("No 'entities' section found in the YAML file")) + + # Initialize processing log + log_lines = [ + "=" * 80, + f"Processing YAML File: {self.name}", + f"Started at: {fields.Datetime.now()}", + "=" * 80, + "", + ] + + created_fields_count = 0 + skipped_fields_count = 0 + + # Process each entity + for entity in entities: + entity_name = entity.get("name") + entity_label = entity.get("label", entity_name) + entity_fields = entity.get("fields", []) + + log_lines.append(f"Entity: {entity_name} ({entity_label})") + log_lines.append("-" * 40) + + # Process each field in the entity + for field_spec in entity_fields: + field_id = field_spec.get("id") + field_label = field_spec.get("label") + field_type = field_spec.get("type") + + try: + result = self._create_field_from_spec(entity_name, field_spec) + if result: + created_fields_count += 1 + log_lines.append(f" ✓ Created: {field_id} ({field_label}) - Type: {field_type}") + else: + skipped_fields_count += 1 + log_lines.append(f" ⊗ Skipped: {field_id} ({field_label}) - Already exists") + except Exception as e: + log_lines.append(f" ✗ Error: {field_id} - {str(e)}") + _logger.error("Error creating field %s: %s", field_id, str(e)) + + log_lines.append("") + + # Summary + log_lines.extend( + [ + "=" * 80, + "SUMMARY", + "=" * 80, + f"Total fields created: {created_fields_count}", + f"Total fields skipped: {skipped_fields_count}", + f"Completed at: {fields.Datetime.now()}", + "=" * 80, + ] + ) + + # Update processing log + processing_log = "\n".join(log_lines) + self.write( + { + "processing_log": processing_log, + "state": "processed", + } + ) + + # Post message in chatter + self.message_post( + body=_( + "Entity processing completed successfully.
" + "Fields created: %s
" + "Fields skipped: %s" + ) + % (created_fields_count, skipped_fields_count), + subject="YAML Processing Complete", + ) + + # Return action to show success message and reload form + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Success"), + "message": _("Entity processing completed. Created %s fields, skipped %s fields.") + % (created_fields_count, skipped_fields_count), + "type": "success", + "sticky": False, + }, + } diff --git a/spp_code_generator/views/code_generator_views.xml b/spp_code_generator/views/code_generator_views.xml index 4b060e6bf..29f18e987 100644 --- a/spp_code_generator/views/code_generator_views.xml +++ b/spp_code_generator/views/code_generator_views.xml @@ -8,6 +8,12 @@ +
@@ -18,6 +24,16 @@ spp.code.generator
+
+