"""Prepare one ERPNext site for Voiced's live integration tests. Run inside the backend container (see README.md): podman exec -i voiced-erp-backend python - [ic] [rounding=commercial] < setup_site.py Creates (idempotently): the Indian company "Voiced Test Co" (abbr VTC, INR), the fiscal year, GST output tax accounts, a bank account, a company address, an integration user with API keys, and a few Items/UOMs. Prints one line `ENV KEY=VALUE` per value the Rust live tests need. Never run against a real site. """ import json import sys import time import frappe SITE = sys.argv[1] IC = "ic" in sys.argv[2:] # `rounding=commercial` switches System Settings > Rounding Method (the ERPNext default is Banker's Rounding). ROUNDING = {"commercial": "Commercial Rounding", "banker": "Banker's Rounding"}.get( next((a.split("=", 1)[1] for a in sys.argv[2:] if a.startswith("rounding=")), ""), None ) ABBR = "VTC" COMPANY = "Voiced Test Co" frappe.init(site=SITE, sites_path="/home/frappe/frappe-bench/sites") frappe.connect() frappe.set_user("Administrator") def out(key, value): print(f"ENV {key}={value}") def exists(doctype, name): return bool(frappe.db.exists(doctype, name)) def insert(doc): d = frappe.get_doc(doc) d.insert(ignore_permissions=True) frappe.db.commit() return d # ---- 1. site, optionally India Compliance (before the wizard so it can add its own accounts) ---- if IC and "india_compliance" not in frappe.get_installed_apps(): from frappe.installer import install_app install_app("india_compliance", verbose=False) frappe.db.commit() # ---- 2. setup wizard ---- if not frappe.db.get_single_value("System Settings", "setup_complete"): from frappe.desk.page.setup_wizard.setup_wizard import setup_complete setup_complete( { "language": "English", "country": "India", "timezone": "Asia/Kolkata", "currency": "INR", "full_name": "Voiced Admin", "email": "admin@voiced.test", "password": "admin", "company_name": COMPANY, "company_abbr": ABBR, "domains": ["Services"], "chart_of_accounts": "Standard", "fy_start_date": "2026-04-01", "fy_end_date": "2027-03-31", "bank_account": "HDFC", "setup_demo": 0, } ) # setup_complete may run its stages on a worker; wait for it. for _ in range(120): frappe.db.rollback() if frappe.db.get_single_value("System Settings", "setup_complete") and exists("Company", COMPANY): break time.sleep(2) frappe.clear_cache() frappe.db.commit() assert exists("Company", COMPANY), "setup wizard did not create the company" # ---- 3. accounts ---- duties = frappe.db.get_value("Account", {"company": COMPANY, "account_name": "Duties and Taxes", "is_group": 1}, "name") assert duties, "Duties and Taxes group not found" for head in ("CGST", "SGST", "UTGST", "IGST"): name = f"Output Tax {head} - {ABBR}" if not exists("Account", name): insert( { "doctype": "Account", "account_name": f"Output Tax {head}", "parent_account": duties, "company": COMPANY, "account_type": "Tax", "root_type": "Liability", "is_group": 0, } ) tds = f"TDS Receivable - {ABBR}" if not exists("Account", tds): parent = frappe.db.get_value("Account", {"company": COMPANY, "account_name": "Current Assets", "is_group": 1}, "name") insert( { "doctype": "Account", "account_name": "TDS Receivable", "parent_account": parent, "company": COMPANY, "account_type": "Tax", "root_type": "Asset", "is_group": 0, } ) # ---- 4. company address (India Compliance needs one with a GSTIN-ish state) ---- addr_name = f"{COMPANY}-Billing" if not exists("Address", addr_name): d = { "doctype": "Address", "address_title": COMPANY, "address_type": "Billing", "address_line1": "1 Test Street", "city": "Mumbai", "state": "Maharashtra", "pincode": "400001", "country": "India", "links": [{"link_doctype": "Company", "link_name": COMPANY}], } if IC: d["gst_category"] = "Registered Regular" d["gstin"] = "27AAPFU0939F1ZV" insert(d) # ---- 5. items / UOMs ---- for uom in ("Hour", "Minute", "Second"): if not exists("UOM", uom): insert({"doctype": "UOM", "uom_name": uom}) for code, uom in (("VOICED-SERVICE", "Nos"), ("VOICED-HOURLY", "Hour")): if not exists("Item", code): d = { "doctype": "Item", "item_code": code, "item_name": code, "item_group": "Services", "stock_uom": uom, "is_stock_item": 0, } if IC: d["gst_hsn_code"] = "998314" insert(d) # ---- 5b. rounding method ---- if ROUNDING: frappe.db.set_single_value("System Settings", "rounding_method", ROUNDING) frappe.db.commit() frappe.clear_cache() # ---- 6. integration user ---- USER = f"voiced-integration@{SITE.replace('.', '-')}.test" # The role set that was verified live (with and without India Compliance): "Accounts User" creates and submits Sales # Invoices and Payment Entries and reads the accounting masters; "Sales User" adds the Customer and Address rights # (look up, create) and the tax-template list. Accounts User alone gets HTTP 403 on Customer. ROLES = ["Accounts User", "Sales User"] if not exists("User", USER): insert( { "doctype": "User", "email": USER, "first_name": "Voiced", "last_name": "Integration", "send_welcome_email": 0, "user_type": "System User", "roles": [{"role": r} for r in ROLES], } ) user = frappe.get_doc("User", USER) user.set("roles", [{"role": r} for r in ROLES]) secret = frappe.generate_hash(length=32) if not user.api_key: user.api_key = frappe.generate_hash(length=15) user.api_secret = secret user.save(ignore_permissions=True) frappe.db.commit() frappe.clear_cache(user=USER) frappe.clear_cache() out("ERPNEXT_KEY", user.api_key) out("ERPNEXT_SECRET", secret) out("ERPNEXT_COMPANY", COMPANY) out("ERPNEXT_ABBR", ABBR) out("ERPNEXT_IC", "1" if IC else "0") out("ERPNEXT_USER", USER) out("ERPNEXT_ROUNDING", "commercial" if frappe.db.get_single_value("System Settings", "rounding_method") == "Commercial Rounding" else "banker") print(json.dumps({"roles": ROLES})) frappe.destroy()