# -*- 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`.')