Compare commits

...

3 Commits

Author SHA1 Message Date
gsinghpal
8217bb0ff6 fix(fusion_accounting_reports): expose dynamic OWL reports as menu items
User reported that after Enterprise uninstall, clicking 'Reports' opened
PDF statements instead of the dynamic Fusion report viewer. Root cause:
the OWL ReportViewer (registered as view_type='fusion_reports') was only
reachable via the period-picker WIZARD; no menu items used the OWL view
directly. Plus the JS service ignored report_code, so even within the
viewer, all PnL-typed reports rendered the canonical P&L line_specs.

Changes:

JS layer
- reports_service.js: runReport now accepts and forwards reportCode;
  state tracks currentReportCode so re-runs after period/comparison
  changes preserve the variant.
- report_viewer.js: reads default_report_code (and default_comparison)
  from the action context.
- period_filter.js: passes the cached reportCode on date changes;
  clears it when the user picks a different report_type.

Backend
- New fusion_accounting_reports/views/report_actions.xml with 11
  dedicated ir.actions.act_window records, one per built-in report
  (P&L, Balance Sheet, Trial Balance, GL, Cash Flow, Executive Summary,
  Annual Statements, Aged Receivable, Aged Payable, Partner Ledger,
  Tax Summary). Each opens view_mode='fusion_reports' with the
  appropriate default_report_type + default_report_code context.
- views/menu_views.xml: each report now gets its own menu item
  directly under Accounting > Reporting (sequence 10-40), matching
  Enterprise's flat structure. Custom Period wizard, XLSX export and
  Anomaly browser collected under a 'Tools' sub-group at the bottom.
- fusion_accounting_l10n_ca: adds menu items for 'Profit and Loss
  (Canada)' and 'Balance Sheet (Canada)' as siblings, plus a 'Tax
  Returns (CA)' configuration menu.

Verified live on westin-v19:
- pnl rendering 3 rows, cash_flow 9, executive_summary 7,
  annual_statements 5, ca_profit_loss 9 \u2014 each report now renders
  its own line_specs correctly.
- Reporting menu shows 14 Fusion report entries + Tools group.
- 136/136 reports + l10n_ca tests pass.

Version bumps: reports 19.0.1.1.1, l10n_ca 19.0.1.1.0.

Made-with: Cursor
2026-04-20 01:11:48 -04:00
gsinghpal
867b5f71a1 fix(fusion_accounting): unified Accounting menu under one root, hide migration when Enterprise gone
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled
User reported two UX problems after the Enterprise uninstall:
1. Each Fusion sub-module showed up as its own standalone app in the
   launcher (Bank Reconciliation, Financial Reports, Asset Management,
   Customer Follow-ups, Fusion AI). Should look like ONE Accounting app.
2. Clicking the 'Fusion Accounting' app still opened the migration
   wizard even though Enterprise had been uninstalled and there was
   nothing to migrate.

Fix:
- Move all Fusion sub-module roots under the Community account.menu_finance
  hierarchy:
    * Bank Reconciliation \u2192 Accounting > Bank Reconciliation
    * Asset Management    \u2192 Accounting > Asset Management
    * Financial Reports   \u2192 Reporting > Financial Reports
    * Follow-ups          \u2192 Customers > Follow-ups
    * Fusion AI           \u2192 Configuration > Fusion AI
    * Migrate from Ent.   \u2192 Configuration > Migrate from Enterprise
- Rename Community's 'Invoicing' top-level menu to 'Accounting' (what
  Enterprise's accountant module did). Set the Fusion icon on it. This
  rename lives in the meta-module so it only fires when the full suite
  is installed.
- Add second computed group 'group_fusion_show_when_enterprise_present'
  (inverse of the existing 'absent' group). Migration menus are gated
  by this group, so they auto-hide once Enterprise is uninstalled.
- _fusion_recompute_coexistence_group now maintains both groups in lockstep.
- Meta-module now also depends on l10n_ca, hr_payroll, ocr, documents
  (the Phase 6/7 sub-modules) for one-click full-suite install.
- Fusion AI menu's old parent ('accountant.menu_accounting') was deleted
  with the Enterprise uninstall \u2014 reparented under Configuration.

Result: single 'Accounting' top-level menu containing the standard
V19 Community structure (Dashboard / Customers / Vendors / Accounting /
Reporting / Configuration), with all Fusion features slotted into the
appropriate sub-section. Verified live on westin-v19: 6 separate
Fusion top-level menus collapsed to 1; coexistence groups recomputed
(absent=10 users, present=0 users); 604/604 tests pass.

Version bump: all touched modules \u2192 19.0.1.1.0.

Made-with: Cursor
2026-04-20 01:04:49 -04:00
gsinghpal
bee5ba4d3f fix(plating): UAT-caught UX annoyances + lurking bugs
Five fixes from the end-to-end UAT debrief:

