diff --git a/makap_backend/makap_backend/setup_uganda.py b/makap_backend/makap_backend/setup_uganda.py new file mode 100644 index 0000000..0ab99e7 --- /dev/null +++ b/makap_backend/makap_backend/setup_uganda.py @@ -0,0 +1,215 @@ +"""One-off setup of the Ugandan company and its operating structure (backlog 1, 16, 17). + +Never runs on migrate. Run it by hand, dry-run first: + + bench --site execute makap_backend.makap_backend.setup_uganda.run + bench --site execute makap_backend.makap_backend.setup_uganda.run --kwargs "{'confirm': 1}" + +It creates and never deletes. The original Kenya/KES company is left alone: +once the new company is checked, remove the old one in the desk (it has no +transactions) so nobody posts to it by mistake. + +Choices below are recommended defaults for Makap to confirm, not decisions +taken for them: company name, abbreviation, July-June fiscal year, VAT at 18%. +""" + +import frappe +from frappe.utils import getdate + +COUNTRY = "Uganda" +CURRENCY = "UGX" +TIME_ZONE = "Africa/Kampala" +VAT_RATE = 18 + +# Added beside the four warehouses ERPNext creates with every company +# (Stores, Work In Progress, Finished Goods, Goods In Transit). +WAREHOUSES = [ + ("Raw Materials", {}), + ("Quarantine", {}), + ("WIP - Printing", {}), + ("WIP - Lamination and Curing", {}), + ("WIP - Slitting", {}), + ("WIP - Bag Making", {}), + ("Returns Hold", {"makap_is_returns_hold_warehouse": 1}), + ("Rejected", {}), +] + + +class Plan: + def __init__(self, confirm): + self.confirm = confirm + self.steps = [] + + def do(self, description, action): + self.steps.append(("DONE " if self.confirm else "WOULD ") + description) + if self.confirm: + action() + + def skip(self, description): + self.steps.append("SKIP " + description) + + +def run(company_name="Makap Uganda Limited", abbr="MUL", fiscal_year_start="2026-07-01", confirm=0): + plan = Plan(bool(int(confirm))) + + _system_settings(plan) + _company(plan, company_name, abbr) + _fiscal_year(plan, company_name, getdate(fiscal_year_start)) + _vat(plan, company_name, abbr) + _warehouses(plan, company_name, abbr) + _manufacturing_and_stock_settings(plan, abbr) + + if plan.confirm: + frappe.db.commit() + print("\n".join(plan.steps)) + if not plan.confirm: + print("\nDry run only. Re-run with --kwargs \"{'confirm': 1}\" to apply.") + return plan.steps + + +def _system_settings(plan): + def apply(): + if not frappe.db.get_value("Currency", CURRENCY, "enabled"): + frappe.db.set_value("Currency", CURRENCY, "enabled", 1) + settings = frappe.get_single("System Settings") + settings.country = COUNTRY + settings.time_zone = TIME_ZONE + settings.currency = CURRENCY + settings.save() + + plan.do(f"set System Settings to {COUNTRY}, {TIME_ZONE}, {CURRENCY}", apply) + + +def _company(plan, company_name, abbr): + if frappe.db.exists("Company", company_name): + plan.skip(f"company {company_name} already exists") + return + + def apply(): + frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "country": COUNTRY, + "default_currency": CURRENCY, + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ).insert() + defaults = frappe.get_single("Global Defaults") + defaults.default_company = company_name + defaults.default_currency = CURRENCY + defaults.country = COUNTRY + defaults.save() + + plan.do(f"create company {company_name} ({abbr}) in {CURRENCY} with the Standard chart, and make it the default", apply) + + +def _fiscal_year(plan, company_name, start): + end = frappe.utils.add_days(frappe.utils.add_months(start, 12), -1) + overlapping = frappe.get_all( + "Fiscal Year", + filters={"year_start_date": ["<=", end], "year_end_date": [">=", start]}, + pluck="name", + ) + if overlapping: + plan.skip( + f"fiscal year {start}..{end}: overlaps {', '.join(overlapping)}. " + "Add the company to it, or adjust it, in the desk" + ) + return + + def apply(): + doc = frappe.new_doc("Fiscal Year") + doc.year = f"{start.year}-{end.year}" + doc.year_start_date = start + doc.year_end_date = end + doc.append("companies", {"company": company_name}) + doc.insert() + + plan.do(f"create fiscal year {start}..{end} for {company_name}", apply) + + +def _tax_account(company_name, abbr, account_name): + parent = frappe.db.get_value( + "Account", {"company": company_name, "account_name": "Duties and Taxes", "is_group": 1}, "name" + ) + if not parent: + frappe.throw(f"No 'Duties and Taxes' group in {company_name}'s chart of accounts.") + name = f"{account_name} - {abbr}" + if not frappe.db.exists("Account", name): + frappe.get_doc( + { + "doctype": "Account", + "account_name": account_name, + "parent_account": parent, + "company": company_name, + "account_type": "Tax", + "tax_rate": VAT_RATE, + } + ).insert() + return name + + +def _vat(plan, company_name, abbr): + def apply(): + for doctype, account_name, title in ( + ("Sales Taxes and Charges Template", "VAT Output", "Uganda VAT 18%"), + ("Purchase Taxes and Charges Template", "VAT Input", "Uganda VAT 18%"), + ): + account = _tax_account(company_name, abbr, account_name) + if frappe.db.exists(doctype, {"title": title, "company": company_name}): + continue + frappe.get_doc( + { + "doctype": doctype, + "title": title, + "company": company_name, + "is_default": 1, + "taxes": [ + { + "charge_type": "On Net Total", + "account_head": account, + "description": f"VAT @ {VAT_RATE}%", + "rate": VAT_RATE, + **({"category": "Total", "add_deduct_tax": "Add"} if doctype.startswith("Purchase") else {}), + } + ], + } + ).insert() + + plan.do(f"create VAT Output/Input accounts and default {VAT_RATE}% sales and purchase tax templates", apply) + + +def _warehouses(plan, company_name, abbr): + def apply(): + parent = frappe.db.get_value("Warehouse", {"company": company_name, "is_group": 1, "parent_warehouse": ["is", "not set"]}) + for name, extra in WAREHOUSES: + if frappe.db.exists("Warehouse", f"{name} - {abbr}"): + continue + frappe.get_doc( + {"doctype": "Warehouse", "warehouse_name": name, "company": company_name, "parent_warehouse": parent, **extra} + ).insert() + + plan.do("create warehouses: " + ", ".join(name for name, _ in WAREHOUSES), apply) + + +def _manufacturing_and_stock_settings(plan, abbr): + def apply(): + manufacturing = frappe.get_single("Manufacturing Settings") + manufacturing.backflush_raw_materials_based_on = "Material Transferred for Manufacture" + manufacturing.enforce_time_logs = 1 + manufacturing.default_wip_warehouse = f"Work In Progress - {abbr}" + manufacturing.default_fg_warehouse = f"Finished Goods - {abbr}" + manufacturing.save() + + stock = frappe.get_single("Stock Settings") + stock.enable_stock_reservation = 1 + stock.save() + + plan.do( + "Manufacturing Settings: backflush on Material Transferred, enforce Job Card time logs, " + "default WIP/FG warehouses; Stock Settings: enable stock reservation", + apply, + )