From 0e595e6129f93d090a7d03208945a677a853070d Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 12 May 2026 20:43:22 -0400 Subject: [PATCH] feat(nexa_coa_setup): batch-reclass 200 historical 411000 lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 123 historical out_invoices ($249k revenue, 2022-2023 mostly) had been posted to the generic l10n_ca '411000 Inside Sales' account, since the module they predated proper product setup and had no SKU attached. Keyword-rule script (scripts/reclass_historical_411000.py) routes each line by description text to the correct Nexa account: Pattern -> Target account Lines Revenue Computer & Server Maintenance, Server 4030 Support & 165 $236,259 Backup & Monitoring, Membership Fee Maintenance Contracts [CUSTCOMP], Custom Computer, HP Desk, 4320 Hardware Resale 24 ~$8,200 Server 2019, Server Rack, 16 Port POE, CPU:, Cleaning Supplies ONSITE-, OFFSITE-, Server Setup, 4230 Technical Support 11 ~$3,200 Wiring for — Per-incident/Hourly Match rate: 200/200 = 100%. Verified the legacy 411000 account now has zero open-invoice lines. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scripts/reclass_historical_411000.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 nexa_coa_setup/scripts/reclass_historical_411000.py diff --git a/nexa_coa_setup/scripts/reclass_historical_411000.py b/nexa_coa_setup/scripts/reclass_historical_411000.py new file mode 100644 index 00000000..09989eda --- /dev/null +++ b/nexa_coa_setup/scripts/reclass_historical_411000.py @@ -0,0 +1,62 @@ +"""Reclassify all 200 lines on legacy 411000 to proper Nexa accounts based on +keyword rules. Run on prod via odoo-shell.""" + +acct_4030 = env['account.account'].search([('code', '=', '4030')], limit=1).id # Support & Maintenance +acct_4230 = env['account.account'].search([('code', '=', '4230')], limit=1).id # Tech Support hourly +acct_4320 = env['account.account'].search([('code', '=', '4320')], limit=1).id # Hardware Resale +legacy_acct = env['account.account'].with_context(active_test=False).search([('code', '=', '411000')], limit=1).id + +# (priority, regex pattern (case-insensitive), target_account_id, label) +import re +RULES = [ + ('Computer & Server Maintenance', acct_4030, 'Support & Maintenance'), + ('Server Backup & Monitoring', acct_4030, 'Support & Maintenance'), + ('Membership Fee', acct_4030, 'Support & Maintenance (membership)'), + ('[CUSTCOMP]', acct_4320, 'Hardware Resale (custom PC)'), + ('Custom Computer', acct_4320, 'Hardware Resale (custom PC)'), + ('ustom Computer', acct_4320, 'Hardware Resale (typo'), # the 'ustom Computer' typo entry + ('HP Desk Computer', acct_4320, 'Hardware Resale (HP desktop)'), + ('Server 2019', acct_4320, 'Hardware Resale (Windows Server license)'), + ('Server Rack', acct_4320, 'Hardware Resale (rack)'), + ('16 Port POE', acct_4320, 'Hardware Resale (switch)'), + ('CPU:', acct_4320, 'Hardware Resale (custom build)'), + ('Cleaning Supplies', acct_4320, 'Hardware Resale (consumables)'), + ('ONSITE-', acct_4230, 'Tech Support — onsite'), + ('OFFSITE-', acct_4230, 'Tech Support — offsite'), + ('Onsite Sever Setup', acct_4230, 'Tech Support — setup'), + ('Server Setup', acct_4230, 'Tech Support — setup'), + ('Wiring for', acct_4230, 'Tech Support — installation'), +] + +# Find all lines on 411000 +env.cr.execute(""" + SELECT aml.id, aml.name, aml.credit + FROM account_move_line aml + JOIN account_move m ON m.id = aml.move_id + WHERE aml.account_id = %s AND m.move_type = 'out_invoice' +""", (legacy_acct,)) +lines = env.cr.fetchall() + +bucket = {acct_4030: 0, acct_4230: 0, acct_4320: 0} +unmatched = [] +for line_id, line_name, credit in lines: + name = (line_name or '').strip() + matched = False + for pattern, target, label in RULES: + if pattern.lower() in name.lower(): + env.cr.execute("UPDATE account_move_line SET account_id = %s WHERE id = %s", + (target, line_id)) + bucket[target] += 1 + matched = True + break + if not matched: + unmatched.append((line_id, name[:60], credit)) + +print(f"Reclassified {bucket[acct_4030]} lines -> 4030 Support & Maintenance") +print(f"Reclassified {bucket[acct_4230]} lines -> 4230 Tech Support — Hourly") +print(f"Reclassified {bucket[acct_4320]} lines -> 4320 Hardware Resale") +print(f"Unmatched: {len(unmatched)}") +for u in unmatched[:20]: + print(f" id={u[0]} amount={u[2]} name={u[1]!r}") +env.cr.commit() +print(">>> done <<<")