1. Menu discoverability (HIGH)
   Added a prominent "+ New Direct Order" button in the Sale Orders
   list header toolbar (class=btn-primary, display=always). The
   existing menuitem at Plating > Sales > New Direct Order was
   buried in a submenu that didn't always expand; the toolbar
   button is a guaranteed entry point from the most common screen.

2. Escape/X destroys wizard state (HIGH)
   Added a prominent info banner at the top of the wizard form:
   "Changes are not saved until you click Create & Confirm Order.
   Closing this window (Esc or X) discards your entries." The
   Cancel button now has confirm="Discard this order? All header
   data and line items will be lost." so the intentional-cancel
   path also prompts.

3. Shell/cron crash in _fp_auto_create_mo (MEDIUM)
   bridge_mrp/models/sale_order.py:232-264 used _() inside list
   comprehensions to format the internal chatter summary of newly
   created / adopted MOs. _() resolves language from env.context,
   which is empty in odoo-shell and cron contexts — triggering a
   translate.get_text_alias crash AFTER the MOs had been created.
   These strings are internal audit log text, not user-facing UI;
   dropped the _() wrappers so the message builds safely from any
   context. Same for the per-group error-message on savepoint
   rollback.

4. Misleading "100%" margin (MEDIUM)
   x_fc_margin_percent displayed 100% on every SO because the cost
   rollup from fp.coating.config.unit_cost isn't populated yet.
   Added x_fc_margin_available Boolean (True only when at least
   one line's coating has a non-zero unit_cost). The SO Plating
   tab now hides the margin numbers when margin_available=False
   and shows an inline muted note: "Margin n/a — coating cost
   rollup not yet populated on any line's treatment."

5. Account Hold banner too loud (LOW)
   fusion_plating_invoicing was injecting a full-height danger
   alert above every SO header. Slimmed it to a one-line compact
   alert with icon: "Account Hold — SO confirmation, invoicing
   and shipping are blocked for non-managers." Half the vertical
   footprint, less visual competition with the Plating chip bar.

Verified via UAT on S00071.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:03:26 -04:00
28 changed files with 485 additions and 95 deletions

View File

@@ -1,26 +1,30 @@
{
'name': 'Fusion Accounting',
'version': '19.0.1.0.4',
'version': '19.0.1.1.0',
'category': 'Accounting/Accounting',
'sequence': 25,
'summary': 'Meta-module that installs the full Fusion Accounting suite (core, AI, migration; bank rec, reports, etc. as later sub-modules ship).',
'summary': 'Meta-module that installs the full Fusion Accounting suite as a Community-edition replacement for Odoo Enterprise accounting.',
'description': """
Fusion Accounting (Meta-Module)
===============================
One-click install of the entire Fusion Accounting suite.
One-click install of the entire Fusion Accounting suite \u2014 a Community-edition
replacement for Odoo Enterprise's accounting modules.
Currently installs:
- fusion_accounting_core Shared schema, security, runtime helpers
- fusion_accounting_ai AI Co-Pilot (Claude/GPT)
- fusion_accounting_migration Transitional Enterprise->Fusion data migration
- fusion_accounting_bank_rec AI-assisted bank reconciliation (Phase 1)
- fusion_accounting_reports AI-augmented financial reports (Phase 2)
- fusion_accounting_assets AI-augmented asset management (Phase 3)
- fusion_accounting_followup AI-augmented customer follow-ups (Phase 4)
Sub-modules installed:
- fusion_accounting_core Shared schema, security, runtime helpers
- fusion_accounting_ai AI Co-Pilot (Claude/GPT/local LLM)
- fusion_accounting_migration Transitional Enterprise->Fusion data migration
- fusion_accounting_bank_rec AI-assisted bank reconciliation
- fusion_accounting_reports AI-augmented financial reports
- fusion_accounting_assets AI-augmented asset management
- fusion_accounting_followup AI-augmented customer follow-ups
- fusion_accounting_l10n_ca Canadian reports + tax return tracking
- fusion_accounting_hr_payroll Payroll \u2192 GL bridge (replaces hr_payroll_account)
- fusion_accounting_ocr Tesseract + LLM invoice OCR
- fusion_accounting_documents Documents app \u2194 invoice bridge
Future sub-modules (added per the roadmap as each Phase ships):
- fusion_accounting_dashboard (Phase 5)
- fusion_accounting_budget (Phase 6)
Renames the Community "Invoicing" top-level menu to "Accounting" and slots
all Fusion sub-features as sub-menus, mirroring the Odoo Enterprise UX.
Built by Nexa Systems Inc.
""",
@@ -37,8 +41,14 @@ Built by Nexa Systems Inc.
'fusion_accounting_reports',
'fusion_accounting_assets',
'fusion_accounting_followup',
'fusion_accounting_l10n_ca',
'fusion_accounting_hr_payroll',
'fusion_accounting_ocr',
'fusion_accounting_documents',
],
'data': [
'data/menu_overrides.xml',
],
'data': [],
'installable': True,
'application': True,
'license': 'OPL-1',

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="0">
<!--
Top-level "Invoicing" menu rename + visual rebrand.
V19 Community ships this menu as "Invoicing" with the standard
accounting icon. Odoo Enterprise's `accountant` module renames it
to "Accounting" and swaps the icon. We do the same here so that
once Enterprise is uninstalled, the unified menu still presents
as "Accounting" (not "Invoicing") to users.
This file lives in the meta-module so the rename only takes effect
when the full Fusion suite is installed; sub-modules installed
a-la-carte don't change the menu's name.
-->
<record id="account.menu_finance" model="ir.ui.menu">
<field name="name">Accounting</field>
<field name="web_icon">fusion_accounting,static/description/icon.png</field>
<field name="sequence">25</field>
</record>
</odoo>

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting AI',
'version': '19.0.1.0.1',
'version': '19.0.1.1.0',
'category': 'Accounting/Accounting',
'sequence': 26,
'summary': 'AI Co-Pilot for Odoo accounting (Claude/GPT) with conversational interface, dashboard, rules.',

View File

@@ -1,10 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Root menu under Accounting (account_accountant uses accountant.menu_accounting) -->
<!-- Lives under Community Accounting "Configuration" sub-menu so the
AI co-pilot is reachable regardless of whether Enterprise is
installed. (Was previously parented to `accountant.menu_accounting`
which doesn't exist after the Enterprise uninstall.) -->
<menuitem id="menu_fusion_accounting_root"
name="Fusion AI"
parent="accountant.menu_accounting"
sequence="8"
parent="account.menu_finance_configuration"
sequence="55"
groups="fusion_accounting_core.group_fusion_accounting_user"/>
<!-- Dashboard -->

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Assets',
'version': '19.0.1.0.36',
'version': '19.0.1.1.0',
'category': 'Accounting/Accounting',
'summary': 'AI-augmented asset management with depreciation schedules.',
'description': """

View File

@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Top-level menu (visible only when account_asset Enterprise NOT installed) -->
<!-- Lives under Community Accounting "Accounting" sub-menu. Only visible
when Enterprise's account_asset is absent. -->
<menuitem id="menu_fusion_assets_root"
name="Asset Management"
sequence="60"
web_icon="fusion_accounting_assets,static/description/icon.png"
parent="account.menu_finance_entries"
sequence="25"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<!-- Asset list/form -->

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting — Bank Reconciliation',
'version': '19.0.1.0.26',
'version': '19.0.1.1.0',
'category': 'Accounting/Accounting',
'sequence': 28,
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',

View File

@@ -20,11 +20,14 @@
</field>
</record>
<!-- Top-level menu — only visible when Enterprise's account_accountant is absent -->
<!-- Container menu lives under the Community Accounting "Accounting"
sub-menu (account.menu_finance_entries). Only visible when
Enterprise's account_accountant is absent (Enterprise's reconcile
widget owns the same UI surface). -->
<menuitem id="menu_fusion_bank_rec_root"
name="Bank Reconciliation"
sequence="40"
web_icon="fusion_accounting_bank_rec,static/description/icon.png"
parent="account.menu_finance_entries"
sequence="15"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_bank_rec_main"
@@ -34,9 +37,8 @@
sequence="10"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<!-- Sub-menu for the auto-reconcile wizard -->
<menuitem id="menu_fusion_auto_reconcile_wizard"
name="Auto-Reconcile"
name="Auto-Reconcile\u2026"
parent="menu_fusion_bank_rec_root"
action="action_fusion_auto_reconcile_wizard"
sequence="20"

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Core',
'version': '19.0.1.0.2',
'version': '19.0.1.1.0',
'category': 'Accounting/Accounting',
'sequence': 24,
'summary': 'Shared base for the Fusion Accounting sub-module suite (security, shared schema, runtime helpers).',

View File

@@ -8,20 +8,46 @@ class ResUsers(models.Model):
@api.model
def _fusion_recompute_coexistence_group(self):
"""Set group membership = all internal users iff Enterprise absent.
"""Maintain the two coexistence groups based on Enterprise presence.
- ``group_fusion_show_when_enterprise_absent``: members = all internal
users when NO Enterprise accounting module is installed. Used to
unhide Fusion menus that would conflict with Enterprise UIs.
- ``group_fusion_show_when_enterprise_present``: members = all internal
users when AT LEAST ONE Enterprise accounting module IS installed.
Used to hide migration/transitional UIs once Enterprise has been
uninstalled (so the user doesn't see "Migrate from Enterprise" with
nothing to migrate).
The two groups are mutually exclusive at any moment in time, but a
user can transition between them as Enterprise modules are installed
or uninstalled. Idempotent; safe to call multiple times.
Called from ir.module.module.button_immediate_install / uninstall
overrides. Idempotent; safe to call multiple times.
overrides.
"""
group = self.env.ref(
absent_group = self.env.ref(
'fusion_accounting_core.group_fusion_show_when_enterprise_absent',
raise_if_not_found=False,
)
if not group:
present_group = self.env.ref(
'fusion_accounting_core.group_fusion_show_when_enterprise_present',
raise_if_not_found=False,
)
if not absent_group and not present_group:
return
enterprise_installed = self.env['ir.module.module']._fusion_is_enterprise_accounting_installed()
all_internal = self.sudo().search([('share', '=', False)])
if enterprise_installed:
group.sudo().write({'user_ids': [(5, 0, 0)]})
if absent_group:
absent_group.sudo().write({'user_ids': [(5, 0, 0)]})
if present_group:
present_group.sudo().write({'user_ids': [(6, 0, all_internal.ids)]})
else:
all_internal = self.sudo().search([('share', '=', False)])
group.sudo().write({'user_ids': [(6, 0, all_internal.ids)]})
if absent_group:
absent_group.sudo().write({'user_ids': [(6, 0, all_internal.ids)]})
if present_group:
present_group.sudo().write({'user_ids': [(5, 0, 0)]})

View File

@@ -49,4 +49,12 @@
<field name="name">Fusion: Show menus when Enterprise absent</field>
<field name="comment">Computed group. Membership: all internal users when no Enterprise accounting module is installed. Used to hide fusion sub-module menus that would conflict with Enterprise UIs.</field>
</record>
<!-- Phase 8: inverse coexistence group \u2014 visible only when Enterprise IS present.
Used to hide migration/transitional UIs once the migration is complete and
Enterprise has been uninstalled. -->
<record id="group_fusion_show_when_enterprise_present" model="res.groups">
<field name="name">Fusion: Show menus when Enterprise present</field>
<field name="comment">Computed group. Membership: all internal users WHEN at least one Enterprise accounting module is installed. Used to hide migration/transitional UIs that are irrelevant once Enterprise has been uninstalled.</field>
</record>
</odoo>

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Follow-up',
'version': '19.0.1.0.30',
'version': '19.0.1.1.0',
'category': 'Accounting/Accounting',
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
'description': """

View File

@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Top-level menu (visible only when account_followup Enterprise NOT installed) -->
<!-- Lives under Community Accounting "Customers" sub-menu. Only visible
when Enterprise's account_followup is absent. -->
<menuitem id="menu_fusion_followup_root"
name="Customer Follow-ups"
sequence="70"
web_icon="fusion_accounting_followup,static/description/icon.png"
name="Follow-ups"
parent="account.menu_finance_receivables"
sequence="50"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<!-- Partners list (gated to overdue) -->

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting — Canadian Reports',
'version': '19.0.1.0.0',
'version': '19.0.1.1.0',
'category': 'Accounting/Localizations/Reporting',
'summary': 'Canadian-specific report definitions and tax return templates for Fusion Accounting.',
'description': """
@@ -21,6 +21,7 @@ Auto-installs when l10n_ca + fusion_accounting_reports are both present.
'data/fusion_tax_return_data.xml',
'data/report_ca_balance_sheet.xml',
'data/report_ca_profit_loss.xml',
'views/menu_views.xml',
],
'auto_install': ['l10n_ca', 'fusion_accounting_reports'],
'installable': True,

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Canadian-flavored P&L + BS menus. Live alongside the rest of the
Fusion reports under Accounting > Reporting. Open the OWL
ReportViewer with the Canadian report_code so the line_specs
come from the Canadian definitions seeded in this module.
-->
<record id="action_fusion_report_ca_pnl" model="ir.actions.act_window">
<field name="name">Profit and Loss (Canada)</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'ca_profit_loss', 'default_comparison': 'previous_year'}</field>
</record>
<record id="action_fusion_report_ca_bs" model="ir.actions.act_window">
<field name="name">Balance Sheet (Canada)</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'balance_sheet', 'default_report_code': 'ca_balance_sheet', 'default_comparison': 'previous_period'}</field>
</record>
<menuitem id="menu_fusion_report_ca_pnl"
name="Profit and Loss (Canada)"
parent="account.menu_finance_reports"
action="action_fusion_report_ca_pnl"
sequence="15"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_report_ca_bs"
name="Balance Sheet (Canada)"
parent="account.menu_finance_reports"
action="action_fusion_report_ca_bs"
sequence="16"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<!-- Tax-return tracking list -->
<record id="action_fusion_tax_return" model="ir.actions.act_window">
<field name="name">Tax Returns</field>
<field name="res_model">fusion.tax.return</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">No tax returns recorded yet</p>
<p>Track GST/HST/PST/T4/T5018/payroll-remittance filings.
Each return covers a (date_from, date_to) window and moves
from draft \u2192 to-file \u2192 filed as you submit it.</p>
</field>
</record>
<menuitem id="menu_fusion_tax_return"
name="Tax Returns (CA)"
parent="account.menu_finance_configuration"
action="action_fusion_tax_return"
sequence="100"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
</odoo>

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Migration',
'version': '19.0.1.0.0',
'version': '19.0.1.1.0',
'category': 'Accounting/Accounting',
'sequence': 27,
'summary': 'Transitional module: migrates Odoo Enterprise accounting data to Fusion Accounting tables before Enterprise uninstall.',

