From 7275007948e729b78a546e66c7ef56bc649d4e22 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 25 Apr 2026 09:16:48 -0400 Subject: [PATCH] =?UTF-8?q?fix(jobs):=20cleanup=20script=20=E2=80=94=20del?= =?UTF-8?q?ete=20SOs,=20invoices,=20payments,=20pickings,=20quotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original cleanup script left behind 33 sale.order, 31 invoice account.move, 13 payments, 1 picking, 17 quote requests. Extended to nuke them too — SOs in any state, invoices regardless of posted/draft (force-cancels via SQL when ORM blocks it), payments forced to cancel before delete, stock.move + stock.move.line force-deleted, quote requests unlinked. Section ordering: payments -> invoices -> pickings/moves -> SOs (FK direction). Reconciliation links cleared via direct SQL. Sequences for sale.order and account.move.invoice reset to 1 so fresh SOs start at S00001. Re-seeded 31 fp.jobs across all 6 states after running the extended cleanup. Part of: native job model migration (spec 2026-04-25) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scripts/cleanup_demo_data.py | 190 +++++++++++++++--- 1 file changed, 167 insertions(+), 23 deletions(-) diff --git a/fusion_plating/fusion_plating_jobs/scripts/cleanup_demo_data.py b/fusion_plating/fusion_plating_jobs/scripts/cleanup_demo_data.py index c3f5a9b8..cb560c69 100644 --- a/fusion_plating/fusion_plating_jobs/scripts/cleanup_demo_data.py +++ b/fusion_plating/fusion_plating_jobs/scripts/cleanup_demo_data.py @@ -3,10 +3,12 @@ # License OPL-1 (Odoo Proprietary License v1.0) # # DESTRUCTIVE: deletes ALL fp.job, fp.job.step, fp.job.step.timelog, -# mrp.production, mrp.workorder 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). +# 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. @@ -109,30 +111,172 @@ def run(env): env['mrp.production'].sudo().search([]).unlink() print(' Deleted %d mrp.production rows' % n) - # 14. Test SOs — delete ALL non-invoiced sale orders. Force state to - # 'cancel' via SQL because Odoo's _unlink_except_draft_or_cancel - # guard otherwise blocks the unlink. Demo data only. - so_ids = env['sale.order'].sudo().search([ - ('invoice_ids', '=', False), - ]).ids - n = len(so_ids) - if so_ids: + # 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( - "UPDATE sale_order SET state='cancel' WHERE id = ANY(%s)", - (so_ids,), + "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), ) - # Also cancel stock pickings the SOs may have created (forces - # Odoo's cascade-aware unlink to pass) env.cr.execute( - "UPDATE stock_picking SET state='cancel' " - "WHERE sale_id = ANY(%s)", - (so_ids,), + "DELETE FROM account_payment WHERE id = ANY(%s)", + (payments.ids,), ) - env.invalidate_all() - env['sale.order'].sudo().browse(so_ids).unlink() - print(' Deleted %d sale.order rows (uninvoiced)' % n) + print(' Deleted %d account.payment rows' % n) - # 15. Reset fp.job sequence so new ones start from JOB/00001 + # 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