From 485756212d54896adf127edaf57b09ea43cd1817 Mon Sep 17 00:00:00 2001 From: Virginia Date: Thu, 29 Jan 2026 16:02:51 +0000 Subject: [PATCH 1/6] [ADD] pot_github_push: backport --- pot_github_push/README.rst | 64 ++++++++ pot_github_push/__init__.py | 33 +++++ pot_github_push/__manifest__.py | 12 ++ pot_github_push/wizard/__init__.py | 1 + .../wizard/pot_generator_wizard.py | 138 ++++++++++++++++++ 5 files changed, 248 insertions(+) create mode 100644 pot_github_push/README.rst create mode 100644 pot_github_push/__init__.py create mode 100644 pot_github_push/__manifest__.py create mode 100644 pot_github_push/wizard/__init__.py create mode 100644 pot_github_push/wizard/pot_generator_wizard.py diff --git a/pot_github_push/README.rst b/pot_github_push/README.rst new file mode 100644 index 00000000..845471a4 --- /dev/null +++ b/pot_github_push/README.rst @@ -0,0 +1,64 @@ +.. |company| replace:: ADHOC SA + +.. |company_logo| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-logo.png + :alt: ADHOC SA + :target: https://www.adhoc.com.ar + +.. |icon| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-icon.png + +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +============= +POT Generator +============= + +Automatic POT (Portable Object Template) file generator for Odoo modules with GitHub API integration. + +Features +======== + +**POT Generation** + - Generate .pot files using Odoo's native ``trans_export`` + - Direct GitHub API push (no local Git required) + - Smart content comparison (ignores timestamp changes) + +**Integration** + - Runbot compatible execution + - Auto-execution on module installation + - Environment variable configuration + +Configuration +============= + +Set environment variables for GitHub integration:: + + export GITHUB_TOKEN="your_github_token" + export GITHUB_REPO_OWNER="your_organization" + export GITHUB_REPO_NAME="your_repository" + export GITHUB_BRANCH="your_branch" + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: http://runbot.adhoc.com.ar/ + +Credits +======= + +Images +------ + +* |company| |icon| + +Contributors +------------ + +Maintainer +---------- + +|company_logo| + +This module is maintained by the |company|. + +To contribute to this module, please visit https://www.adhoc.com.ar. diff --git a/pot_github_push/__init__.py b/pot_github_push/__init__.py new file mode 100644 index 00000000..56c6798f --- /dev/null +++ b/pot_github_push/__init__.py @@ -0,0 +1,33 @@ +from . import wizard + +import logging +import ast +import os + +_logger = logging.getLogger(__name__) + + +def post_init_hook(env): + """Auto-generate POT files on installation + + Environment variables: + - MODULE_INFO: Dict with tuple key (repo_owner, repo_name) and modules list as value + {("owner", "repo"): ["module1", "module2"], ...} + - GITHUB_TOKEN: GitHub token (required) + - GITHUB_BRANCH: Target branch (required) + """ + module_info = os.getenv("MODULE_INFO", "{}") + github_token = os.getenv("GITHUB_TOKEN") + github_branch = os.getenv("GITHUB_BRANCH") + + if not module_info or module_info == "{}": + _logger.info("No modules specified for POT generation (MODULE_INFO)") + return False + + try: + module_info = ast.literal_eval(module_info) + except Exception as e: + _logger.error("Error parsing MODULE_INFO: %s", str(e)) + return False + + env["pot.generator"]._generate_pots(module_info, github_token, github_branch) diff --git a/pot_github_push/__manifest__.py b/pot_github_push/__manifest__.py new file mode 100644 index 00000000..8bc33c02 --- /dev/null +++ b/pot_github_push/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "POT Generator", + "version": "18.0.1.0.0", + "category": "Tools", + "summary": "Helper module to generate POT files", + "author": "ADHOC SA", + "license": "AGPL-3", + "depends": ["base"], + "data": [], + "installable": True, + "post_init_hook": "post_init_hook", +} diff --git a/pot_github_push/wizard/__init__.py b/pot_github_push/wizard/__init__.py new file mode 100644 index 00000000..8d16e2f7 --- /dev/null +++ b/pot_github_push/wizard/__init__.py @@ -0,0 +1 @@ +from . import pot_generator_wizard diff --git a/pot_github_push/wizard/pot_generator_wizard.py b/pot_github_push/wizard/pot_generator_wizard.py new file mode 100644 index 00000000..158457e9 --- /dev/null +++ b/pot_github_push/wizard/pot_generator_wizard.py @@ -0,0 +1,138 @@ +import base64 +import contextlib +import io +import logging + +import requests +from odoo import api, models +from odoo.tools.translate import trans_export + +_logger = logging.getLogger(__name__) + + +class PotGenerator(models.AbstractModel): + _name = "pot.generator" + _description = "Simple POT Generator" + + @api.model + def _generate_pots(self, module_info, github_token, github_branch): + """Generate POT files for specified modules and push to GitHub + + :param module_info: Dict with tuple key (owner, repo) and modules list {("owner", "repo"): ["mod1"]} + :param github_token: GitHub API token + :param github_branch: Target branch name + """ + try: + for repo_key, module_names in module_info.items(): + # repo_key should be tuple (owner, repo) + if isinstance(repo_key, tuple): + repo_owner, repo_name = repo_key + else: + _logger.error("Invalid repo key type: %s", type(repo_key)) + continue + + for module_name in module_names: + content = self._generate_pot(module_name) + if content: + self._github_push(module_name, content, repo_owner, repo_name, github_token, github_branch) + return True + + except Exception as e: + _logger.exception("POT generation failed: %s", str(e)) + return False + + def _generate_pot(self, module_name): + """Generate single POT file""" + try: + # Get content using Odoo's trans_export + with contextlib.closing(io.BytesIO()) as buf: + trans_export(False, [module_name], buf, "po", self._cr) + return buf.getvalue().decode("utf-8") + except Exception as e: + _logger.exception("Failed POT generation for %s: %s", module_name, str(e)) + return False + + def _github_push(self, module_name, content, repo_owner, repo_name, github_token, branch): + """Push POT file to GitHub using API + + :param module_name: Name of the module + :param content: POT file content + :param repo_owner: GitHub repository owner + :param repo_name: GitHub repository name + :param github_token: GitHub API token + :param branch: Target branch name + """ + headers = {} + try: + # File path in repository + file_path = f"{module_name}/i18n/{module_name}.pot" + + # GitHub API headers + headers = {"Authorization": f"Bearer {github_token}", "Accept": "application/vnd.github.v3+json"} + + # Get current file SHA (if exists) + url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/contents/{file_path}" + params = {"ref": branch} + response = requests.get(url, headers=headers, params=params, timeout=30) + + sha = None + if response.status_code == 200: + file_info = response.json() + sha = file_info["sha"] + + # Compare content to avoid unnecessary pushes + existing_content = base64.b64decode(file_info["content"]).decode("utf-8") + if self._pot_content_equal(existing_content, content): + _logger.info("File %s content unchanged (ignoring timestamps), skipping push", file_path) + return True + + elif response.status_code == 404: + _logger.info("File %s does not exist, will create new", file_path) + else: + _logger.error("Error getting file info: %s", response.text) + return False + + content_encoded = base64.b64encode(content.encode("utf-8")).decode("utf-8") + + # Prepare commit data + commit_data = { + "message": f"[I18N] {module_name}: export source terms", + "content": content_encoded, + "branch": branch, + } + if sha: + commit_data["sha"] = sha + + # Push to GitHub + response = requests.put(url, json=commit_data, headers=headers, timeout=30) + if response.status_code in [200, 201]: + _logger.info("GitHub push completed for %s", module_name) + return True + else: + _logger.error("GitHub push failed for %s: %s", module_name, response.text) + return False + + except Exception as e: + _logger.error("GitHub push failed for %s: %s", module_name, str(e)) + return False + finally: + # Clear headers to avoid keeping sensitive token data in memory + headers.clear() + + def _pot_content_equal(self, content1, content2): + """Compare POT files ignoring timestamp changes""" + + def normalize_pot_content(content): + """Remove timestamp lines and normalize content for comparison""" + lines = content.strip().split("\n") + normalized_lines = [] + for line in lines: + # Skip POT-Creation-Date and PO-Revision-Date lines + if line.startswith('"POT-Creation-Date:') or line.startswith('"PO-Revision-Date:'): + continue + normalized_lines.append(line) + return "\n".join(normalized_lines) + + normalized1 = normalize_pot_content(content1) + normalized2 = normalize_pot_content(content2) + return normalized1 == normalized2 From bc8331753d40c5df20cc86ec62d1730f006847da Mon Sep 17 00:00:00 2001 From: Virginia Date: Thu, 29 Jan 2026 16:44:33 -0300 Subject: [PATCH 2/6] Update project.toml from template --- .copier-answers.yml | 2 +- .github/workflows/pre-commit.yml | 7 ++++++- .pre-commit-config.yaml | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 638b4ac4..ec83194c 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: 2f2f7c4 +_commit: a740779 _src_path: https://github.com/ingadhoc/addons-repo-template.git description: '' is_private: false diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 349c52d8..baa05dbf 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -6,8 +6,13 @@ name: pre-commit on: push: - branches: "[0-9][0-9].0" + branches: + - "1[8-9].0" + - "[2-9][0-9].0" pull_request_target: + branches: + - "1[8-9].0*" + - "[2-9][0-9].0*" jobs: pre-commit: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc269814..c4be55ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,8 @@ repos: - id: check-docstring-first - id: check-executables-have-shebangs - id: check-merge-conflict + args: ['--assume-in-merge'] + exclude: '\.rst$' - id: check-symlinks - id: check-xml - id: check-yaml From 69042b25b826302f7f5f884a89116cf25c1fcea4 Mon Sep 17 00:00:00 2001 From: Franco Leyes Date: Thu, 29 Jan 2026 20:40:32 +0000 Subject: [PATCH 3/6] [IMP] export_bg: clean data rows before writing to Excel closes ingadhoc/miscellaneous#362 Signed-off-by: Filoquin adhoc --- export_bg/models/export_bg_mixin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/export_bg/models/export_bg_mixin.py b/export_bg/models/export_bg_mixin.py index 234f6041..6a893e04 100644 --- a/export_bg/models/export_bg_mixin.py +++ b/export_bg/models/export_bg_mixin.py @@ -129,7 +129,8 @@ def _combine_chunks(self, export_id, export_format): ws.write_row(0, 0, chunk_data["headers"]) row_num = 1 for row in chunk_data["rows"]: - ws.write_row(row_num, 0, row) + cleaned_row = [str(cell) if isinstance(cell, (dict, list)) else cell for cell in row] + ws.write_row(row_num, 0, cleaned_row) row_num += 1 wb.close() chunks.unlink() From 6b4b4eaaade7af3a13838878461a30d63aa66cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roc=C3=ADo=20Vega?= Date: Mon, 9 Feb 2026 09:19:47 -0300 Subject: [PATCH 4/6] [FIX] export_bg: Handle import_compat parameter correctly in background export When import_compat=False, avoid using field 'value' for data extraction to prevent issues when the exported data is used for record updates. - In import_compat mode: use name -> value -> id fallback chain - In regular export mode: use only name -> id (skip 'value') - Field labels (headers) are handled appropriately for each mode This ensures exported data maintains proper field references based on the intended use case (import vs display/update). closes ingadhoc/miscellaneous#367 Signed-off-by: Franco Leyes --- export_bg/models/export_bg_mixin.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/export_bg/models/export_bg_mixin.py b/export_bg/models/export_bg_mixin.py index 6a893e04..96096902 100644 --- a/export_bg/models/export_bg_mixin.py +++ b/export_bg/models/export_bg_mixin.py @@ -39,8 +39,18 @@ def _export_chunk_bg(self, data, export_id, export_format): ] ) - field_names = [f.get("name") or f.get("value") or f.get("id") for f in params["fields"]] - field_labels = [f.get("label") or f.get("string") for f in params["fields"]] + # Extract field names considering import_compat mode + import_compat = params.get("import_compat", True) + + # For field_names (data extraction), always use the technical field name + # Only use 'value' as fallback when import_compat=True (for import compatibility) + if import_compat: + field_names = [f.get("name") or f.get("value") or f.get("id") for f in params["fields"]] + field_labels = field_names # Use field names as headers for import compatibility + else: + # When not import_compat, use only 'name' or 'id' for field_names, not 'value' + field_names = [f.get("name") or f.get("id") for f in params["fields"]] + field_labels = [f.get("label") or f.get("string") for f in params["fields"]] export_data = self.export_data(field_names).get("datas", []) From 14eb546d51670f7b543ae24814c220f6430a36af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roc=C3=ADo=20Vega?= Date: Thu, 12 Feb 2026 12:49:56 -0300 Subject: [PATCH 5/6] [FIX] export_bg: Handle binary values in JSON export Encode bytes/bytearray/memoryview values as base64 strings during JSON export. Remove the leftover debugger breakpoint. closes ingadhoc/miscellaneous#371 Signed-off-by: Franco Leyes --- export_bg/models/export_bg_mixin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/export_bg/models/export_bg_mixin.py b/export_bg/models/export_bg_mixin.py index 96096902..50dee5ca 100644 --- a/export_bg/models/export_bg_mixin.py +++ b/export_bg/models/export_bg_mixin.py @@ -14,6 +14,8 @@ class DateTimeEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, (datetime, date, time)): return obj.isoformat() + if isinstance(obj, (bytes, bytearray, memoryview)): + return base64.b64encode(bytes(obj)).decode() return super().default(obj) From a70c9952aae0218bf9a1f0717ba0cf42c8301a86 Mon Sep 17 00:00:00 2001 From: adhoc-cicd-bot <116299102+adhoc-cicd-bot@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:43:39 -0300 Subject: [PATCH 6/6] [UPD] Copilot instructions --- .github/copilot-instructions.md | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a30ad3cf..783d6e4d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -38,8 +38,6 @@ * Confirmar que todos los archivos usados (vistas, seguridad, datos, reportes, wizards) estén referenciados en el manifest. * Verificar dependencias declaradas: que no falten módulos requeridos ni se declaren innecesarios. -* **Regla de versión (obligatoria):** - Solo sugerir bump de versión si el `__manifest__.py` no incrementa `version` y se modificó la estructura de un modelo, una vista, o algún record .xml (ej. cambios en definición de campos, vistas XML, datos XML, seguridad). * Solo hacerlo una vez por revisión, aunque haya múltiples archivos afectados. --- @@ -61,7 +59,6 @@ * Verificar los archivos `ir.model.access.csv` para nuevos modelos: deben tener permisos mínimos necesarios. * No proponer abrir acceso global sin justificación. -* Si se agregan nuevos modelos o campos de control de acceso, **recordar el bump de versión** (ver sección de manifest). * Si se cambian `record rules`, revisar especialmente combinaciones multi-compañía y multi-website. ### Seguridad y rendimiento del ORM @@ -86,7 +83,7 @@ ## Cambios estructurales y scripts de migración – **cuestiones generales** -Cuando el diff sugiera **cambios de estructura de datos**, **siempre evaluar** si corresponde proponer un **script de migración** en `migrations/` (pre/post/end) **y recordar el bump de versión**. +Cuando el diff sugiera **cambios de estructura de datos**, **siempre evaluar** si corresponde proponer un **script de migración** en `migrations/` (pre/post/end). ### Reglas generales de estructura de `migrations/` @@ -283,7 +280,6 @@ def migrate(cr, registry): | ------------------ | -------------------------------------------------------------------------------------------------------- | | Modelos | Relaciones válidas; constraints; uso adecuado de `@api.depends`; `super()` correcto | | Vistas XML | Herencias correctas; campos válidos; adaptación a cambios de versión (p.ej. `` vs ``) | -| Manifest | **Bump de versión obligatorio** si hay cambios estructurales en modelos/vistas/records .xml; archivos referenciados | | Seguridad | Accesos mínimos necesarios; reglas revisadas | | Migraciones | **Si hay cambios estructurales, sugerir script en `migrations/` (pre/post/end)** y describir qué hace | | Rendimiento / ORM | Evitar loops costosos; no SQL innecesario; aprovechar las optimizaciones del ORM de la versión | @@ -291,15 +287,6 @@ def migrate(cr, registry): --- -## Heurística práctica para el bump de versión (general) - -* **SI** el diff modifica la estructura de un modelo, una vista, o algún record .xml (ej. cambios en definición de campos, vistas XML, datos XML, seguridad) - **Y** `__manifest__.py` no cambia `version` → **Sugerir bump**. -* **SI** hay scripts `migrations/pre_*.py` o `migrations/post_*.py` nuevos → **Sugerir al menos minor bump**. -* **SI** hay cambios que rompen compatibilidad (renombres, cambios de tipo con impacto, limpieza masiva de datos) → **Sugerir minor/major** según impacto. - ---- - ## Estilo del feedback (general) * Ser breve, claro y útil. Ejemplos: @@ -307,7 +294,7 @@ def migrate(cr, registry): * “El campo `partner_id` no se encuentra referenciado en la vista.” * “Este método redefine `write()` sin usar `super()`.” * “Tip: hay un error ortográfico en el nombre del parámetro.” - * **Bump + migración:** “Se renombra `old_ref` → `new_ref`: falta **bump de versión** y **pre-script** en `migrations/` para copiar valores antes del upgrade; añadir **post-script** para recompute del stored.” + * **Migración:** “Se renombra `old_ref` → `new_ref`: falta **pre-script** en `migrations/` para copiar valores antes del upgrade; añadir **post-script** para recompute del stored.” * Evitar explicaciones largas o reescrituras completas salvo que el cambio sea claro y necesario. * Priorizar comentarios en forma de **lista corta de puntos** (3–7 ítems) y frases breves en lugar de bloques de texto extensos. @@ -316,10 +303,10 @@ def migrate(cr, registry): ## Resumen operativo para Copilot -1. **Detecta cambios estructurales en modelos, vistas o records .xml → exige bump de `version` en `__manifest__.py` si no está incrementada.** -2. **Si hay cambio estructural (según la lista actualizada) → propone y describe script(s) de migración en `migrations/` (pre/post/end)**, con enfoque idempotente y en lotes. -3. Distingue entre: +1. **Si hay cambio estructural (según la lista actualizada) → propone y describe script(s) de migración en `migrations/` (pre/post/end)**, con enfoque idempotente y en lotes. +2. Distingue entre: * **cuestiones generales** (válidas para cualquier versión), * y **matices específicos de Odoo 18** (por ejemplo, uso de ``, passkeys, tours y comportamiento del framework). -4. Mantén el feedback **concreto, breve y accionable**. \ No newline at end of file + +3. Mantén el feedback **concreto, breve y accionable**. \ No newline at end of file