View File

@@ -27,21 +27,29 @@
</record>
<!--
Top-level "Fusion Accounting" menu so the UserError guidance
("Fusion Accounting -> Migrate from Enterprise") is actually reachable.
Placed at top level (no parent) because the migration is a one-time
admin task; making it visible during switchover is the point.
`groups` hides the menu from non-admins (mirroring the ACL on the wizard).
Migration wizard lives under Accounting > Configuration, and is
ONLY visible while at least one Enterprise accounting module is
still installed. Once the operator has uninstalled Enterprise, the
wizard is hidden \u2014 there's nothing left to migrate.
Visibility is gated by the intersection of:
- group_fusion_accounting_admin (admin-only feature)
- group_fusion_show_when_enterprise_present (computed: members
iff at least one Enterprise accounting module is installed)
-->
<!-- Note: gating uses ONLY group_fusion_show_when_enterprise_present.
Admin-restriction is enforced via the model ACL
(ir.model.access.csv only grants access to group_fusion_accounting_admin).
Odoo `groups=` on menuitems uses OR semantics, so listing both groups
would let any admin see the menu even after Enterprise is uninstalled. -->
<menuitem id="menu_fusion_migration_root"
name="Fusion Accounting"
sequence="95"
web_icon="fusion_accounting_migration,static/description/icon.png"
groups="fusion_accounting_core.group_fusion_accounting_admin"/>
<menuitem id="menu_fusion_migration_wizard"
name="Migrate from Enterprise"
parent="account.menu_finance_configuration"
sequence="95"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_present"/>
<menuitem id="menu_fusion_migration_wizard"
name="Run Migration Wizard"
parent="menu_fusion_migration_root"
action="action_fusion_migration_wizard"
sequence="10"
groups="fusion_accounting_core.group_fusion_accounting_admin"/>
groups="fusion_accounting_core.group_fusion_show_when_enterprise_present"/>
</odoo>

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Reports',
'version': '19.0.1.0.38',
'version': '19.0.1.1.1',
'category': 'Accounting/Accounting',
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
'description': """
@@ -47,6 +47,7 @@ menu hides; the engine and AI tools remain available for the chat.
'reports/report_pdf_template.xml',
'wizards/xlsx_export_wizard_views.xml',
'wizards/period_picker_wizard_views.xml',
'views/report_actions.xml',
'views/menu_views.xml',
],
'external_dependencies': {

View File

@@ -15,9 +15,11 @@ export class PeriodFilter extends Component {
async onReportTypeChange(ev) {
const reportType = ev.target.value;
if (reportType && this.state.dateFrom && this.state.dateTo) {
// Switching report type clears the report_code (user is picking
// a different category, not a variant).
await this.reports.runReport(
reportType, this.state.dateFrom, this.state.dateTo,
this.state.comparison);
this.state.comparison, null);
}
}
@@ -27,7 +29,8 @@ export class PeriodFilter extends Component {
await this.reports.runReport(
this.state.currentReportType,
this.state.dateFrom, this.state.dateTo,
this.state.comparison);
this.state.comparison,
this.state.currentReportCode);
}
}

View File

@@ -16,6 +16,7 @@ export class ReportsService {
this.state = reactive({
availableReports: [],
currentReportType: null,
currentReportCode: null,
currentResult: null,
currentAnomalies: [],
currentCommentary: null,
@@ -41,15 +42,17 @@ export class ReportsService {
}
}
async runReport(reportType, dateFrom, dateTo, comparison = 'none') {
async runReport(reportType, dateFrom, dateTo, comparison = 'none', reportCode = null) {
this.state.isLoading = true;
this.state.currentReportType = reportType;
this.state.currentReportCode = reportCode;
this.state.dateFrom = dateFrom;
this.state.dateTo = dateTo;
this.state.comparison = comparison;
try {
this.state.currentResult = await this.rpc(`${ENDPOINT_BASE}/run`, {
report_type: reportType,
report_code: reportCode,
date_from: dateFrom,
date_to: dateTo,
comparison: comparison,
@@ -136,7 +139,8 @@ export class ReportsService {
this.state.comparison = mode;
if (this.state.currentReportType) {
return this.runReport(this.state.currentReportType,
this.state.dateFrom, this.state.dateTo, mode);
this.state.dateFrom, this.state.dateTo, mode,
this.state.currentReportCode);
}
}
}

View File

@@ -22,6 +22,11 @@ export class ReportViewer extends Component {
const ctx = this.props.action?.context || {};
const reportType = ctx.default_report_type || 'pnl';
// default_report_code lets multiple reports of the same type
// (e.g. pnl, cash_flow, executive_summary, annual_statements all
// type='pnl') resolve to their own line_specs.
const reportCode = ctx.default_report_code || null;
const comparison = ctx.default_comparison || 'none';
const companyId = this.env.services.user?.context?.allowed_company_ids?.[0];
onWillStart(async () => {
@@ -29,7 +34,8 @@ export class ReportViewer extends Component {
const today = new Date();
const year = today.getFullYear();
await this.reports.runReport(
reportType, `${year}-01-01`, `${year}-12-31`, 'none');
reportType, `${year}-01-01`, `${year}-12-31`,
comparison, reportCode);
});
}

View File

@@ -1,21 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_fusion_reports_root"
name="Financial Reports"
sequence="50"
web_icon="fusion_accounting_reports,static/description/icon.png"
<!--
Fusion dynamic financial reports live as direct children of the
Community "Reporting" sub-menu (account.menu_finance_reports),
sitting alongside Community's PDF-based "Statement Reports" /
"Partner Reports" / "Taxes & Fiscal" / "Management" wrappers.
Each menu opens an act_window with view_mode='fusion_reports'
(the OWL ReportViewer). report_actions.xml defines the actions.
All gated to the coexistence group so they only appear when
Enterprise's account_reports is uninstalled.
-->
<!-- Top of the list \u2014 daily-driver statements -->
<menuitem id="menu_fusion_reports_pnl"
name="Profit and Loss"
parent="account.menu_finance_reports"
action="action_fusion_report_pnl"
sequence="10"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_bs"
name="Balance Sheet"
parent="account.menu_finance_reports"
action="action_fusion_report_balance_sheet"
sequence="11"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_executive_summary"
name="Executive Summary"
parent="account.menu_finance_reports"
action="action_fusion_report_executive_summary"
sequence="12"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_cash_flow"
name="Cash Flow Statement"
parent="account.menu_finance_reports"
action="action_fusion_report_cash_flow"
sequence="13"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_annual_statements"
name="Annual Statements"
parent="account.menu_finance_reports"
action="action_fusion_report_annual_statements"
sequence="14"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<!-- Audit / drill-down statements -->
<menuitem id="menu_fusion_reports_tb"
name="Trial Balance"
parent="account.menu_finance_reports"
action="action_fusion_report_trial_balance"
sequence="20"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_gl"
name="General Ledger"
parent="account.menu_finance_reports"
action="action_fusion_report_general_ledger"
sequence="21"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<!-- Partner-grouped -->
<menuitem id="menu_fusion_reports_aged_receivable"
name="Aged Receivable"
parent="account.menu_finance_reports"
action="action_fusion_report_aged_receivable"
sequence="30"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_aged_payable"
name="Aged Payable"
parent="account.menu_finance_reports"
action="action_fusion_report_aged_payable"
sequence="31"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_partner_ledger"
name="Partner Ledger"
parent="account.menu_finance_reports"
action="action_fusion_report_partner_ledger"
sequence="32"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<!-- Tax -->
<menuitem id="menu_fusion_reports_tax"
name="Tax Summary"
parent="account.menu_finance_reports"
action="action_fusion_report_tax_summary"
sequence="40"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<!--
Tools group at the bottom: custom-period picker, XLSX export wizard,
anomaly browser. Less-frequently used; bundled together so they
don't clutter the report list.
-->
<menuitem id="menu_fusion_reports_tools_group"
name="Tools"
parent="account.menu_finance_reports"
sequence="90"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_open"
name="Open Report..."
parent="menu_fusion_reports_root"
name="Custom Period..."
parent="menu_fusion_reports_tools_group"
action="action_fusion_period_picker_wizard"
sequence="10"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
<menuitem id="menu_fusion_reports_xlsx"
name="Export to XLSX..."
parent="menu_fusion_reports_root"
parent="menu_fusion_reports_tools_group"
action="action_fusion_xlsx_export_wizard"
sequence="20"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>
@@ -28,7 +118,7 @@
<menuitem id="menu_fusion_reports_anomalies"
name="Anomalies"
parent="menu_fusion_reports_root"
parent="menu_fusion_reports_tools_group"
action="action_fusion_report_anomaly_list"
sequence="30"
groups="fusion_accounting_core.group_fusion_show_when_enterprise_absent"/>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
One ir.actions.act_window per built-in report, each opens the OWL
ReportViewer (view_mode='fusion_reports'). The viewer reads
``default_report_type`` and ``default_report_code`` from the action
context to pick which fusion.report to render.
New menus per-report live below; users no longer need to go through
the period-picker wizard for the standard reports.
-->
<!-- ============================================================
CORE REPORTS (one per Fusion engine method)
============================================================ -->
<record id="action_fusion_report_pnl" model="ir.actions.act_window">
<field name="name">Profit and Loss</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'pnl'}</field>
</record>
<record id="action_fusion_report_balance_sheet" model="ir.actions.act_window">
<field name="name">Balance Sheet</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'balance_sheet', 'default_report_code': 'balance_sheet'}</field>
</record>
<record id="action_fusion_report_trial_balance" model="ir.actions.act_window">
<field name="name">Trial Balance</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'trial_balance', 'default_report_code': 'trial_balance'}</field>
</record>
<record id="action_fusion_report_general_ledger" model="ir.actions.act_window">
<field name="name">General Ledger</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'general_ledger', 'default_report_code': 'general_ledger'}</field>
</record>
<!-- ============================================================
SECONDARY PnL VARIANTS (engine.compute_pnl with code)
============================================================ -->
<record id="action_fusion_report_cash_flow" model="ir.actions.act_window">
<field name="name">Cash Flow Statement</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'cash_flow', 'default_comparison': 'previous_year'}</field>
</record>
<record id="action_fusion_report_executive_summary" model="ir.actions.act_window">
<field name="name">Executive Summary</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'executive_summary', 'default_comparison': 'previous_year'}</field>
</record>
<record id="action_fusion_report_annual_statements" model="ir.actions.act_window">
<field name="name">Annual Statements</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'annual_statements', 'default_comparison': 'previous_year'}</field>
</record>
<record id="action_fusion_report_tax_summary" model="ir.actions.act_window">
<field name="name">Tax Summary</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'trial_balance', 'default_report_code': 'tax_summary'}</field>
</record>
<!-- ============================================================
PARTNER-GROUPED REPORTS
============================================================ -->
<record id="action_fusion_report_aged_receivable" model="ir.actions.act_window">
<field name="name">Aged Receivable</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'aged_receivable', 'default_report_code': 'aged_receivable'}</field>
</record>
<record id="action_fusion_report_aged_payable" model="ir.actions.act_window">
<field name="name">Aged Payable</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'aged_payable', 'default_report_code': 'aged_payable'}</field>
</record>
<record id="action_fusion_report_partner_ledger" model="ir.actions.act_window">
<field name="name">Partner Ledger</field>
<field name="res_model">fusion.report</field>
<field name="view_mode">fusion_reports</field>
<field name="context">{'default_report_type': 'partner_ledger', 'default_report_code': 'partner_ledger'}</field>
</record>
</odoo>

View File

@@ -229,35 +229,38 @@ class SaleOrder(models.Model):
self.env.cr.execute('RELEASE SAVEPOINT %s' % savepoint_name)
except Exception as exc:
self.env.cr.execute('ROLLBACK TO SAVEPOINT %s' % savepoint_name)
self.message_post(body=_(
self.message_post(body=(
'Auto-MO group %s failed: %s'
) % (tag or 'single-line', exc))
continue
if created or adopted:
# _() needs a lang in env.context; in shell/cron this may be
# unset. Compose the message with plain format strings — this
# text is an internal chatter log, not user-facing UI.
msg_parts = []
if created:
lines_html = '<br/>'.join([
_('MO <a href="/odoo/manufacturing/%s">%s</a> '
'(%s, %d source line%s)') % (
mo.id, mo.name, tag or 'untagged',
n, 's' if n != 1 else ''
)
'MO <a href="/odoo/manufacturing/%s">%s</a> '
'(%s, %d source line%s)' % (
mo.id, mo.name, tag or 'untagged',
n, 's' if n != 1 else ''
)
for mo, tag, n in created
])
msg_parts.append(
_('%d draft MO(s) auto-created:<br/>%s') % (
'%d draft MO(s) auto-created:<br/>%s' % (
len(created), lines_html,
)
)
if adopted:
adopted_html = '<br/>'.join([
_('MO <a href="/odoo/manufacturing/%s">%s</a> '
'(legacy, now line-linked)') % (mo.id, mo.name)
'MO <a href="/odoo/manufacturing/%s">%s</a> '
'(legacy, now line-linked)' % (mo.id, mo.name)
for mo in adopted
])
msg_parts.append(
_('%d legacy MO(s) adopted:<br/>%s') % (
'%d legacy MO(s) adopted:<br/>%s' % (
len(adopted), adopted_html,
)
)

View File

@@ -113,6 +113,12 @@ class SaleOrder(models.Model):
string='Margin %',
compute='_compute_margin',
)
x_fc_margin_available = fields.Boolean(
string='Margin Available',
compute='_compute_margin',
help='False when no order line has a costed coating — the '
'margin fields should render "n/a" in the UI.',
)
x_fc_workorder_count = fields.Integer(
string='Active WOs',
@@ -486,24 +492,34 @@ class SaleOrder(models.Model):
@api.depends('order_line.price_subtotal', 'amount_untaxed')
def _compute_margin(self):
"""Simple margin: untaxed total minus rolled-up cost from coating configs.
"""Margin = untaxed total rolled-up cost from coating configs.
x_fc_margin_percent is stored as a fraction (0.0 - 1.0) so the
widget='percentage' formats it correctly (a 100% margin reads
as 100%, not 10000%).
widget='percentage' formats 100% as 100%, not 10000%.
x_fc_margin_available is False when NO line has a costed coating
(i.e. fp.coating.config.unit_cost isn't populated anywhere). The
UI should render margin fields as "n/a" in that case rather than
showing a misleading 100%.
"""
for rec in self:
has_cost_data = False
cost = 0.0
for line in rec.order_line:
if line.x_fc_coating_config_id:
cost_per_unit = getattr(
line.x_fc_coating_config_id, 'unit_cost', 0.0,
) or 0.0
cost += cost_per_unit * (line.product_uom_qty or 0)
cc = line.x_fc_coating_config_id
if not cc:
continue
if 'unit_cost' not in cc._fields:
continue
if cc.unit_cost:
has_cost_data = True
cost_per_unit = cc.unit_cost or 0.0
cost += cost_per_unit * (line.product_uom_qty or 0)
rec.x_fc_margin_available = has_cost_data
rec.x_fc_margin_amount = (rec.amount_untaxed or 0) - cost
rec.x_fc_margin_percent = (
(rec.x_fc_margin_amount / rec.amount_untaxed)
if rec.amount_untaxed else 0.0
if (rec.amount_untaxed and has_cost_data) else 0.0
)
@api.onchange('upload_rfq_file')

