Files
Odoo-Modules/fusion_plating/fusion_plating_jobs/scripts/cleanup_demo_data.py
gsinghpal 8c76a16366 chore(plating): de-dash shipped code + intake-neutral customer emails
Replace em-dashes and en-dashes with hyphens across 789 shipped source
files (py/xml/js/scss) so the delivered module reads as human-written;
em-dashes had become a recognizable AI-generated tell. Internal .md dev
notes are excluded. The WO-sticker mojibake strippers keep their dash
search targets (now written — / –). No logic changes: comments
and display strings only; validated with py_compile + lxml parse.

Rewrite the 7 customer notification emails to be intake-neutral
(ship-in / drop-off / pickup) and repair-aware, and fix the Shipped
email documents line (packing slip vs bill of lading; certificate only
when issued). Subjects use a hyphen separator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 00:16:19 -04:00

293 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# DESTRUCTIVE: deletes ALL fp.job, fp.job.step, fp.job.step.timelog,
# mrp.production, mrp.workorder, sale.order, account.move (invoices),
# account.payment, stock.picking, stock.move, fusion.plating.quote.request
# records and their dependent data (deliveries, certs, thickness readings,
# holds, portal jobs, racking inspections). Preserves masters (partners,
# parts, recipes, coating configs, baths, tanks, work centres, users,
# groups, settings).
#
# Use only on demo/dev environments. Take a Proxmox snapshot first.
def run(env):
print('=== Cleanup starting ===')
# Walk dependents bottom-up so FK cascades don't bite us.
# 1. Time logs (cascades on step delete, but be explicit)
n = env['fp.job.step.timelog'].search_count([])
env['fp.job.step.timelog'].sudo().search([]).unlink()
print(' Deleted %d fp.job.step.timelog rows' % n)
# 2. fp.job.node.override (cascades on job delete)
n = env['fp.job.node.override'].search_count([])
env['fp.job.node.override'].sudo().search([]).unlink()
print(' Deleted %d fp.job.node.override rows' % n)
# 3. Deliveries linked to jobs OR with job_ref set OR linked to a SO that
# we will delete. Delete ALL deliveries - they're test data.
if 'fusion.plating.delivery' in env:
deliveries = env['fusion.plating.delivery'].sudo().search([])
n = len(deliveries)
deliveries.unlink()
print(' Deleted %d fusion.plating.delivery rows' % n)
# 4. Certificates linked to jobs/MOs
if 'fp.certificate' in env:
certs = env['fp.certificate'].sudo().search([])
n = len(certs)
certs.unlink()
print(' Deleted %d fp.certificate rows' % n)
# 5. Thickness readings
if 'fp.thickness.reading' in env:
tr = env['fp.thickness.reading'].sudo().search([])
n = len(tr)
tr.unlink()
print(' Deleted %d fp.thickness.reading rows' % n)
# 6. Quality holds linked to jobs/MOs
if 'fusion.plating.quality.hold' in env:
holds = env['fusion.plating.quality.hold'].sudo().search([])
n = len(holds)
holds.unlink()
print(' Deleted %d fusion.plating.quality.hold rows' % n)
# 7. Portal jobs (linked to jobs OR legacy production)
if 'fusion.plating.portal.job' in env:
portals = env['fusion.plating.portal.job'].sudo().search([])
n = len(portals)
portals.unlink()
print(' Deleted %d fusion.plating.portal.job rows' % n)
# 8. Racking inspections - required FK to mrp.production, so delete
# BEFORE we kill the productions.
if 'fp.racking.inspection' in env:
insps = env['fp.racking.inspection'].sudo().search([])
n = len(insps)
insps.unlink()
print(' Deleted %d fp.racking.inspection rows' % n)
# 9. Receiving records (required FK to sale.order - delete before SOs)
if 'fp.receiving' in env:
recs = env['fp.receiving'].sudo().search([])
n = len(recs)
recs.unlink()
print(' Deleted %d fp.receiving rows' % n)
# 10. fp.job.step (cascade-safe via job_id, but be explicit)
n = env['fp.job.step'].search_count([])
env['fp.job.step'].sudo().search([]).unlink()
print(' Deleted %d fp.job.step rows' % n)
# 11. fp.job
n = env['fp.job'].search_count([])
env['fp.job'].sudo().search([]).unlink()
print(' Deleted %d fp.job rows' % n)
# 12. mrp.workorder (legacy)
n = env['mrp.workorder'].search_count([])
env['mrp.workorder'].sudo().search([]).unlink()
print(' Deleted %d mrp.workorder rows' % n)
# 13. mrp.production (legacy) - force state via SQL so unlink() bypasses
# Odoo's _unlink_except_done guard (which forbids deleting done MOs)
# and the action_cancel guard (which forbids cancelling done MOs).
# Demo data only.
n = env['mrp.production'].search_count([])
if n:
# 'cancel' state is the only state mrp.production._unlink_except_done
# explicitly permits.
env.cr.execute("UPDATE mrp_production SET state='cancel'")
# Also clear stock moves' state so cascaded checks pass
env.cr.execute(
"UPDATE stock_move SET state='cancel' "
"WHERE raw_material_production_id IN (SELECT id FROM mrp_production) "
"OR production_id IN (SELECT id FROM mrp_production)"
)
env.invalidate_all()
env['mrp.production'].sudo().search([]).unlink()
print(' Deleted %d mrp.production rows' % n)
# 14. Account payments (must come before invoices - payment is reconciled
# against move lines)
Payment = env['account.payment'].sudo()
payments = Payment.search([])
n = len(payments)
if payments:
for p in payments:
if p.state == 'paid':
try:
p.action_draft()
except Exception:
env.cr.execute(
"UPDATE account_payment SET state='draft' WHERE id=%s",
(p.id,),
)
try:
p.action_cancel()
except Exception:
pass
# Clear reconciliation links pointing at the payment moves
env.cr.execute(
"DELETE FROM account_partial_reconcile "
"WHERE debit_move_id IN (SELECT id FROM account_move_line WHERE move_id IN ("
" SELECT move_id FROM account_payment WHERE id = ANY(%s))) "
"OR credit_move_id IN (SELECT id FROM account_move_line WHERE move_id IN ("
" SELECT move_id FROM account_payment WHERE id = ANY(%s)))",
(payments.ids, payments.ids),
)
env.cr.execute(
"DELETE FROM account_payment WHERE id = ANY(%s)",
(payments.ids,),
)
print(' Deleted %d account.payment rows' % n)
# 15. Invoices (account.move with out_invoice / out_refund / in_invoice
# / in_refund move types). Posted ones must be drafted/cancelled first.
Move = env['account.move'].sudo()
invoices = Move.search([
('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')),
])
n = len(invoices)
if invoices:
for inv in invoices:
if inv.state == 'posted':
try:
inv.button_draft()
except Exception:
env.cr.execute(
"UPDATE account_move SET state='draft' WHERE id=%s",
(inv.id,),
)
try:
inv.button_cancel()
except Exception:
env.cr.execute(
"UPDATE account_move SET state='cancel' WHERE id=%s",
(inv.id,),
)
env.invalidate_all()
# Force-clear reconciliation links so unlink doesn't trip on
# partial_reconcile_id
env.cr.execute(
"DELETE FROM account_partial_reconcile "
"WHERE debit_move_id IN (SELECT id FROM account_move_line WHERE move_id = ANY(%s)) "
"OR credit_move_id IN (SELECT id FROM account_move_line WHERE move_id = ANY(%s))",
(invoices.ids, invoices.ids),
)
env.cr.execute(
"DELETE FROM account_move_line WHERE move_id = ANY(%s)",
(invoices.ids,),
)
env.cr.execute(
"DELETE FROM account_move WHERE id = ANY(%s)",
(invoices.ids,),
)
print(' Deleted %d account.move (invoice) rows' % n)
# 16. Stock pickings + moves (any leftovers from MOs / SOs)
pickings = env['stock.picking'].sudo().search([])
n = len(pickings)
if pickings:
for pk in pickings:
if pk.state not in ('cancel', 'draft'):
try:
pk.action_cancel()
except Exception:
pass
env.cr.execute(
"UPDATE stock_picking SET state='cancel' WHERE id = ANY(%s)",
(pickings.ids,),
)
env.cr.execute(
"DELETE FROM stock_move_line WHERE picking_id = ANY(%s)",
(pickings.ids,),
)
env.cr.execute(
"DELETE FROM stock_move WHERE picking_id = ANY(%s)",
(pickings.ids,),
)
env.cr.execute(
"DELETE FROM stock_picking WHERE id = ANY(%s)",
(pickings.ids,),
)
print(' Deleted %d stock.picking rows' % n)
# Any remaining orphan stock.move rows
moves = env['stock.move'].sudo().search([])
n = len(moves)
if moves:
env.cr.execute(
"DELETE FROM stock_move_line WHERE move_id = ANY(%s)",
(moves.ids,),
)
env.cr.execute(
"DELETE FROM stock_move WHERE id = ANY(%s)",
(moves.ids,),
)
print(' Deleted %d stock.move rows' % n)
# 17. Sale orders (cancel any non-cancel state first). Delete ALL -
# demo data only.
sos = env['sale.order'].sudo().search([])
n = len(sos)
if sos:
for so in sos:
if so.state not in ('cancel', 'draft'):
try:
so.action_cancel()
except Exception:
env.cr.execute(
"UPDATE sale_order SET state='cancel' WHERE id=%s",
(so.id,),
)
env.invalidate_all()
# Drop SO lines explicitly to avoid FK trip on unlink
env.cr.execute(
"DELETE FROM sale_order_line WHERE order_id = ANY(%s)",
(sos.ids,),
)
env.cr.execute(
"DELETE FROM sale_order WHERE id = ANY(%s)",
(sos.ids,),
)
print(' Deleted %d sale.order rows' % n)
# 18. Quote requests
if 'fusion.plating.quote.request' in env:
qrs = env['fusion.plating.quote.request'].sudo().search([])
n = len(qrs)
if qrs:
try:
qrs.unlink()
except Exception:
env.cr.execute(
"DELETE FROM fusion_plating_quote_request WHERE id = ANY(%s)",
(qrs.ids,),
)
print(' Deleted %d fusion.plating.quote.request rows' % n)
# 19. Reset sequences for SO and invoices so new ones start fresh
for code in ('sale.order', 'account.move.invoice'):
seq = env['ir.sequence'].sudo().search([('code', '=', code)], limit=1)
if seq:
seq.number_next = 1
# 20. Reset fp.job sequence so new ones start from JOB/00001
seq = env['ir.sequence'].sudo().search([('code', '=', 'fp.job')], limit=1)
if seq:
seq.number_next = 1
print(' Reset fp.job sequence to start at 1')
env.cr.commit()
print('=== Cleanup complete ===')
try:
run(env)
except NameError:
print('Run inside `odoo shell`.')