From 861ba677e4d291098e3bf2346a8d245d4d05618c Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Tue, 24 Feb 2026 10:56:15 +0700 Subject: [PATCH] feat: Add a collection of Odoo scripts to migrate company data, clone journals and payment methods, and address data inconsistencies. --- check_pos_rounding.py | 33 +++ cleanup_cloned_data_tenggilis.py | 49 +++++ clone_accounting_journals.py | 70 ++++++ clone_cash_rounding_pricelist.py | 112 ++++++++++ clone_pos_payment_methods.py | 58 +++++ diagnose_pos_close.py | 40 ++++ fix_company_journals.py | 56 +++++ fix_mismatched_journals.py | 82 +++++++ migrate_to_branch.py | 125 +++++++++++ run_all_migrations.py | 361 +++++++++++++++++++++++++++++++ 10 files changed, 986 insertions(+) create mode 100644 check_pos_rounding.py create mode 100644 cleanup_cloned_data_tenggilis.py create mode 100644 clone_accounting_journals.py create mode 100644 clone_cash_rounding_pricelist.py create mode 100644 clone_pos_payment_methods.py create mode 100644 diagnose_pos_close.py create mode 100644 fix_company_journals.py create mode 100644 fix_mismatched_journals.py create mode 100644 migrate_to_branch.py create mode 100644 run_all_migrations.py diff --git a/check_pos_rounding.py b/check_pos_rounding.py new file mode 100644 index 0000000..ac1e996 --- /dev/null +++ b/check_pos_rounding.py @@ -0,0 +1,33 @@ +import sys + +def check_pos_rounding(env): + source_name = 'Kedai Kipas 58 Rungkut' + target_name = 'Kedai Kipas 58 Tenggilis' + + source_company = env['res.company'].search([('name', 'ilike', source_name)], limit=1) + target_company = env['res.company'].search([('name', 'ilike', target_name)], limit=1) + + if not source_company or not target_company: + print(f"Could not find one or both companies: '{source_name}', '{target_name}'") + return + + print("--- Rungkut POS Configs (Source) ---") + source_configs = env['pos.config'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id) + ]) + for c in source_configs: + print(f"[{c.name}] cash_rounding: {c.cash_rounding}") + print(f"[{c.name}] rounding_method: {c.rounding_method.name if c.rounding_method else 'None'} (ID: {c.rounding_method.id if c.rounding_method else 'None'})") + print(f"[{c.name}] only_round_cash_method: {c.only_round_cash_method}") + + print("\n--- Tenggilis POS Configs (Target) ---") + target_configs = env['pos.config'].with_context(active_test=False).search([ + ('company_id', '=', target_company.id) + ]) + for c in target_configs: + print(f"[{c.name}] cash_rounding: {c.cash_rounding}") + print(f"[{c.name}] rounding_method: {c.rounding_method.name if c.rounding_method else 'None'} (ID: {c.rounding_method.id if c.rounding_method else 'None'})") + print(f"[{c.name}] only_round_cash_method: {c.only_round_cash_method}") + +if __name__ == '__main__': + check_pos_rounding(env) diff --git a/cleanup_cloned_data_tenggilis.py b/cleanup_cloned_data_tenggilis.py new file mode 100644 index 0000000..69e09f7 --- /dev/null +++ b/cleanup_cloned_data_tenggilis.py @@ -0,0 +1,49 @@ +import sys + +def cleanup(env): + target_company = env['res.company'].search([('name', 'ilike', 'Tenggilis')], limit=1) + + if not target_company: + print("Tenggilis company not found") + return + + print("Cleaning up copied payment methods...") + methods = env['pos.payment.method'].with_context(active_test=False).search([ + ('company_id', '=', target_company.id), + ('create_date', '>=', '2026-02-24') + ]) + num_methods = len(methods) + for m in methods: + print(f" Deleting Payment Method: {m.name}") + methods.unlink() + + print("Cleaning up copied journals...") + journals = env['account.journal'].with_context(active_test=False).search([ + ('company_id', '=', target_company.id), + ('create_date', '>=', '2026-02-24') + ]) + num_journals = len(journals) + for j in journals: + print(f" Deleting Journal: {j.name}") + journals.unlink() + + print("Cleaning up (copy) accounts...") + accounts = env['account.account'].with_context(active_test=False).search([ + ('company_id', '=', target_company.id), + '|', + ('name', 'ilike', '(copy)'), + ('create_date', '>=', '2026-02-24') + ]) + num_accounts = len(accounts) + for a in accounts: + print(f" Deleting Account: {a.name} ({a.code})") + accounts.unlink() + + print(f"Cleanup complete. Deleted {num_methods} methods, {num_journals} journals, {num_accounts} accounts.") + env.cr.commit() + +if __name__ == '__main__': + try: + cleanup(env) + except NameError: + print("Run via odoo shell") diff --git a/clone_accounting_journals.py b/clone_accounting_journals.py new file mode 100644 index 0000000..57e8ed9 --- /dev/null +++ b/clone_accounting_journals.py @@ -0,0 +1,70 @@ +import sys + +def clone_journals(env): + source_name = 'Kedai Kipas 58 Rungkut' + target_name = 'Kedai Kipas 58 Tenggilis' + + source_company = env['res.company'].search([('name', 'ilike', source_name)], limit=1) + target_company = env['res.company'].search([('name', 'ilike', target_name)], limit=1) + + if not source_company or not target_company: + print(f"Could not find one or both companies: '{source_name}', '{target_name}'") + return + + print(f"Source: {source_company.name} (ID: {source_company.id})") + print(f"Target: {target_company.name} (ID: {target_company.id})") + + # Fetch all journals from the source company + journals = env['account.journal'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id) + ]) + + print(f"Found {len(journals)} journals in {source_company.name} to clone.") + + count = 0 + for journal in journals: + # Check if a journal with the same code already exists in target + existing = env['account.journal'].with_context(active_test=False).search([ + ('code', '=', journal.code), + ('company_id', '=', target_company.id) + ], limit=1) + + if existing: + print(f" -> '{journal.name}' (Code: {journal.code}) already exists in {target_company.name}. Skipping.") + continue + + try: + with env.cr.savepoint(): + # Explicitly pass the shared accounts so Odoo doesn't try to auto-create new ones + copy_defaults = { + 'company_id': target_company.id, + 'default_account_id': journal.default_account_id.id if journal.default_account_id else False, + 'suspense_account_id': journal.suspense_account_id.id if journal.suspense_account_id else False, + 'profit_account_id': journal.profit_account_id.id if journal.profit_account_id else False, + 'loss_account_id': journal.loss_account_id.id if journal.loss_account_id else False, + } + + # Clone the journal + new_journal = journal.copy(copy_defaults) + + # Odoo's copy might override name/code or append "(copy)" + # Write to ensure they are identical to source + new_journal.write({ + 'name': journal.name, + 'code': journal.code, + }) + print(f" -> Cloned '{journal.name}' [{journal.code}] (New ID: {new_journal.id})") + count += 1 + except Exception as e: + print(f" -> Failed to clone '{journal.name}' [{journal.code}]: {e}") + + print(f"Committing changes... (Cloned {count} journals)") + env.cr.commit() + print("Cloning complete!") + +if __name__ == '__main__': + try: + clone_journals(env) + except NameError: + print("Please run this script using Odoo shell:") + print("Example: ./.venv/bin/python ./odoo/odoo-bin shell -d -c odoo.conf < scripts/clone_accounting_journals.py") diff --git a/clone_cash_rounding_pricelist.py b/clone_cash_rounding_pricelist.py new file mode 100644 index 0000000..9d3830e --- /dev/null +++ b/clone_cash_rounding_pricelist.py @@ -0,0 +1,112 @@ +import sys + +def clone_rounding_and_pricelist(env): + source_name = 'Kedai Kipas 58 Rungkut' + target_name = 'Kedai Kipas 58 Tenggilis' + + source_company = env['res.company'].search([('name', 'ilike', source_name)], limit=1) + target_company = env['res.company'].search([('name', 'ilike', target_name)], limit=1) + + if not source_company or not target_company: + print(f"Could not find one or both companies: '{source_name}', '{target_name}'") + return + + print(f"Source: {source_company.name} (ID: {source_company.id})") + print(f"Target: {target_company.name} (ID: {target_company.id})") + + # Let's clone Cash Rounding. If it's owned by parent, we don't necessarily need to clone the model, + # but let's check what roundings Rungkut has. Rungkut might share the parent's rounding. + # The user asked to "clone cash rounding setup". This probably means the POS config settings! + + print("\n--- Applying Cash Rounding Setup from Rungkut to Tenggilis POS ---") + source_configs = env['pos.config'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id), + ('cash_rounding', '=', True) # Only grab the one that actually has it configured! + ], limit=1) + + target_configs = env['pos.config'].with_context(active_test=False).search([ + ('company_id', '=', target_company.id) + ]) + + if source_configs and target_configs: + template_config = source_configs[0] + for t_config in target_configs: + t_config.write({ + 'cash_rounding': template_config.cash_rounding, + 'rounding_method': template_config.rounding_method.id if template_config.rounding_method else False, + 'only_round_cash_method': template_config.only_round_cash_method, + }) + print(f" -> Applied Cash Rounding setup to POS '{t_config.name}'") + else: + print(" -> Could not find POS Configs in one or both companies, skipping pos.config rounding setup.") + + # 2. Clone Pricelists + print("\n--- Cloning Pricelists ---") + pricelists = env['product.pricelist'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id) + ]) + + count_pricelist = 0 + for pricelist in pricelists: + existing = env['product.pricelist'].with_context(active_test=False).search([ + ('name', '=', pricelist.name), + ('company_id', '=', target_company.id) + ], limit=1) + + if existing: + print(f" -> '{pricelist.name}' already exists in {target_company.name}. Skipping.") + continue + + try: + with env.cr.savepoint(): + new_pricelist = pricelist.copy({ + 'company_id': target_company.id, + # We clear website_id to fix the website company restriction error + 'website_id': False, + }) + # Ensure name matches exactly to avoid "(copy)" + new_pricelist.write({'name': pricelist.name}) + print(f" -> Cloned Pricelist '{pricelist.name}' (New ID: {new_pricelist.id})") + count_pricelist += 1 + except Exception as e: + print(f" -> Failed to clone Pricelist '{pricelist.name}': {e}") + + # Do Tenggilis POS configs need the cloned pricelists? + # If the Rungkut POS had a pricelist set, we should set the equivalent in Tenggilis + if source_configs and target_configs: + print("\n--- Updating POS Pricelist configuration ---") + for s_config, t_config in zip(source_configs, target_configs): + if s_config.pricelist_id: + # Find the Tenggilis equivalent of this pricelist + matching_pl = env['product.pricelist'].with_context(active_test=False).search([ + ('name', '=', s_config.pricelist_id.name), + ('company_id', '=', target_company.id) + ], limit=1) + if matching_pl: + t_config.write({'pricelist_id': matching_pl.id}) + print(f" -> Set Pricelist '{matching_pl.name}' on POS '{t_config.name}'") + + # Also handle available pricelists + if s_config.available_pricelist_ids: + mapped_ids = [] + for pl in s_config.available_pricelist_ids: + match = env['product.pricelist'].with_context(active_test=False).search([ + ('name', '=', pl.name), + ('company_id', '=', target_company.id) + ], limit=1) + if match: + mapped_ids.append(match.id) + if mapped_ids: + t_config.write({'available_pricelist_ids': [(6, 0, mapped_ids)]}) + print(f" -> Set {len(mapped_ids)} Available Pricelists on POS '{t_config.name}'") + + print(f"\nCommitting changes... (Cloned setup, {count_pricelist} pricelists)") + env.cr.commit() + print("Done!") + +if __name__ == '__main__': + try: + clone_rounding_and_pricelist(env) + except NameError: + print("Please run this script using Odoo shell:") + print("Example: ./.venv/bin/python ./odoo/odoo-bin shell -d -c odoo.conf < scripts/clone_cash_rounding_pricelist.py") diff --git a/clone_pos_payment_methods.py b/clone_pos_payment_methods.py new file mode 100644 index 0000000..22996a4 --- /dev/null +++ b/clone_pos_payment_methods.py @@ -0,0 +1,58 @@ +import sys + +def clone_payment_methods(env): + source_name = 'Kedai Kipas 58 Rungkut' + target_name = 'Kedai Kipas 58 Tenggilis' + + # Using 'ilike' to be safe with exact naming + source_company = env['res.company'].search([('name', 'ilike', source_name)], limit=1) + target_company = env['res.company'].search([('name', 'ilike', target_name)], limit=1) + + if not source_company or not target_company: + print(f"Could not find one or both companies: '{source_name}', '{target_name}'") + return + + print(f"Source: {source_company.name} (ID: {source_company.id})") + print(f"Target: {target_company.name} (ID: {target_company.id})") + + # Fetch all payment methods from the source company + methods = env['pos.payment.method'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id) + ]) + + print(f"Found {len(methods)} payment methods in {source_company.name} to clone.") + + count = 0 + for method in methods: + # Check if a method with the same name already exists in target to avoid duplicates + existing = env['pos.payment.method'].with_context(active_test=False).search([ + ('name', '=', method.name), + ('company_id', '=', target_company.id) + ], limit=1) + + if existing: + print(f" -> '{method.name}' already exists in {target_company.name}. Skipping.") + continue + + # Clone the method and assign to target company + # the .copy() method will duplicate the record. We just override company_id. + # Ensure we pass the explicit account mappings to avoid Odoo triggering a duplicate account creation + new_method = method.copy({ + 'company_id': target_company.id, + 'receivable_account_id': method.receivable_account_id.id if method.receivable_account_id else False, + 'outstanding_account_id': method.outstanding_account_id.id if method.outstanding_account_id else False, + 'journal_id': method.journal_id.id if method.journal_id else False, + }) + print(f" -> Cloned '{method.name}' (New ID: {new_method.id})") + count += 1 + + print(f"Committing changes... (Cloned {count} methods)") + env.cr.commit() + print("Cloning complete!") + +if __name__ == '__main__': + try: + clone_payment_methods(env) + except NameError: + print("Please run this script using Odoo shell:") + print("Example: ./.venv/bin/python ./odoo/odoo-bin shell -d -c odoo.conf < scripts/clone_pos_payment_methods.py") diff --git a/diagnose_pos_close.py b/diagnose_pos_close.py new file mode 100644 index 0000000..6512cd9 --- /dev/null +++ b/diagnose_pos_close.py @@ -0,0 +1,40 @@ +import sys +import traceback + +def diagnose_pos_sessions(env): + sessions = env['pos.session'].search([('state', '!=', 'closed')]) + if not sessions: + print("No open POS sessions found.") + return + + for session in sessions: + print(f"Session: {session.name} | State: {session.state} | Company ID: {session.company_id.id}") + + orders = env['pos.order'].search([('session_id', '=', session.id)]) + for order in orders: + if order.company_id.id != session.company_id.id: + print(f" [!] Order {order.name} has Company ID {order.company_id.id} (Expected: {session.company_id.id})") + + for line in order.lines: + if line.company_id.id != session.company_id.id: + print(f" [!] Order Line {line.id} has Company ID {line.company_id.id}") + + payments = env['pos.payment'].search([('session_id', '=', session.id)]) + for payment in payments: + if payment.company_id.id != session.company_id.id: + print(f" [!] Payment {payment.name} has Company ID {payment.company_id.id}") + + if session.move_id and session.move_id.company_id.id != session.company_id.id: + print(f" [!] Account Move {session.move_id.name} has Company ID {session.move_id.company_id.id}") + + print(" -> Attempting to close session...") + try: + # We bypass user domain restrictions for the script + session.with_context(allowed_company_ids=[session.company_id.id]).action_pos_session_closing_control() + print(f" -> Successfully closed session {session.name}!") + except Exception as e: + print(f" -> Failed to close session {session.name}:") + traceback.print_exc() + +if __name__ == '__main__': + diagnose_pos_sessions(env) diff --git a/fix_company_journals.py b/fix_company_journals.py new file mode 100644 index 0000000..f4ac347 --- /dev/null +++ b/fix_company_journals.py @@ -0,0 +1,56 @@ +import sys + +def fix_company_journals(env): + company_name = 'Kedai Kipas 58 Tenggilis' + company = env['res.company'].search([('name', 'ilike', company_name)], limit=1) + + if not company: + print(f"Company {company_name} not found.") + return + + print(f"Fixing journals for company: {company.name} (ID: {company.id})") + + # We look for the equivalent journals in Tenggilis + # 1. Cash Basis Taxes (Usually code CABA or name 'Cash Basis Taxes') + cash_basis_journal = env['account.journal'].search([ + ('company_id', '=', company.id), + ('code', '=', 'CABA') # Assuming code is CABA. If not, try by name. + ], limit=1) + + if not cash_basis_journal: + cash_basis_journal = env['account.journal'].search([ + ('company_id', '=', company.id), + ('name', 'ilike', 'Cash Basis') + ], limit=1) + + vals = {} + if cash_basis_journal: + print(f"Setting Cash Basis Journal to: {cash_basis_journal.name} (ID: {cash_basis_journal.id})") + vals['tax_cash_basis_journal_id'] = cash_basis_journal.id + else: + print("Could not find a Cash Basis journal for Tenggilis to set. You may need to clear it or create one.") + vals['tax_cash_basis_journal_id'] = False # Temporary clear if not found to allow saving + + # 2. account_tax_periodicity_journal_id + tax_journal = env['account.journal'].search([ + ('company_id', '=', company.id), + ('name', 'ilike', 'Kas Kecil Operasional Tenggilis') + ], limit=1) + + if tax_journal: + print(f"Setting Tax Periodicity Journal to: {tax_journal.name} (ID: {tax_journal.id})") + vals['account_tax_periodicity_journal_id'] = tax_journal.id + else: + print("Could not find matching Tax Periodicity journal for Tenggilis. Clearing it to allow saving.") + vals['account_tax_periodicity_journal_id'] = False + + company.write(vals) + print(f"Fixed {len(vals)} company settings.") + env.cr.commit() + print("Done!") + +if __name__ == '__main__': + try: + fix_company_journals(env) + except NameError: + print("Run via odoo shell") diff --git a/fix_mismatched_journals.py b/fix_mismatched_journals.py new file mode 100644 index 0000000..06174fb --- /dev/null +++ b/fix_mismatched_journals.py @@ -0,0 +1,82 @@ +import sys + +def fix_all_mismatched_journals(env): + company_name = 'Kedai Kipas 58 Tenggilis' + company = env['res.company'].search([('name', 'ilike', company_name)], limit=1) + + if not company: + print(f"Company {company_name} not found.") + return + + print(f"Checking {company.name} for mismatched journal entries...") + + # Let's forcefully clear ANY journal that doesn't belong to Tenggilis from its configurations + # Common journal configuration fields + fields_to_check = [ + 'account_tax_periodicity_journal_id', + 'tax_cash_basis_journal_id', + 'currency_exdiff_journal_id' + # Can add more if needed + ] + + vals = {} + for field in fields_to_check: + try: + journal_id = getattr(company, field, False) + if journal_id and journal_id.company_id and journal_id.company_id.id != company.id: + print(f"[x] Mismatched Journal in field '{field}': {journal_id.name} (from {journal_id.company_id.name}) -> Clearing") + vals[field] = False + except Exception as e: + print(f"Error checking {field}: {e}") + + # Also check pos configs for this company + pos_configs = env['pos.config'].search([('company_id', '=', company.id)]) + for config in pos_configs: + config_vals = {} + if config.journal_id and config.journal_id.company_id and config.journal_id.company_id.id != company.id: + print(f"[x] Mismatched Journal in POS Config '{config.name}': {config.journal_id.name} -> Clearing") + config_vals['journal_id'] = False + + if config.invoice_journal_id and config.invoice_journal_id.company_id and config.invoice_journal_id.company_id.id != company.id: + print(f"[x] Mismatched Invoice Journal in POS Config '{config.name}': {config.invoice_journal_id.name} -> Clearing") + config_vals['invoice_journal_id'] = False + + if config_vals: + config.write(config_vals) + + if vals: + company.write(vals) + print(f"Updated {len(vals)} fields on company.") + else: + print("No mismatched journals found on company settings.") + + print("Finding and fixing pos payment methods with wrong journal...") + methods = env['pos.payment.method'].with_context(active_test=False).search([ + ('company_id', '=', company.id), + ('journal_id.company_id', '!=', company.id), + ('journal_id', '!=', False) + ]) + + for method in methods: + print(f"[x] Fixing payment method '{method.name}' (Journal {method.journal_id.name} is from {method.journal_id.company_id.name}) -> Clearing") + method.journal_id = False + + # Check for Bank Accounts (res.partner.bank) linked to the wrong journal + banks = env['res.partner.bank'].with_context(active_test=False).search([ + ('company_id', '=', company.id), + ('journal_id.company_id', '!=', company.id), + ('journal_id', '!=', False) + ]) + for bank in banks: + print(f"[x] Fixing bank account '{bank.acc_number}' (Journal {bank.journal_id.name} is from {bank.journal_id.company_id.name}) -> Clearing") + bank.journal_id = False + + + env.cr.commit() + print("Done!") + +if __name__ == '__main__': + try: + fix_all_mismatched_journals(env) + except NameError: + print("Run via odoo shell") diff --git a/migrate_to_branch.py b/migrate_to_branch.py new file mode 100644 index 0000000..686deee --- /dev/null +++ b/migrate_to_branch.py @@ -0,0 +1,125 @@ +import sys + +def migrate_to_branch(env): + source_name = 'PT Kipas Lima Delapan' + target_name = 'Kedai Kipas 58 Rungkut' + + source_company = env['res.company'].search([('name', '=', source_name)], limit=1) + target_company = env['res.company'].search([('name', '=', target_name)], limit=1) + + if not source_company or not target_company: + print(f"Could not find one or both companies: '{source_name}', '{target_name}'") + return + + print(f"Source Company: {source_company.name} (ID: {source_company.id})") + print(f"Target Company: {target_company.name} (ID: {target_company.id})") + + # List of safe transaction and config tables to update. + # We avoid global/shared configs like account_account, account_tax, product_template, res_partner, res_users + # as these should either remain with the parent company (in Odoo 17 branch setups) or be global. + tables_to_update = [ + # POS Config & Transactions + 'pos_config', + 'pos_session', + 'pos_order', + 'pos_order_line', + 'pos_payment', + 'pos_payment_method', + + # Shop Floor / Inventory + 'stock_warehouse', + 'stock_location', + 'stock_picking_type', + 'stock_picking', + 'stock_move', + 'stock_move_line', + 'stock_quant', + 'stock_valuation_layer', + 'stock_scrap', + 'stock_inventory', + 'stock_rule', + 'stock_route', + 'stock_putaway_rule', + + # Shop Floor / Manufacturing + 'mrp_production', + 'mrp_workorder', + 'mrp_workcenter', + 'mrp_routing_workcenter', + 'mrp_bom', + 'mrp_bom_line', + 'mrp_unbuild', + 'mrp_consumption_warning', + + # Journals and Accounting Transactions + 'account_journal', + 'account_move', + 'account_move_line', + 'account_payment', + 'account_bank_statement', + 'account_bank_statement_line', + 'account_partial_reconcile', + 'account_payment_term', + + # Sales and Purchases + 'sale_order', + 'sale_order_line', + 'purchase_order', + 'purchase_order_line', + 'purchase_requisition', + 'purchase_requisition_line', + + # Employees and HR (Optional, remove if employees shouldn't be moved) + 'hr_employee', + 'hr_contract', + 'hr_attendance', + 'hr_payslip', + 'hr_expense', + 'hr_expense_sheet', + + # Products / Pricing + 'product_pricelist', + + # Properties / Sequences + 'ir_sequence', + ] + + # We'll use raw SQL because Odoo ORM will block updates to company_id on posted entries and confirmed orders. + total_updated = 0 + for table in tables_to_update: + # Check if table exists and has company_id + env.cr.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name=%s AND column_name='company_id' + """, (table,)) + + if env.cr.fetchone(): + env.cr.execute(f""" + UPDATE {table} + SET company_id = %s + WHERE company_id = %s + """, (target_company.id, source_company.id)) + + rowcount = env.cr.rowcount + if rowcount > 0: + print(f"Updated {rowcount} records in table '{table}'") + total_updated += rowcount + + # Note on ir_property: it stores things like default receivable/payable accounts + # Updating them blindly might break the parent company. If the parent + # needs to remain somewhat functional, we probably shouldn't migrate all ir_properties. + # But if it's strictly a "move everything to the new shop", we could. + # For safety, I've left ir_property out and let standard branch inheritance handle accounts. + + print(f"Committing changes... (Total {total_updated} rows updated)") + env.cr.commit() + env.invalidate_all() + print("Migration complete!") + +if __name__ == '__main__': + try: + migrate_to_branch(env) + except NameError: + print("Please run this script using Odoo shell:") + print("Example: ./.venv/bin/python ./odoo/odoo-bin shell -d -c odoo.conf < scripts/migrate_to_branch.py") diff --git a/run_all_migrations.py b/run_all_migrations.py new file mode 100644 index 0000000..931033b --- /dev/null +++ b/run_all_migrations.py @@ -0,0 +1,361 @@ +import sys + +# ========================================== +# 1. MIGRATE TO BRANCH +# ========================================== +def migrate_to_branch(env): + print("\n==========================================") + print("1. RUNNING CORE DATA MIGRATION") + print("==========================================") + source_name = 'PT Kipas Lima Delapan' + target_name = 'Kedai Kipas 58 Rungkut' + + source_company = env['res.company'].search([('name', '=', source_name)], limit=1) + target_company = env['res.company'].search([('name', '=', target_name)], limit=1) + + if not source_company or not target_company: + print(f"Could not find one or both companies: '{source_name}', '{target_name}'") + return + + print(f"Source Company: {source_company.name} (ID: {source_company.id})") + print(f"Target Company: {target_company.name} (ID: {target_company.id})") + + tables_to_update = [ + 'pos_config', 'pos_session', 'pos_order', 'pos_order_line', 'pos_payment', 'pos_payment_method', + 'stock_warehouse', 'stock_location', 'stock_picking_type', 'stock_picking', 'stock_move', 'stock_move_line', + 'stock_quant', 'stock_valuation_layer', 'stock_scrap', 'stock_inventory', 'stock_rule', 'stock_route', 'stock_putaway_rule', + 'mrp_production', 'mrp_workorder', 'mrp_workcenter', 'mrp_routing_workcenter', 'mrp_bom', 'mrp_bom_line', 'mrp_unbuild', 'mrp_consumption_warning', + 'account_journal', 'account_move', 'account_move_line', 'account_payment', 'account_bank_statement', 'account_bank_statement_line', 'account_partial_reconcile', 'account_payment_term', + 'sale_order', 'sale_order_line', 'purchase_order', 'purchase_order_line', 'purchase_requisition', 'purchase_requisition_line', + 'hr_employee', 'hr_contract', 'hr_attendance', 'hr_payslip', 'hr_expense', 'hr_expense_sheet', + 'product_pricelist', 'ir_sequence' + ] + + total_updated = 0 + for table in tables_to_update: + env.cr.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name=%s AND column_name='company_id' + """, (table,)) + + if env.cr.fetchone(): + env.cr.execute(f""" + UPDATE {table} + SET company_id = %s + WHERE company_id = %s + """, (target_company.id, source_company.id)) + rowcount = env.cr.rowcount + if rowcount > 0: + print(f"Updated {rowcount} records in table '{table}'") + total_updated += rowcount + + print(f"Committing changes... (Total {total_updated} rows updated)") + env.cr.commit() + env.invalidate_all() + print("Migration complete!") + + +# ========================================== +# 2. CLONE ACCOUNTING JOURNALS +# ========================================== +def clone_journals(env): + print("\n==========================================") + print("2. CLONING ACCOUNTING JOURNALS") + print("==========================================") + source_name = 'Kedai Kipas 58 Rungkut' + target_name = 'Kedai Kipas 58 Tenggilis' + + source_company = env['res.company'].search([('name', 'ilike', source_name)], limit=1) + target_company = env['res.company'].search([('name', 'ilike', target_name)], limit=1) + + if not source_company or not target_company: + print(f"Could not find one or both companies: '{source_name}', '{target_name}'") + return + + journals = env['account.journal'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id) + ]) + + print(f"Found {len(journals)} journals in {source_company.name} to clone.") + + count = 0 + for journal in journals: + existing = env['account.journal'].with_context(active_test=False).search([ + ('code', '=', journal.code), + ('company_id', '=', target_company.id) + ], limit=1) + + if existing: + print(f" -> '{journal.name}' (Code: {journal.code}) already exists in {target_company.name}. Skipping.") + continue + + try: + with env.cr.savepoint(): + copy_defaults = { + 'company_id': target_company.id, + 'default_account_id': journal.default_account_id.id if journal.default_account_id else False, + 'suspense_account_id': journal.suspense_account_id.id if journal.suspense_account_id else False, + 'profit_account_id': journal.profit_account_id.id if journal.profit_account_id else False, + 'loss_account_id': journal.loss_account_id.id if journal.loss_account_id else False, + } + new_journal = journal.copy(copy_defaults) + new_journal.write({ + 'name': journal.name, + 'code': journal.code, + }) + print(f" -> Cloned '{journal.name}' [{journal.code}] (New ID: {new_journal.id})") + count += 1 + except Exception as e: + print(f" -> Failed to clone '{journal.name}' [{journal.code}]: {e}") + + print(f"Committing changes... (Cloned {count} journals)") + env.cr.commit() + + +# ========================================== +# 3. CLONE POS PAYMENT METHODS +# ========================================== +def clone_payment_methods(env): + print("\n==========================================") + print("3. CLONING POS PAYMENT METHODS") + print("==========================================") + source_name = 'Kedai Kipas 58 Rungkut' + target_name = 'Kedai Kipas 58 Tenggilis' + + source_company = env['res.company'].search([('name', 'ilike', source_name)], limit=1) + target_company = env['res.company'].search([('name', 'ilike', target_name)], limit=1) + + methods = env['pos.payment.method'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id) + ]) + print(f"Found {len(methods)} payment methods to clone.") + + count = 0 + for method in methods: + existing = env['pos.payment.method'].with_context(active_test=False).search([ + ('name', '=', method.name), + ('company_id', '=', target_company.id) + ], limit=1) + + if existing: + print(f" -> '{method.name}' already exists in target. Skipping.") + continue + + new_method = method.copy({ + 'company_id': target_company.id, + 'receivable_account_id': method.receivable_account_id.id if method.receivable_account_id else False, + 'outstanding_account_id': method.outstanding_account_id.id if method.outstanding_account_id else False, + 'journal_id': method.journal_id.id if method.journal_id else False, + }) + print(f" -> Cloned '{method.name}' (New ID: {new_method.id})") + count += 1 + + print(f"Committing changes... (Cloned {count} methods)") + env.cr.commit() + + +# ========================================== +# 4. CLONE CASH ROUNDING & PRICELISTS +# ========================================== +def clone_rounding_and_pricelist(env): + print("\n==========================================") + print("4. CLONING CASH ROUNDING & PRICELISTS") + print("==========================================") + source_name = 'Kedai Kipas 58 Rungkut' + target_name = 'Kedai Kipas 58 Tenggilis' + + source_company = env['res.company'].search([('name', 'ilike', source_name)], limit=1) + target_company = env['res.company'].search([('name', 'ilike', target_name)], limit=1) + + print("--- Applying Cash Rounding Setup from Rungkut to Tenggilis POS ---") + source_configs = env['pos.config'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id), + ('cash_rounding', '=', True) + ], limit=1) + + target_configs = env['pos.config'].with_context(active_test=False).search([ + ('company_id', '=', target_company.id) + ]) + + if source_configs and target_configs: + template_config = source_configs[0] + for t_config in target_configs: + t_config.write({ + 'cash_rounding': template_config.cash_rounding, + 'rounding_method': template_config.rounding_method.id if template_config.rounding_method else False, + 'only_round_cash_method': template_config.only_round_cash_method, + }) + print(f" -> Applied Cash Rounding setup to POS '{t_config.name}'") + + print("\n--- Cloning Pricelists ---") + pricelists = env['product.pricelist'].with_context(active_test=False).search([ + ('company_id', '=', source_company.id) + ]) + + count_pricelist = 0 + for pricelist in pricelists: + existing = env['product.pricelist'].with_context(active_test=False).search([ + ('name', '=', pricelist.name), + ('company_id', '=', target_company.id) + ], limit=1) + + if existing: + print(f" -> '{pricelist.name}' already exists. Skipping.") + continue + + try: + with env.cr.savepoint(): + new_pricelist = pricelist.copy({ + 'company_id': target_company.id, + 'website_id': False, + }) + new_pricelist.write({'name': pricelist.name}) + print(f" -> Cloned Pricelist '{pricelist.name}'") + count_pricelist += 1 + except Exception as e: + print(f" -> Failed to clone Pricelist: {e}") + + if source_configs and target_configs: + print("\n--- Updating POS Pricelist configuration ---") + for s_config, t_config in zip(source_configs, target_configs): + if s_config.pricelist_id: + matching_pl = env['product.pricelist'].with_context(active_test=False).search([ + ('name', '=', s_config.pricelist_id.name), + ('company_id', '=', target_company.id) + ], limit=1) + if matching_pl: + t_config.write({'pricelist_id': matching_pl.id}) + print(f" -> Set Pricelist '{matching_pl.name}' on POS '{t_config.name}'") + + if s_config.available_pricelist_ids: + mapped_ids = [] + for pl in s_config.available_pricelist_ids: + match = env['product.pricelist'].with_context(active_test=False).search([ + ('name', '=', pl.name), + ('company_id', '=', target_company.id) + ], limit=1) + if match: + mapped_ids.append(match.id) + if mapped_ids: + t_config.write({'available_pricelist_ids': [(6, 0, mapped_ids)]}) + print(f" -> Set Available Pricelists on POS '{t_config.name}'") + + print(f"Committing changes... (Cloned setup, {count_pricelist} pricelists)") + env.cr.commit() + + +# ========================================== +# 5. FIX COMPANY JOURNALS +# ========================================== +def fix_company_journals(env): + print("\n==========================================") + print("5. FIXING RES.COMPANY JOURNALS") + print("==========================================") + company_name = 'Kedai Kipas 58 Tenggilis' + company = env['res.company'].search([('name', 'ilike', company_name)], limit=1) + + if not company: + return + + cash_basis_journal = env['account.journal'].search([ + ('company_id', '=', company.id), + ('code', '=', 'CABA') + ], limit=1) + + if not cash_basis_journal: + cash_basis_journal = env['account.journal'].search([ + ('company_id', '=', company.id), + ('name', 'ilike', 'Cash Basis') + ], limit=1) + + vals = {} + if cash_basis_journal: + vals['tax_cash_basis_journal_id'] = cash_basis_journal.id + else: + vals['tax_cash_basis_journal_id'] = False + + tax_journal = env['account.journal'].search([ + ('company_id', '=', company.id), + ('name', 'ilike', 'Kas Kecil Operasional Tenggilis') + ], limit=1) + + if tax_journal: + vals['account_tax_periodicity_journal_id'] = tax_journal.id + else: + vals['account_tax_periodicity_journal_id'] = False + + company.write(vals) + print(f"Fixed company journal settings.") + env.cr.commit() + + +# ========================================== +# 6. FIX ALL MISMATCHED JOURNALS +# ========================================== +def fix_all_mismatched_journals(env): + print("\n==========================================") + print("6. FIXING ALL MISMATCHED JOURNALS") + print("==========================================") + company_name = 'Kedai Kipas 58 Tenggilis' + company = env['res.company'].search([('name', 'ilike', company_name)], limit=1) + + if not company: + return + + fields_to_check = ['account_tax_periodicity_journal_id', 'tax_cash_basis_journal_id', 'currency_exdiff_journal_id'] + vals = {} + for field in fields_to_check: + try: + journal_id = getattr(company, field, False) + if journal_id and journal_id.company_id and journal_id.company_id.id != company.id: + vals[field] = False + except Exception: + pass + + pos_configs = env['pos.config'].search([('company_id', '=', company.id)]) + for config in pos_configs: + config_vals = {} + if config.journal_id and config.journal_id.company_id and config.journal_id.company_id.id != company.id: + config_vals['journal_id'] = False + if config.invoice_journal_id and config.invoice_journal_id.company_id and config.invoice_journal_id.company_id.id != company.id: + config_vals['invoice_journal_id'] = False + if config_vals: + config.write(config_vals) + + if vals: + company.write(vals) + + methods = env['pos.payment.method'].with_context(active_test=False).search([ + ('company_id', '=', company.id), + ('journal_id.company_id', '!=', company.id), + ('journal_id', '!=', False) + ]) + for method in methods: + method.journal_id = False + + banks = env['res.partner.bank'].with_context(active_test=False).search([ + ('company_id', '=', company.id), + ('journal_id.company_id', '!=', company.id), + ('journal_id', '!=', False) + ]) + for bank in banks: + bank.journal_id = False + + print("Fixed leftover mismatched journals.") + env.cr.commit() + + +if __name__ == '__main__': + try: + print("Starting full migration and cloning process...") + migrate_to_branch(env) + clone_journals(env) + clone_payment_methods(env) + clone_rounding_and_pricelist(env) + fix_company_journals(env) + fix_all_mismatched_journals(env) + print("\n*** ALL MIGRATIONS COMPLETED SUCCESSFULLY! ***") + except NameError: + print("Please run this script using Odoo shell:") + print("Example: ./.venv/bin/python ./odoo/odoo-bin shell -d -c odoo.conf < scripts/run_all_migrations.py")