View File

@@ -148,11 +148,21 @@
</group>
<group>
<group string="Margin">
<div colspan="2"
invisible="x_fc_margin_available"
class="text-muted">
<i class="fa fa-info-circle me-1"/>
Margin n/a — coating cost rollup not yet
populated on any line's treatment.
</div>
<field name="x_fc_margin_amount"
widget="monetary"
options="{'currency_field': 'currency_id'}"/>
options="{'currency_field': 'currency_id'}"
invisible="not x_fc_margin_available"/>
<field name="x_fc_margin_percent"
widget="percentage"/>
widget="percentage"
invisible="not x_fc_margin_available"/>
<field name="x_fc_margin_available" invisible="1"/>
</group>
</group>
<group>
@@ -188,6 +198,13 @@
<field name="arch" type="xml">
<list string="Sale Orders" decoration-info="state == 'draft'"
decoration-muted="state == 'cancel'">
<header>
<button name="%(action_fp_direct_order_wizard)d"
type="action"
string="+ New Direct Order"
class="btn-primary"
display="always"/>
</header>
<field name="name"/>
<field name="partner_id"/>
<field name="x_fc_po_number"/>

View File

@@ -6,6 +6,13 @@
<field name="model">fp.direct.order.wizard</field>
<field name="arch" type="xml">
<form string="Direct Order Entry">
<div class="alert alert-info py-2 mb-0 small"
role="alert">
<i class="fa fa-info-circle me-1"/>
Changes are not saved until you click
<strong>Create &amp; Confirm Order</strong>. Closing this
window (Esc or X) discards your entries.
</div>
<div class="alert alert-warning mb-0"
role="alert"
invisible="not missing_info_msg">
@@ -194,7 +201,10 @@
type="object"
string="Create &amp; Confirm Order"
class="btn-primary"/>
<button string="Cancel" special="cancel" class="btn-secondary"/>
<button string="Cancel"
special="cancel"
class="btn-secondary"
confirm="Discard this order? All header data and line items will be lost."/>
</footer>
</form>
</field>

View File

@@ -13,10 +13,12 @@
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//form/header" position="before">
<div class="alert alert-danger" role="alert"
<div class="alert alert-danger py-1 px-2 mb-0 small"
role="alert"
invisible="not partner_id or not partner_id.x_fc_account_hold">
<strong>Account Hold</strong> — This customer is on account hold.
SO confirmation, invoicing, and shipping are blocked for non-managers.
<i class="fa fa-ban me-1"/>
<strong>Account Hold</strong> — SO confirmation, invoicing
and shipping are blocked for non-managers.
</div>
</xpath>
</field>