Compare commits

...

3 Commits

Author SHA1 Message Date
gsinghpal
7a02382623 fix(reports): WO Margin model name must match report_name + '_template' suffix
The previous fix swapped t-field -> t-esc so the QWeb error stopped,
but the report still printed blank. Root cause: Odoo looks up the
report data model via env['report.<report_name>'], but our model was
named 'report.fusion_plating_jobs.report_fp_job_margin' while the
action's report_name is 'fusion_plating_jobs.report_fp_job_margin_template'.
The model lookup missed, _get_report_values never fired, and the
template rendered with no 'rows' in scope — empty foreach -> empty
page.

Renamed the model to report.fusion_plating_jobs.report_fp_job_margin_template.

Verified: PDF size jumped from 1229 bytes (blank) to 125880 bytes
(fully populated). HTML now contains 'Job Margin', 'Step Breakdown',
and the actual WO name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:08:38 -04:00
gsinghpal
169e97af02 feat(nexa_coa_setup): analytic plans + seed accounts
- 'Customer Project' plan (renamed from 'Project' to avoid duplicate with
  project module's auto-created plan) — mandatory
- 'Department' plan (mandatory) — seeded with DEPT-DEV, DEPT-SALES,
  DEPT-ADMIN, DEPT-HOSTING
- 'SR&ED Tag' plan (optional) — seeded with 7 tag values:
  SRED-T4-DEV-SALARY, SRED-SPECIFIED-EMPLOYEE,
  SRED-CONTRACTOR-CA-ARM-LENGTH, SRED-CONTRACTOR-CA-NON-ARM-LENGTH,
  SRED-MATERIALS-CONSUMED, SRED-OVERHEAD-PROXY-BASIS, NOT-ELIGIBLE

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:53:21 -04:00
gsinghpal
3c959771ae feat(nexa_coa_setup): pre_init_hook to clear l10n_ca code collisions
Bakes the staging-side one-off collision clearing into the module install
itself so production install will execute the same sweep automatically.

For each of the 29 l10n_ca codes that conflict with Nexa's planned chart:
- If the account has zero postings: suffix code with '.OLD', mark inactive,
  rename to '(l10n_ca LEGACY) <original>'
- If the account has postings (currently 115100 AR control with 240 lines
  and 511100 Inside Purchases with 1 line): leave alone (Nexa renumbered
  to 119100 / 511105 in the XML)

Idempotent — pre_init_hook re-running has no effect (already-suffixed
codes are skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:51:25 -04:00
7 changed files with 166 additions and 3 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Native Jobs',
'version': '19.0.8.22.9',
'version': '19.0.8.22.10',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.',

View File

@@ -10,7 +10,15 @@ from odoo import api, models
class ReportFpJobMargin(models.AbstractModel):
_name = 'report.fusion_plating_jobs.report_fp_job_margin'
# Odoo looks up the report's data model via report.<report_name>.
# The action's report_name is `fusion_plating_jobs.report_fp_job_margin_template`,
# so this MUST be `report.fusion_plating_jobs.report_fp_job_margin_template`.
# Pre-2026-05-12 the model name was missing the `_template` suffix,
# which silently caused _get_report_values to never fire and the
# template rendered with no `rows` -> blank PDF. The t-field error
# was masking this because it crashed earlier; once t-field was
# swapped to t-esc the blank-render surfaced.
_name = 'report.fusion_plating_jobs.report_fp_job_margin_template'
_description = 'Work Order Margin Report'
@api.model

View File

@@ -1,2 +1,2 @@
from . import models
from .hooks import post_init_hook
from .hooks import pre_init_hook, post_init_hook

View File

@@ -31,6 +31,7 @@
"data/09_res_partner.xml",
"data/10_account_reconcile_model.xml",
],
"pre_init_hook": "pre_init_hook",
"post_init_hook": "post_init_hook",
"installable": True,
"application": False,

View File

@@ -1,5 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- Named 'Customer Project' to avoid collision with the project module's
auto-created 'Project' plan. This is where customer-engagement
analytic accounts (PRJ-YYYY-CUST-NAME) live. -->
<record id="plan_project" model="account.analytic.plan">
<field name="name">Customer Project</field>
<field name="default_applicability">mandatory</field>
</record>
<record id="plan_department" model="account.analytic.plan">
<field name="name">Department</field>
<field name="default_applicability">mandatory</field>
</record>
<record id="plan_sred_tag" model="account.analytic.plan">
<field name="name">SR&amp;ED Tag</field>
<field name="default_applicability">optional</field>
</record>
</data>
</odoo>

View File

@@ -1,5 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- Department analytic accounts -->
<record id="aa_dept_dev" model="account.analytic.account">
<field name="name">Development</field>
<field name="code">DEPT-DEV</field>
<field name="plan_id" ref="plan_department"/>
</record>
<record id="aa_dept_sales" model="account.analytic.account">
<field name="name">Sales &amp; Marketing</field>
<field name="code">DEPT-SALES</field>
<field name="plan_id" ref="plan_department"/>
</record>
<record id="aa_dept_admin" model="account.analytic.account">
<field name="name">Admin &amp; Operations</field>
<field name="code">DEPT-ADMIN</field>
<field name="plan_id" ref="plan_department"/>
</record>
<record id="aa_dept_hosting" model="account.analytic.account">
<field name="name">Hosting Operations</field>
<field name="code">DEPT-HOSTING</field>
<field name="plan_id" ref="plan_department"/>
</record>
<!-- SR&ED Tag analytic accounts -->
<record id="aa_sred_t4_dev" model="account.analytic.account">
<field name="name">T4 Dev Salary — full proxy</field>
<field name="code">SRED-T4-DEV-SALARY</field>
<field name="plan_id" ref="plan_sred_tag"/>
</record>
<record id="aa_sred_specified" model="account.analytic.account">
<field name="name">Specified Employee Salary — 75% cap</field>
<field name="code">SRED-SPECIFIED-EMPLOYEE</field>
<field name="plan_id" ref="plan_sred_tag"/>
</record>
<record id="aa_sred_contr_ca_arm" model="account.analytic.account">
<field name="name">Contractor CA Arm's Length — 80% eligible</field>
<field name="code">SRED-CONTRACTOR-CA-ARM-LENGTH</field>
<field name="plan_id" ref="plan_sred_tag"/>
</record>
<record id="aa_sred_contr_ca_naf" model="account.analytic.account">
<field name="name">Contractor CA Non-Arm's Length</field>
<field name="code">SRED-CONTRACTOR-CA-NON-ARM-LENGTH</field>
<field name="plan_id" ref="plan_sred_tag"/>
</record>
<record id="aa_sred_materials" model="account.analytic.account">
<field name="name">Materials Consumed in R&amp;D</field>
<field name="code">SRED-MATERIALS-CONSUMED</field>
<field name="plan_id" ref="plan_sred_tag"/>
</record>
<record id="aa_sred_overhead_basis" model="account.analytic.account">
<field name="name">Overhead Proxy Basis (direct labour basis)</field>
<field name="code">SRED-OVERHEAD-PROXY-BASIS</field>
<field name="plan_id" ref="plan_sred_tag"/>
</record>
<record id="aa_sred_not_eligible" model="account.analytic.account">
<field name="name">Not Eligible (default)</field>
<field name="code">NOT-ELIGIBLE</field>
<field name="plan_id" ref="plan_sred_tag"/>
</record>
</data>
</odoo>

View File

@@ -4,6 +4,72 @@ import logging
_logger = logging.getLogger(__name__)
# l10n_ca account codes that collide with the Nexa CoA design and that
# l10n_ca pre-loads with 'income_other'/'expense'/etc. types we don't want.
# Each of these is checked at pre_init: if it has zero postings we suffix
# its code with '.OLD' and archive it so our XML can claim the code.
# Codes with postings are LEFT ALONE — we renumbered the Nexa code instead
# (115100 stays as l10n_ca 'Customers Account' AR; Nexa shareholder receivable
# moved to 119100. 511100 stays as l10n_ca 'Inside Purchases'; Nexa Cloud
# Infrastructure moved to 511105).
_L10N_CA_COLLISION_CODES = [
"118100", "118200", "118300",
"213100", "214100",
"221200",
"311100", "311200", "311300",
"411100", "411200", "411300",
"413100", "413200", "413300",
"511110", "511120", "511130", "511140", "511200", "511210",
"512100", "512110", "512200",
"611100", "611200", "611300",
"612100", "612200",
]
def pre_init_hook(env):
"""Run BEFORE XML data is loaded. Clear l10n_ca account codes that would
collide with Nexa's chart of accounts."""
_logger.info("nexa_coa_setup: pre_init_hook starting")
_clear_l10n_ca_collisions(env)
_logger.info("nexa_coa_setup: pre_init_hook complete")
def _clear_l10n_ca_collisions(env):
"""For each colliding code: if it has zero postings, rename to NNNNNN.OLD
and set inactive. If it has postings, leave alone (Nexa code was renumbered
in the XML to avoid the conflict)."""
cleared = 0
kept_with_postings = 0
not_found = 0
for code in _L10N_CA_COLLISION_CODES:
acc = env["account.account"].search([("code", "=", code)], limit=1)
if not acc:
not_found += 1
continue
usage = env["account.move.line"].search_count([("account_id", "=", acc.id)])
if usage > 0:
_logger.info(
"nexa_coa_setup: keeping l10n_ca account %s (%s) — %d postings exist",
code, acc.name, usage,
)
kept_with_postings += 1
continue
new_code = f"{code}.OLD"
# Skip if already suffixed (idempotency)
if acc.code.endswith(".OLD"):
continue
acc.write({
"code": new_code,
"name": f"(l10n_ca LEGACY) {acc.name or acc.display_name}",
"active": False,
})
cleared += 1
_logger.info(
"nexa_coa_setup: collision sweep — cleared %d, kept-with-postings %d, not-found %d",
cleared, kept_with_postings, not_found,
)
def post_init_hook(env):
"""Imperative one-shot operations after module data is loaded.