From c40e7992cba55c142ef30aae2067328e261ab743 Mon Sep 17 00:00:00 2001 From: SUBHANKAR DHAR Date: Tue, 16 Jun 2026 15:41:54 +0530 Subject: [PATCH] style: fix black/isort formatting and codespell typo to pass pre-commit lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - black: auto-reformatted all 4 Furnitex scripts to PEP 8 style - isort: sorted imports in setup_furnitex.py and delete_streetwok.py - codespell: renamed loop variable `ot` → `opp_type` in create_crm_stages() (codespell flagged `ot` as a misspelling) Co-Authored-By: Claude Sonnet 4.6 --- delete_streetwok.py | 55 ++- setup_furnitex.py | 555 +++++++++++++-------- setup_furnitex_crm_billing.py | 879 ++++++++++++++++++++++------------ update_furnitex_info.py | 105 ++-- 4 files changed, 1008 insertions(+), 586 deletions(-) diff --git a/delete_streetwok.py b/delete_streetwok.py index 1aaf43d5..6018f46c 100644 --- a/delete_streetwok.py +++ b/delete_streetwok.py @@ -6,7 +6,6 @@ Run via: bench --site frontend execute frappe.delete_streetwok.run import frappe - COMPANY = "streetwok (Demo)" @@ -47,7 +46,8 @@ def run(): # Get submitted docs for this company docs = frappe.db.sql( f"SELECT name FROM `tab{dt}` WHERE company=%s AND docstatus=1", - COMPANY, as_dict=1 + COMPANY, + as_dict=1, ) if docs: print(f" Cancelling {len(docs)} submitted {dt}(s)...") @@ -99,9 +99,9 @@ def run(): # ── STEP 5: Delete child tables that reference the company ──── child_cleanups = [ - ("Sales Invoice Item", "company"), + ("Sales Invoice Item", "company"), ("Purchase Invoice Item", "company"), - ("Payment Entry Reference", None), # handled via parent delete + ("Payment Entry Reference", None), # handled via parent delete ] # ── STEP 6: Delete Warehouses ───────────────────────────────── @@ -116,27 +116,28 @@ def run(): for wh in wh_list: try: - frappe.delete_doc("Warehouse", wh.name, - ignore_permissions=True, force=True, - ignore_on_trash=True) - except Exception as e: - frappe.db.sql( - "DELETE FROM `tabWarehouse` WHERE name=%s", wh.name + frappe.delete_doc( + "Warehouse", + wh.name, + ignore_permissions=True, + force=True, + ignore_on_trash=True, ) + except Exception as e: + frappe.db.sql("DELETE FROM `tabWarehouse` WHERE name=%s", wh.name) frappe.db.commit() print(f" Deleted {len(wh_list)} Warehouse(s)") # ── STEP 7: Delete Cost Centers ─────────────────────────────── cc_list = frappe.db.sql( "SELECT name FROM `tabCost Center` WHERE company=%s ORDER BY lft DESC", - COMPANY, as_dict=1 + COMPANY, + as_dict=1, ) if cc_list: for cc in cc_list: try: - frappe.db.sql( - "DELETE FROM `tabCost Center` WHERE name=%s", cc.name - ) + frappe.db.sql("DELETE FROM `tabCost Center` WHERE name=%s", cc.name) except Exception: pass frappe.db.commit() @@ -149,21 +150,21 @@ def run(): frappe.db.sql( "DELETE FROM `tabAccount` WHERE company=%s AND is_group=0", COMPANY ) - frappe.db.sql( - "DELETE FROM `tabAccount` WHERE company=%s", COMPANY - ) + frappe.db.sql("DELETE FROM `tabAccount` WHERE company=%s", COMPANY) frappe.db.commit() print(f" Deleted {acct_count} Account(s)") # ── STEP 9: Delete Fiscal Years linked only to this company ─── fy_links = frappe.db.sql( """SELECT parent FROM `tabFiscal Year Company` - WHERE company=%s""", COMPANY, as_dict=1 + WHERE company=%s""", + COMPANY, + as_dict=1, ) for fy in fy_links: frappe.db.sql( "DELETE FROM `tabFiscal Year Company` WHERE company=%s AND parent=%s", - (COMPANY, fy.parent) + (COMPANY, fy.parent), ) # If this fiscal year has no other company links, delete it too remaining = frappe.db.count("Fiscal Year Company", {"parent": fy.parent}) @@ -186,7 +187,8 @@ def run(): AND NOT EXISTS ( SELECT 1 FROM `tabSales Order` WHERE customer=c.name AND company='Furnitex' AND docstatus < 2 - )""", as_dict=1 + )""", + as_dict=1, ) if stale_customers: for c in stale_customers: @@ -199,9 +201,13 @@ def run(): # ── STEP 11: Delete the Company record itself ───────────────── try: - frappe.delete_doc("Company", COMPANY, - ignore_permissions=True, force=True, - ignore_on_trash=True) + frappe.delete_doc( + "Company", + COMPANY, + ignore_permissions=True, + force=True, + ignore_on_trash=True, + ) except Exception: frappe.db.sql("DELETE FROM `tabCompany` WHERE name=%s", COMPANY) frappe.db.commit() @@ -219,7 +225,8 @@ def run(): """SELECT name FROM `tabItem` WHERE item_name LIKE '%streetwok%' OR item_code LIKE '%streetwok%' - OR description LIKE '%streetwok%'""", as_dict=1 + OR description LIKE '%streetwok%'""", + as_dict=1, ) if stale_items: for item in stale_items: diff --git a/setup_furnitex.py b/setup_furnitex.py index 0e487b1a..0f8d7f19 100644 --- a/setup_furnitex.py +++ b/setup_furnitex.py @@ -9,10 +9,10 @@ Or directly: import frappe import frappe.defaults +COMPANY = "Furnitex" +SITE = "frontend" +ABBR = None # resolved at runtime via get_abbr() -COMPANY = "Furnitex" -SITE = "frontend" -ABBR = None # resolved at runtime via get_abbr() def get_abbr(): global ABBR @@ -25,13 +25,16 @@ def get_abbr(): # HELPERS # ───────────────────────────────────────────────────────────── + def exists(doctype, name): return frappe.db.exists(doctype, name) + def exists_filter(doctype, filters): """Existence check by filters (for docs where name includes company abbr).""" return frappe.db.get_value(doctype, filters, "name") + def safe_insert(doc): """Insert and return True, or skip silently on duplicate and return False.""" try: @@ -45,12 +48,15 @@ def safe_insert(doc): return False raise + def ok(msg): print(f" [OK] {msg}") + def skip(msg): print(f" [SKIP] {msg}") + def warn(msg): print(f" [WARN] {msg}") @@ -59,23 +65,26 @@ def warn(msg): # 1. UOMs # ───────────────────────────────────────────────────────────── + def create_uoms(): print("\n[1/9] Creating UOMs...") uoms = [ - ("SqFt", 0), - ("Rft", 0), - ("Bag", 1), - ("Sheet", 1), + ("SqFt", 0), + ("Rft", 0), + ("Bag", 1), + ("Sheet", 1), ("Bundle", 1), ("Cubic Ft", 0), ] for uom_name, whole in uoms: if not exists("UOM", uom_name): - d = frappe.get_doc({ - "doctype": "UOM", - "uom_name": uom_name, - "must_be_whole_number": whole, - }) + d = frappe.get_doc( + { + "doctype": "UOM", + "uom_name": uom_name, + "must_be_whole_number": whole, + } + ) d.flags.ignore_mandatory = True if safe_insert(d): ok(f"UOM: {uom_name}") @@ -90,25 +99,28 @@ def create_uoms(): # 2. ITEM GROUPS # ───────────────────────────────────────────────────────────── + def create_item_groups(): print("\n[2/9] Creating Item Groups...") groups = [ - ("Raw Materials - Furnitex", "All Item Groups"), - ("Plywood & Board", "Raw Materials - Furnitex"), - ("Laminates & Veneer", "Raw Materials - Furnitex"), - ("Hardware & Fittings", "Raw Materials - Furnitex"), + ("Raw Materials - Furnitex", "All Item Groups"), + ("Plywood & Board", "Raw Materials - Furnitex"), + ("Laminates & Veneer", "Raw Materials - Furnitex"), + ("Hardware & Fittings", "Raw Materials - Furnitex"), ("Civil & Surface Materials", "Raw Materials - Furnitex"), - ("Execution Services", "All Item Groups"), - ("Loose Furniture", "All Item Groups"), + ("Execution Services", "All Item Groups"), + ("Loose Furniture", "All Item Groups"), ] for name, parent in groups: if not exists("Item Group", name): - d = frappe.get_doc({ - "doctype": "Item Group", - "item_group_name": name, - "parent_item_group": parent, - "is_group": 0, - }) + d = frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": name, + "parent_item_group": parent, + "is_group": 0, + } + ) if safe_insert(d): ok(f"Item Group: {name}") else: @@ -122,20 +134,23 @@ def create_item_groups(): # 3. SUPPLIER GROUPS # ───────────────────────────────────────────────────────────── + def create_supplier_groups(): print("\n[3/9] Creating Supplier Groups...") groups = [ ("Local Market Vendor (Unregistered)", "All Supplier Groups"), - ("GST Registered Vendor", "All Supplier Groups"), - ("Labour Contractor", "All Supplier Groups"), + ("GST Registered Vendor", "All Supplier Groups"), + ("Labour Contractor", "All Supplier Groups"), ] for name, parent in groups: if not exists("Supplier Group", name): - d = frappe.get_doc({ - "doctype": "Supplier Group", - "supplier_group_name": name, - "parent_supplier_group": parent, - }) + d = frappe.get_doc( + { + "doctype": "Supplier Group", + "supplier_group_name": name, + "parent_supplier_group": parent, + } + ) if safe_insert(d): ok(f"Supplier Group: {name}") else: @@ -149,34 +164,35 @@ def create_supplier_groups(): # 4. WAREHOUSES # ───────────────────────────────────────────────────────────── + def create_warehouses(): print("\n[4/9] Creating Warehouses...") abbr = get_abbr() # Find the company's root warehouse group root_wh = frappe.db.get_value( - "Warehouse", - {"company": COMPANY, "is_group": 1}, - "name" + "Warehouse", {"company": COMPANY, "is_group": 1}, "name" ) if not root_wh: root_wh = f"All Warehouses - {abbr}" warehouses = [ - ("Main Store", root_wh, 0), + ("Main Store", root_wh, 0), ("Rejected Stock", root_wh, 0), ] for w_short, parent, is_group in warehouses: # ERPNext appends company abbr: "Main Store - F" w_full = f"{w_short} - {abbr}" if not exists("Warehouse", w_full): - d = frappe.get_doc({ - "doctype": "Warehouse", - "warehouse_name": w_short, - "parent_warehouse": parent, - "company": COMPANY, - "is_group": is_group, - }) + d = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": w_short, + "parent_warehouse": parent, + "company": COMPANY, + "is_group": is_group, + } + ) if safe_insert(d): ok(f"Warehouse: {w_full}") else: @@ -190,6 +206,7 @@ def create_warehouses(): # 5. TAX TEMPLATES # ───────────────────────────────────────────────────────────── + def _find_account(account_name_fragment, root_type=None, account_type=None): """Find an account by partial name match under the company.""" filters = {"company": COMPANY, "is_group": 0} @@ -199,8 +216,9 @@ def _find_account(account_name_fragment, root_type=None, account_type=None): filters["account_type"] = account_type # Try exact name match first - result = frappe.db.get_value("Account", - dict(filters, account_name=account_name_fragment), "name") + result = frappe.db.get_value( + "Account", dict(filters, account_name=account_name_fragment), "name" + ) if result: return result @@ -211,7 +229,8 @@ def _find_account(account_name_fragment, root_type=None, account_type=None): WHERE company=%s AND is_group=0 AND account_name LIKE %s LIMIT 1""", - (COMPANY, like_pattern), as_dict=0 + (COMPANY, like_pattern), + as_dict=0, ) return result[0][0] if result else None @@ -222,15 +241,18 @@ def create_tax_templates(): # ── No GST (URD Purchase) ── urd_title = "No GST - URD Purchase" - if not exists_filter("Purchase Taxes and Charges Template", - {"title": urd_title, "company": COMPANY}): - d = frappe.get_doc({ - "doctype": "Purchase Taxes and Charges Template", - "title": urd_title, - "company": COMPANY, - "is_default": 0, - "taxes": [], - }) + if not exists_filter( + "Purchase Taxes and Charges Template", {"title": urd_title, "company": COMPANY} + ): + d = frappe.get_doc( + { + "doctype": "Purchase Taxes and Charges Template", + "title": urd_title, + "company": COMPANY, + "is_default": 0, + "taxes": [], + } + ) if safe_insert(d): ok(f"Purchase Tax Template: {urd_title}") else: @@ -240,27 +262,45 @@ def create_tax_templates(): # ── GST 18% Purchase ── gst18_p_title = "GST 18% - Purchase" - if not exists_filter("Purchase Taxes and Charges Template", - {"title": gst18_p_title, "company": COMPANY}): + if not exists_filter( + "Purchase Taxes and Charges Template", + {"title": gst18_p_title, "company": COMPANY}, + ): cgst = _find_account("CGST") sgst = _find_account("SGST") taxes = [] if cgst: - taxes.append({"charge_type": "On Net Total", "account_head": cgst, - "rate": 9, "description": "CGST @ 9%"}) + taxes.append( + { + "charge_type": "On Net Total", + "account_head": cgst, + "rate": 9, + "description": "CGST @ 9%", + } + ) if sgst: - taxes.append({"charge_type": "On Net Total", "account_head": sgst, - "rate": 9, "description": "SGST @ 9%"}) - d = frappe.get_doc({ - "doctype": "Purchase Taxes and Charges Template", - "title": gst18_p_title, - "company": COMPANY, - "is_default": 0, - "taxes": taxes, - }) + taxes.append( + { + "charge_type": "On Net Total", + "account_head": sgst, + "rate": 9, + "description": "SGST @ 9%", + } + ) + d = frappe.get_doc( + { + "doctype": "Purchase Taxes and Charges Template", + "title": gst18_p_title, + "company": COMPANY, + "is_default": 0, + "taxes": taxes, + } + ) if safe_insert(d): - ok(f"Purchase Tax Template: {gst18_p_title}" + - (" (no GST accounts in CoA, left empty)" if not taxes else "")) + ok( + f"Purchase Tax Template: {gst18_p_title}" + + (" (no GST accounts in CoA, left empty)" if not taxes else "") + ) else: skip(f"Purchase Tax Template: {gst18_p_title} (duplicate)") else: @@ -268,27 +308,44 @@ def create_tax_templates(): # ── GST 18% Sales ── gst18_s_title = "GST 18% - Sales" - if not exists_filter("Sales Taxes and Charges Template", - {"title": gst18_s_title, "company": COMPANY}): + if not exists_filter( + "Sales Taxes and Charges Template", {"title": gst18_s_title, "company": COMPANY} + ): cgst = _find_account("CGST") sgst = _find_account("SGST") taxes = [] if cgst: - taxes.append({"charge_type": "On Net Total", "account_head": cgst, - "rate": 9, "description": "CGST @ 9%"}) + taxes.append( + { + "charge_type": "On Net Total", + "account_head": cgst, + "rate": 9, + "description": "CGST @ 9%", + } + ) if sgst: - taxes.append({"charge_type": "On Net Total", "account_head": sgst, - "rate": 9, "description": "SGST @ 9%"}) - d = frappe.get_doc({ - "doctype": "Sales Taxes and Charges Template", - "title": gst18_s_title, - "company": COMPANY, - "is_default": 0, - "taxes": taxes, - }) + taxes.append( + { + "charge_type": "On Net Total", + "account_head": sgst, + "rate": 9, + "description": "SGST @ 9%", + } + ) + d = frappe.get_doc( + { + "doctype": "Sales Taxes and Charges Template", + "title": gst18_s_title, + "company": COMPANY, + "is_default": 0, + "taxes": taxes, + } + ) if safe_insert(d): - ok(f"Sales Tax Template: {gst18_s_title}" + - (" (no GST accounts in CoA, left empty)" if not taxes else "")) + ok( + f"Sales Tax Template: {gst18_s_title}" + + (" (no GST accounts in CoA, left empty)" if not taxes else "") + ) else: skip(f"Sales Tax Template: {gst18_s_title} (duplicate)") else: @@ -301,43 +358,92 @@ def create_tax_templates(): # 6. SERVICE ITEMS (Non-stock billing items) # ───────────────────────────────────────────────────────────── + def create_service_items(): print("\n[6/9] Creating Service Items...") - income_acct = _find_account("Sales", root_type="Income") or \ - _find_account("Service", root_type="Income") + income_acct = _find_account("Sales", root_type="Income") or _find_account( + "Service", root_type="Income" + ) items = [ # (code, name, uom, description) - ("SVC-FC-EXEC", "False Ceiling Execution", "SqFt", "Labour + material for false ceiling. Bill per SqFt OR as lumpsum."), - ("SVC-WAR-LAM", "Laminate Wardrobe Fabrication", "SqFt", "Laminate wardrobe fabrication per SqFt."), - ("SVC-KIT-EXEC", "Modular Kitchen Execution", "SqFt", "Modular kitchen fabrication and installation."), - ("SVC-FLOOR", "Flooring Execution", "SqFt", "Vinyl/Wood/Tile flooring supply and installation."), - ("SVC-LUMP", "Lumpsum Contract Work", "Nos", "Fixed-price lumpsum milestone. Change qty to 1 always."), - ("SVC-DESIGN", "Interior Design Consultation", "Nos", "Design + drawing fee — lumpsum."), - ("SVC-CONVEY", "Site Conveyance & Transport", "Nos", "Transport charges to/from site."), - ("SVC-LABOUR", "Direct Site Labour", "Nos", "Daily-wage labour charges."), - ("SVC-ELECTRIC", "Electrical Work Execution", "Nos", "Electrical points, wiring, fitting."), - ("SVC-PAINT", "Painting & Polish Execution", "SqFt", "Wall painting / wood polish per SqFt."), + ( + "SVC-FC-EXEC", + "False Ceiling Execution", + "SqFt", + "Labour + material for false ceiling. Bill per SqFt OR as lumpsum.", + ), + ( + "SVC-WAR-LAM", + "Laminate Wardrobe Fabrication", + "SqFt", + "Laminate wardrobe fabrication per SqFt.", + ), + ( + "SVC-KIT-EXEC", + "Modular Kitchen Execution", + "SqFt", + "Modular kitchen fabrication and installation.", + ), + ( + "SVC-FLOOR", + "Flooring Execution", + "SqFt", + "Vinyl/Wood/Tile flooring supply and installation.", + ), + ( + "SVC-LUMP", + "Lumpsum Contract Work", + "Nos", + "Fixed-price lumpsum milestone. Change qty to 1 always.", + ), + ( + "SVC-DESIGN", + "Interior Design Consultation", + "Nos", + "Design + drawing fee — lumpsum.", + ), + ( + "SVC-CONVEY", + "Site Conveyance & Transport", + "Nos", + "Transport charges to/from site.", + ), + ("SVC-LABOUR", "Direct Site Labour", "Nos", "Daily-wage labour charges."), + ( + "SVC-ELECTRIC", + "Electrical Work Execution", + "Nos", + "Electrical points, wiring, fitting.", + ), + ( + "SVC-PAINT", + "Painting & Polish Execution", + "SqFt", + "Wall painting / wood polish per SqFt.", + ), ] for code, name, uom, desc in items: if not exists("Item", code): # Non-stock service items: no warehouse needed in item_defaults - d = frappe.get_doc({ - "doctype": "Item", - "item_code": code, - "item_name": name, - "item_group": "Execution Services", - "description": desc, - "stock_uom": uom, - "sales_uom": uom, - "is_stock_item": 0, - "is_purchase_item": 0, - "is_sales_item": 1, - "standard_rate": 0, - # No item_defaults row — avoids warehouse company-mismatch validation - }) + d = frappe.get_doc( + { + "doctype": "Item", + "item_code": code, + "item_name": name, + "item_group": "Execution Services", + "description": desc, + "stock_uom": uom, + "sales_uom": uom, + "is_stock_item": 0, + "is_purchase_item": 0, + "is_sales_item": 1, + "standard_rate": 0, + # No item_defaults row — avoids warehouse company-mismatch validation + } + ) if safe_insert(d): ok(f"Service Item: {code} [{uom}]") else: @@ -352,12 +458,15 @@ def create_service_items(): # 7. RAW MATERIAL ITEMS (Stock items) # ───────────────────────────────────────────────────────────── + def create_raw_material_items(): print("\n[7/9] Creating Raw Material Items...") - cogs_acct = (_find_account("Cost of Goods Sold", root_type="Expense") or - _find_account("Stock Expenses", root_type="Expense") or - _find_account("Expenses Included", root_type="Expense")) + cogs_acct = ( + _find_account("Cost of Goods Sold", root_type="Expense") + or _find_account("Stock Expenses", root_type="Expense") + or _find_account("Expenses Included", root_type="Expense") + ) default_wh = f"Main Store - {get_abbr()}" if not exists("Warehouse", default_wh): @@ -367,39 +476,44 @@ def create_raw_material_items(): items = [ # (code, name, uom, group) - ("RM-PLY-19MM", "Plywood 19mm BWR 8x4", "Sheet", "Plywood & Board"), - ("RM-PLY-12MM", "Plywood 12mm BWR 8x4", "Sheet", "Plywood & Board"), - ("RM-PLY-6MM", "Plywood 6mm 8x4", "Sheet", "Plywood & Board"), - ("RM-MDF-18MM", "MDF Board 18mm 8x4", "Sheet", "Plywood & Board"), - ("RM-HDF-3MM", "HDF 3mm 8x4", "Sheet", "Plywood & Board"), - ("RM-LAM-1MM", "Laminate Sheet 1mm 8x4", "Sheet", "Laminates & Veneer"), - ("RM-VEN-NAT", "Veneer Sheet Natural Wood", "Sheet", "Laminates & Veneer"), - ("RM-LAM-ACRY", "Acrylic Laminate Sheet", "Sheet", "Laminates & Veneer"), - ("RM-HW-HINGE", "Concealed Hinge (pair)", "Nos", "Hardware & Fittings"), - ("RM-HW-CHAN18", "Drawer Channel 18 inch", "Nos", "Hardware & Fittings"), - ("RM-HW-CHAN24", "Drawer Channel 24 inch", "Nos", "Hardware & Fittings"), - ("RM-HW-HANDLE", "Cabinet Handle", "Nos", "Hardware & Fittings"), - ("RM-HW-LOCK", "Drawer Lock", "Nos", "Hardware & Fittings"), - ("RM-HW-SCREW", "Wood Screw Assorted", "Bundle", "Hardware & Fittings"), - ("RM-CIV-CEM", "OPC Cement 53 Grade", "Bag", "Civil & Surface Materials"), - ("RM-CIV-PUTTY", "Wall Putty White", "Bag", "Civil & Surface Materials"), - ("RM-CIV-PRIMER","Primer Interior", "Nos", "Civil & Surface Materials"), - ("RM-CIV-GYPS", "Gypsum Board 8x4 12.5mm", "Sheet", "Civil & Surface Materials"), - ("RM-CIV-GYPS-C","Gypsum Cornice / Grid", "Rft", "Civil & Surface Materials"), + ("RM-PLY-19MM", "Plywood 19mm BWR 8x4", "Sheet", "Plywood & Board"), + ("RM-PLY-12MM", "Plywood 12mm BWR 8x4", "Sheet", "Plywood & Board"), + ("RM-PLY-6MM", "Plywood 6mm 8x4", "Sheet", "Plywood & Board"), + ("RM-MDF-18MM", "MDF Board 18mm 8x4", "Sheet", "Plywood & Board"), + ("RM-HDF-3MM", "HDF 3mm 8x4", "Sheet", "Plywood & Board"), + ("RM-LAM-1MM", "Laminate Sheet 1mm 8x4", "Sheet", "Laminates & Veneer"), + ("RM-VEN-NAT", "Veneer Sheet Natural Wood", "Sheet", "Laminates & Veneer"), + ("RM-LAM-ACRY", "Acrylic Laminate Sheet", "Sheet", "Laminates & Veneer"), + ("RM-HW-HINGE", "Concealed Hinge (pair)", "Nos", "Hardware & Fittings"), + ("RM-HW-CHAN18", "Drawer Channel 18 inch", "Nos", "Hardware & Fittings"), + ("RM-HW-CHAN24", "Drawer Channel 24 inch", "Nos", "Hardware & Fittings"), + ("RM-HW-HANDLE", "Cabinet Handle", "Nos", "Hardware & Fittings"), + ("RM-HW-LOCK", "Drawer Lock", "Nos", "Hardware & Fittings"), + ("RM-HW-SCREW", "Wood Screw Assorted", "Bundle", "Hardware & Fittings"), + ("RM-CIV-CEM", "OPC Cement 53 Grade", "Bag", "Civil & Surface Materials"), + ("RM-CIV-PUTTY", "Wall Putty White", "Bag", "Civil & Surface Materials"), + ("RM-CIV-PRIMER", "Primer Interior", "Nos", "Civil & Surface Materials"), + ( + "RM-CIV-GYPS", + "Gypsum Board 8x4 12.5mm", + "Sheet", + "Civil & Surface Materials", + ), + ("RM-CIV-GYPS-C", "Gypsum Cornice / Grid", "Rft", "Civil & Surface Materials"), ] for code, name, uom, group in items: if not exists("Item", code): d_dict = { - "doctype": "Item", - "item_code": code, - "item_name": name, - "item_group": group, - "stock_uom": uom, - "is_stock_item": 1, - "is_purchase_item": 1, - "is_sales_item": 0, - "valuation_method": "FIFO", + "doctype": "Item", + "item_code": code, + "item_name": name, + "item_group": group, + "stock_uom": uom, + "is_stock_item": 1, + "is_purchase_item": 1, + "is_sales_item": 0, + "valuation_method": "FIFO", } # Only add item_defaults if we have a valid Furnitex warehouse if default_wh or cogs_acct: @@ -425,29 +539,52 @@ def create_raw_material_items(): # 8. SAMPLE SUPPLIERS # ───────────────────────────────────────────────────────────── + def create_suppliers(): print("\n[8/9] Creating Sample Suppliers...") suppliers = [ # (name, group, gst_category) - ("Local Hardware Market - Kolkata", "Local Market Vendor (Unregistered)", "Unregistered"), - ("Burrabazar Plywood Supplier", "Local Market Vendor (Unregistered)", "Unregistered"), - ("Fancy Laminates - Howrah", "Local Market Vendor (Unregistered)", "Unregistered"), - ("Modern Furniture Hardware - BBD Bag","Local Market Vendor (Unregistered)", "Unregistered"), - ("Registered Hardware Supplier Ltd", "GST Registered Vendor", "Registered Regular"), - ("Site Labour Contractor - Ramesh", "Labour Contractor", "Unregistered"), + ( + "Local Hardware Market - Kolkata", + "Local Market Vendor (Unregistered)", + "Unregistered", + ), + ( + "Burrabazar Plywood Supplier", + "Local Market Vendor (Unregistered)", + "Unregistered", + ), + ( + "Fancy Laminates - Howrah", + "Local Market Vendor (Unregistered)", + "Unregistered", + ), + ( + "Modern Furniture Hardware - BBD Bag", + "Local Market Vendor (Unregistered)", + "Unregistered", + ), + ( + "Registered Hardware Supplier Ltd", + "GST Registered Vendor", + "Registered Regular", + ), + ("Site Labour Contractor - Ramesh", "Labour Contractor", "Unregistered"), ] for name, group, gst_cat in suppliers: if not exists("Supplier", name): - d = frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": name, - "supplier_group": group, - "country": "India", - "gst_category": gst_cat, - "default_currency": "INR", - }) + d = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": name, + "supplier_group": group, + "country": "India", + "gst_category": gst_cat, + "default_currency": "INR", + } + ) if safe_insert(d): ok(f"Supplier: {name} [{gst_cat}]") else: @@ -461,8 +598,9 @@ def create_suppliers(): # URD tax bypass is handled instead by the server script: # "Furnitex - Clear GST on URD Purchase" (Before Save on Purchase Invoice) # Tick the "URD Purchase (No GST)" checkbox on any invoice to auto-clear taxes. - urd_suppliers_count = frappe.db.count("Supplier", - {"supplier_group": "Local Market Vendor (Unregistered)"}) + urd_suppliers_count = frappe.db.count( + "Supplier", {"supplier_group": "Local Market Vendor (Unregistered)"} + ) ok(f"URD tax via server script — {urd_suppliers_count} URD suppliers registered") @@ -470,6 +608,7 @@ def create_suppliers(): # 9. CUSTOM FIELDS # ───────────────────────────────────────────────────────────── + def create_custom_fields(): print("\n[9/9] Creating Custom Fields...") @@ -481,25 +620,31 @@ def create_custom_fields(): # (dt, fieldname, label, fieldtype, options, insert_after, in_list_view) fields = [ - ("Purchase Invoice", "is_urd_purchase", "URD Purchase (No GST)", - "Check", None, "supplier", 1), - ("Journal Entry", "project", "Project", - "Link", "Project", "voucher_type", 0), + ( + "Purchase Invoice", + "is_urd_purchase", + "URD Purchase (No GST)", + "Check", + None, + "supplier", + 1, + ), + ("Journal Entry", "project", "Project", "Link", "Project", "voucher_type", 0), ] for dt, fn, label, ft, opts, after, in_list in fields: cf_name = f"{dt}-{fn}" if not exists("Custom Field", cf_name): d_dict = { - "doctype": "Custom Field", - "dt": dt, - "fieldname": fn, - "label": label, - "fieldtype": ft, - "insert_after": after, - "in_list_view": in_list, + "doctype": "Custom Field", + "dt": dt, + "fieldname": fn, + "label": label, + "fieldtype": ft, + "insert_after": after, + "in_list_view": in_list, "in_standard_filter": 1, - "search_index": 1, + "search_index": 1, } if opts: d_dict["options"] = opts @@ -512,19 +657,22 @@ def create_custom_fields(): skip(f"Custom Field: {dt}.{fn}") frappe.db.commit() - ok("Native 'project' field already present on PI, PO, Stock Entry, DN — no custom fields needed there") + ok( + "Native 'project' field already present on PI, PO, Stock Entry, DN — no custom fields needed there" + ) # ───────────────────────────────────────────────────────────── # 10. SERVER SCRIPTS (Automation) # ───────────────────────────────────────────────────────────── + def create_server_scripts(): print("\n[+] Creating Server Scripts...") # Script 1: Auto-create OnSite WIP Warehouse on Project insert - wip_script_name = "Furnitex - Auto Create OnSite WIP Warehouse" - wip_script_code = """ + wip_script_name = "Furnitex - Auto Create OnSite WIP Warehouse" + wip_script_code = """ # Auto-fires after a new Project is saved # Creates "{Project Name} - OnSite WIP" warehouse automatically @@ -560,15 +708,17 @@ if not frappe.db.exists("Warehouse", warehouse_name): """ if not exists("Server Script", wip_script_name): - d = frappe.get_doc({ - "doctype": "Server Script", - "name": wip_script_name, - "script_type": "DocType Event", - "reference_doctype": "Project", - "doctype_event": "After Insert", - "enabled": 1, - "script": wip_script_code, - }) + d = frappe.get_doc( + { + "doctype": "Server Script", + "name": wip_script_name, + "script_type": "DocType Event", + "reference_doctype": "Project", + "doctype_event": "After Insert", + "enabled": 1, + "script": wip_script_code, + } + ) d.flags.ignore_permissions = True d.insert() ok(f"Server Script: {wip_script_name}") @@ -597,15 +747,17 @@ if doc.is_urd_purchase: """ if not exists("Server Script", urd_script_name): - d = frappe.get_doc({ - "doctype": "Server Script", - "name": urd_script_name, - "script_type": "DocType Event", - "reference_doctype": "Purchase Invoice", - "doctype_event": "Before Save", - "enabled": 1, - "script": urd_script_code, - }) + d = frappe.get_doc( + { + "doctype": "Server Script", + "name": urd_script_name, + "script_type": "DocType Event", + "reference_doctype": "Purchase Invoice", + "doctype_event": "Before Save", + "enabled": 1, + "script": urd_script_code, + } + ) d.flags.ignore_permissions = True d.insert() ok(f"Server Script: {urd_script_name}") @@ -706,19 +858,22 @@ def get_data(filters): return rows ''' + def create_custom_report(): print("\n[+] Creating Custom Report: Furnitex Project Profitability...") report_name = "Furnitex Project Profitability" if not exists("Report", report_name): - d = frappe.get_doc({ - "doctype": "Report", - "report_name": report_name, - "ref_doctype": "Project", - "report_type": "Script Report", - "is_standard": "No", - "module": "Projects", - "script": PROFIT_REPORT_SCRIPT, - }) + d = frappe.get_doc( + { + "doctype": "Report", + "report_name": report_name, + "ref_doctype": "Project", + "report_type": "Script Report", + "is_standard": "No", + "module": "Projects", + "script": PROFIT_REPORT_SCRIPT, + } + ) d.flags.ignore_permissions = True d.insert() ok(f"Custom Report: {report_name}") @@ -731,6 +886,7 @@ def create_custom_report(): # MASTER RUNNER # ───────────────────────────────────────────────────────────── + def run_all(): frappe.set_user("Administrator") print("\n" + "=" * 58) @@ -759,6 +915,7 @@ def run_all(): if __name__ == "__main__": import sys + frappe.init(site="frontend") frappe.connect() run_all() diff --git a/setup_furnitex_crm_billing.py b/setup_furnitex_crm_billing.py index 62720f40..459d8112 100644 --- a/setup_furnitex_crm_billing.py +++ b/setup_furnitex_crm_billing.py @@ -9,13 +9,18 @@ import frappe COMPANY = "Furnitex" -def ok(msg): print(f" [OK] {msg}") -def skip(msg): print(f" [SKIP] {msg}") +def ok(msg): + print(f" [OK] {msg}") + + +def skip(msg): + print(f" [SKIP] {msg}") + def safe_insert(doc): try: doc.flags.ignore_permissions = True - doc.flags.ignore_mandatory = True + doc.flags.ignore_mandatory = True doc.insert() return True except frappe.DuplicateEntryError: @@ -25,9 +30,11 @@ def safe_insert(doc): return False raise + def exists(dt, name): return frappe.db.exists(dt, name) + def exists_f(dt, filters): return frappe.db.get_value(dt, filters, "name") @@ -38,24 +45,28 @@ def exists_f(dt, filters): def create_customer_groups(): print("\n[1] Customer Groups...") groups = [ - ("Residential - Individual", "Individual"), - ("Residential - Builder Flat","Individual"), - ("Commercial - Office", "Commercial"), - ("Commercial - Restaurant", "Commercial"), - ("Commercial - Hotel", "Commercial"), - ("Commercial - Retail", "Commercial"), - ("Builder / Developer", "Commercial"), + ("Residential - Individual", "Individual"), + ("Residential - Builder Flat", "Individual"), + ("Commercial - Office", "Commercial"), + ("Commercial - Restaurant", "Commercial"), + ("Commercial - Hotel", "Commercial"), + ("Commercial - Retail", "Commercial"), + ("Builder / Developer", "Commercial"), ] for name, parent in groups: if not exists("Customer Group", name): - d = frappe.get_doc({ - "doctype": "Customer Group", - "customer_group_name": name, - "parent_customer_group": parent, - "is_group": 0, - }) - if safe_insert(d): ok(f"Customer Group: {name}") - else: skip(f"{name} (dup)") + d = frappe.get_doc( + { + "doctype": "Customer Group", + "customer_group_name": name, + "parent_customer_group": parent, + "is_group": 0, + } + ) + if safe_insert(d): + ok(f"Customer Group: {name}") + else: + skip(f"{name} (dup)") else: skip(f"Customer Group: {name}") frappe.db.commit() @@ -68,26 +79,30 @@ def create_territories(): print("\n[2] Territories...") # (name, parent) territories = [ - ("West Bengal", "India"), - ("Kolkata", "West Bengal"), - ("South Kolkata", "Kolkata"), - ("North Kolkata", "Kolkata"), - ("Salt Lake", "Kolkata"), - ("New Town", "Kolkata"), - ("Rajarhat", "Kolkata"), - ("Howrah", "West Bengal"), - ("Hooghly", "West Bengal"), + ("West Bengal", "India"), + ("Kolkata", "West Bengal"), + ("South Kolkata", "Kolkata"), + ("North Kolkata", "Kolkata"), + ("Salt Lake", "Kolkata"), + ("New Town", "Kolkata"), + ("Rajarhat", "Kolkata"), + ("Howrah", "West Bengal"), + ("Hooghly", "West Bengal"), ] for name, parent in territories: if not exists("Territory", name): - d = frappe.get_doc({ - "doctype": "Territory", - "territory_name": name, - "parent_territory": parent, - "is_group": 0, - }) - if safe_insert(d): ok(f"Territory: {name}") - else: skip(f"{name} (dup)") + d = frappe.get_doc( + { + "doctype": "Territory", + "territory_name": name, + "parent_territory": parent, + "is_group": 0, + } + ) + if safe_insert(d): + ok(f"Territory: {name}") + else: + skip(f"{name} (dup)") else: skip(f"Territory: {name}") frappe.db.commit() @@ -96,45 +111,55 @@ def create_territories(): # ────────────────────────────────────────────────────────────── # 3. LEAD SOURCES (added as custom Select field on Lead) # ────────────────────────────────────────────────────────────── -LEAD_SOURCES = "\n".join([ - "Instagram", - "Facebook", - "Word of Mouth", - "Client Referral", - "Just Dial", - "Housing.com", - "99acres / MagicBricks", - "Site Board / Hoarding", - "Google Search", - "Direct Walk-In", - "Architect / Designer Referral", - "Builder Tie-up", - "Exhibition / Home Fair", - "YouTube", - "WhatsApp Broadcast", -]) +LEAD_SOURCES = "\n".join( + [ + "Instagram", + "Facebook", + "Word of Mouth", + "Client Referral", + "Just Dial", + "Housing.com", + "99acres / MagicBricks", + "Site Board / Hoarding", + "Google Search", + "Direct Walk-In", + "Architect / Designer Referral", + "Builder Tie-up", + "Exhibition / Home Fair", + "YouTube", + "WhatsApp Broadcast", + ] +) + def create_lead_sources(): # Lead Source is a standalone CRM-app doctype not installed here. # We add it as a custom Select field on Lead + Opportunity instead. print("\n[3] Lead Sources (as custom Select field)...") - for dt, after_field in [("Lead", "qualification_status"), ("Opportunity", "opportunity_type")]: + for dt, after_field in [ + ("Lead", "qualification_status"), + ("Opportunity", "opportunity_type"), + ]: cf_name = f"{dt}-furnitex_lead_source" if not exists("Custom Field", cf_name): - d = frappe.get_doc({ - "doctype": "Custom Field", - "dt": dt, - "fieldname": "furnitex_lead_source", - "label": "Lead Source", - "fieldtype": "Select", - "options": LEAD_SOURCES, - "insert_after": after_field, - "in_list_view": 1, - "in_standard_filter": 1, - }) - if safe_insert(d): ok(f"Lead Source Select field → {dt}") - else: skip(f"{dt}.furnitex_lead_source (dup)") + d = frappe.get_doc( + { + "doctype": "Custom Field", + "dt": dt, + "fieldname": "furnitex_lead_source", + "label": "Lead Source", + "fieldtype": "Select", + "options": LEAD_SOURCES, + "insert_after": after_field, + "in_list_view": 1, + "in_standard_filter": 1, + } + ) + if safe_insert(d): + ok(f"Lead Source Select field → {dt}") + else: + skip(f"{dt}.furnitex_lead_source (dup)") else: skip(f"Lead Source field exists on {dt}") frappe.db.commit() @@ -146,21 +171,25 @@ def create_lead_sources(): def create_sales_persons(): print("\n[4] Sales Persons...") persons = [ - ("Furnitex - Site Team", "Sales Team"), - ("Furnitex - Design Team","Sales Team"), + ("Furnitex - Site Team", "Sales Team"), + ("Furnitex - Design Team", "Sales Team"), ("Furnitex - BD Manager", "Sales Team"), ] for name, parent in persons: if not exists("Sales Person", name): - d = frappe.get_doc({ - "doctype": "Sales Person", - "sales_person_name": name, - "parent_sales_person": parent, - "is_group": 0, - "enabled": 1, - }) - if safe_insert(d): ok(f"Sales Person: {name}") - else: skip(f"{name} (dup)") + d = frappe.get_doc( + { + "doctype": "Sales Person", + "sales_person_name": name, + "parent_sales_person": parent, + "is_group": 0, + "enabled": 1, + } + ) + if safe_insert(d): + ok(f"Sales Person: {name}") + else: + skip(f"{name} (dup)") else: skip(f"Sales Person: {name}") frappe.db.commit() @@ -172,36 +201,42 @@ def create_sales_persons(): def create_payment_modes(): print("\n[5] Modes of Payment...") modes = [ - ("NEFT / RTGS", "Bank"), - ("IMPS", "Bank"), - ("Google Pay", "Bank"), - ("PhonePe", "Bank"), - ("Paytm", "Bank"), - ("Cash - Site", "Cash"), + ("NEFT / RTGS", "Bank"), + ("IMPS", "Bank"), + ("Google Pay", "Bank"), + ("PhonePe", "Bank"), + ("Paytm", "Bank"), + ("Cash - Site", "Cash"), ] # find default bank account - bank_acct = frappe.db.get_value("Account", - {"account_type": "Bank", "company": COMPANY, "is_group": 0}, "name") - cash_acct = frappe.db.get_value("Account", - {"account_type": "Cash", "company": COMPANY, "is_group": 0}, "name") + bank_acct = frappe.db.get_value( + "Account", {"account_type": "Bank", "company": COMPANY, "is_group": 0}, "name" + ) + cash_acct = frappe.db.get_value( + "Account", {"account_type": "Cash", "company": COMPANY, "is_group": 0}, "name" + ) for name, mtype in modes: if not exists("Mode of Payment", name): d_dict = { - "doctype": "Mode of Payment", - "mode_of_payment": name, - "type": mtype, + "doctype": "Mode of Payment", + "mode_of_payment": name, + "type": mtype, } acct = bank_acct if mtype == "Bank" else cash_acct if acct: - d_dict["accounts"] = [{ - "company": COMPANY, - "default_account": acct, - }] + d_dict["accounts"] = [ + { + "company": COMPANY, + "default_account": acct, + } + ] d = frappe.get_doc(d_dict) - if safe_insert(d): ok(f"Mode of Payment: {name}") - else: skip(f"{name} (dup)") + if safe_insert(d): + ok(f"Mode of Payment: {name}") + else: + skip(f"{name} (dup)") else: skip(f"Mode of Payment: {name}") frappe.db.commit() @@ -214,62 +249,88 @@ def create_payment_terms(): print("\n[6] Payment Terms Templates...") templates = [ - # ── A: Standard Interior Project (milestone-based) ─────── { "name": "Furnitex - Standard Interior Project", "terms": [ - {"payment_term_name": "30% Advance on Agreement", - "invoice_portion": 30, "credit_days": 0, - "description": "Booking advance on signing agreement"}, - {"payment_term_name": "30% on 50% Site Completion", - "invoice_portion": 30, "credit_days": 30, - "description": "Second milestone: 50% work done"}, - {"payment_term_name": "30% on Work Completion", - "invoice_portion": 30, "credit_days": 60, - "description": "Third milestone: work complete"}, - {"payment_term_name": "10% on Final Handover", - "invoice_portion": 10, "credit_days": 75, - "description": "Retention released at handover"}, + { + "payment_term_name": "30% Advance on Agreement", + "invoice_portion": 30, + "credit_days": 0, + "description": "Booking advance on signing agreement", + }, + { + "payment_term_name": "30% on 50% Site Completion", + "invoice_portion": 30, + "credit_days": 30, + "description": "Second milestone: 50% work done", + }, + { + "payment_term_name": "30% on Work Completion", + "invoice_portion": 30, + "credit_days": 60, + "description": "Third milestone: work complete", + }, + { + "payment_term_name": "10% on Final Handover", + "invoice_portion": 10, + "credit_days": 75, + "description": "Retention released at handover", + }, ], }, - # ── B: 50-50 (smaller projects) ────────────────────────── { "name": "Furnitex - 50-50 Advance", "terms": [ - {"payment_term_name": "50% Advance", - "invoice_portion": 50, "credit_days": 0, - "description": "Advance before work begins"}, - {"payment_term_name": "50% on Delivery", - "invoice_portion": 50, "credit_days": 30, - "description": "Balance on delivery / installation"}, + { + "payment_term_name": "50% Advance", + "invoice_portion": 50, + "credit_days": 0, + "description": "Advance before work begins", + }, + { + "payment_term_name": "50% on Delivery", + "invoice_portion": 50, + "credit_days": 30, + "description": "Balance on delivery / installation", + }, ], }, - # ── C: 100% Advance (small orders / loose furniture) ───── { "name": "Furnitex - 100% Advance", "terms": [ - {"payment_term_name": "100% Advance", - "invoice_portion": 100, "credit_days": 0, - "description": "Full payment before production"}, + { + "payment_term_name": "100% Advance", + "invoice_portion": 100, + "credit_days": 0, + "description": "Full payment before production", + }, ], }, - # ── D: Lumpsum 3-stage (commercial projects) ───────────── { "name": "Furnitex - Commercial 3-Stage", "terms": [ - {"payment_term_name": "40% Advance - Commercial", - "invoice_portion": 40, "credit_days": 0, - "description": "Mobilisation advance"}, - {"payment_term_name": "40% Mid-Stage - Commercial", - "invoice_portion": 40, "credit_days": 45, - "description": "Mid-project milestone"}, - {"payment_term_name": "20% Retention - Commercial", - "invoice_portion": 20, "credit_days": 90, - "description": "Retention on final handover"}, + { + "payment_term_name": "40% Advance - Commercial", + "invoice_portion": 40, + "credit_days": 0, + "description": "Mobilisation advance", + }, + { + "payment_term_name": "40% Mid-Stage - Commercial", + "invoice_portion": 40, + "credit_days": 45, + "description": "Mid-project milestone", + }, + { + "payment_term_name": "20% Retention - Commercial", + "invoice_portion": 20, + "credit_days": 90, + "description": "Retention on final handover", + }, ], }, ] @@ -282,30 +343,38 @@ def create_payment_terms(): for term in t["terms"]: pt_name = term["payment_term_name"] if not exists("Payment Term", pt_name): - pt = frappe.get_doc({ - "doctype": "Payment Term", - "payment_term_name": pt_name, - "invoice_portion": term["invoice_portion"], - "credit_days_based_on": "Day(s) after invoice date", - "credit_days": term["credit_days"], - "description": term["description"], - }) + pt = frappe.get_doc( + { + "doctype": "Payment Term", + "payment_term_name": pt_name, + "invoice_portion": term["invoice_portion"], + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": term["credit_days"], + "description": term["description"], + } + ) safe_insert(pt) - term_rows.append({ - "payment_term": pt_name, - "invoice_portion": term["invoice_portion"], - "credit_days_based_on": "Day(s) after invoice date", - "credit_days": term["credit_days"], - "description": term["description"], - }) + term_rows.append( + { + "payment_term": pt_name, + "invoice_portion": term["invoice_portion"], + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": term["credit_days"], + "description": term["description"], + } + ) - d = frappe.get_doc({ - "doctype": "Payment Terms Template", - "template_name": tname, - "terms": term_rows, - }) - if safe_insert(d): ok(f"Payment Terms: {tname}") - else: skip(f"{tname} (dup)") + d = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": tname, + "terms": term_rows, + } + ) + if safe_insert(d): + ok(f"Payment Terms: {tname}") + else: + skip(f"{tname} (dup)") else: skip(f"Payment Terms: {tname}") @@ -319,7 +388,6 @@ def create_terms_conditions(): print("\n[7] Terms & Conditions Templates...") templates = [ - # ── Quotation T&C ───────────────────────────────────────── { "title": "Furnitex - Quotation Terms", @@ -337,7 +405,6 @@ def create_terms_conditions():

Furnitex | Interior Design & Furniture | Kolkata

""", }, - # ── Sales Invoice T&C ───────────────────────────────────── { "title": "Furnitex - Invoice Terms", @@ -352,7 +419,6 @@ def create_terms_conditions():

Bank: [Your Bank] | A/c: [Account No] | IFSC: [IFSC] | UPI: [UPI ID]

Thank you for choosing Furnitex!

""", }, - # ── Purchase Order T&C ──────────────────────────────────── { "title": "Furnitex - Purchase Order Terms", @@ -368,13 +434,17 @@ def create_terms_conditions(): for t in templates: if not exists("Terms and Conditions", t["title"]): - d = frappe.get_doc({ - "doctype": "Terms and Conditions", - "title": t["title"], - "terms": t["terms"], - }) - if safe_insert(d): ok(f"T&C: {t['title']}") - else: skip(f"T&C: {t['title']} (dup)") + d = frappe.get_doc( + { + "doctype": "Terms and Conditions", + "title": t["title"], + "terms": t["terms"], + } + ) + if safe_insert(d): + ok(f"T&C: {t['title']}") + else: + skip(f"T&C: {t['title']} (dup)") else: skip(f"T&C: {t['title']}") frappe.db.commit() @@ -388,16 +458,20 @@ def create_price_list(): pl_name = "Furnitex Interior Rate Card" if not exists("Price List", pl_name): - d = frappe.get_doc({ - "doctype": "Price List", - "price_list_name": pl_name, - "currency": "INR", - "selling": 1, - "buying": 0, - "enabled": 1, - }) - if safe_insert(d): ok(f"Price List: {pl_name}") - else: skip(f"{pl_name} (dup)") + d = frappe.get_doc( + { + "doctype": "Price List", + "price_list_name": pl_name, + "currency": "INR", + "selling": 1, + "buying": 0, + "enabled": 1, + } + ) + if safe_insert(d): + ok(f"Price List: {pl_name}") + else: + skip(f"{pl_name} (dup)") else: skip(f"Price List: {pl_name}") @@ -406,16 +480,16 @@ def create_price_list(): # ── Standard Item Prices (on both Standard Selling + Interior Rate Card) ── item_prices = [ # (item_code, rate) — per SqFt or per Nos - ("SVC-FC-EXEC", 185), # False Ceiling / SqFt - ("SVC-WAR-LAM", 950), # Laminate Wardrobe / SqFt - ("SVC-KIT-EXEC", 1100), # Modular Kitchen / SqFt - ("SVC-FLOOR", 120), # Flooring / SqFt - ("SVC-LUMP", 0), # Lumpsum — rate set per project - ("SVC-DESIGN", 15000), # Design consultation / Nos - ("SVC-CONVEY", 2000), # Conveyance / trip - ("SVC-LABOUR", 700), # Labour / day - ("SVC-ELECTRIC", 0), # Electrical — per quote - ("SVC-PAINT", 35), # Painting / SqFt + ("SVC-FC-EXEC", 185), # False Ceiling / SqFt + ("SVC-WAR-LAM", 950), # Laminate Wardrobe / SqFt + ("SVC-KIT-EXEC", 1100), # Modular Kitchen / SqFt + ("SVC-FLOOR", 120), # Flooring / SqFt + ("SVC-LUMP", 0), # Lumpsum — rate set per project + ("SVC-DESIGN", 15000), # Design consultation / Nos + ("SVC-CONVEY", 2000), # Conveyance / trip + ("SVC-LABOUR", 700), # Labour / day + ("SVC-ELECTRIC", 0), # Electrical — per quote + ("SVC-PAINT", 35), # Painting / SqFt ] price_lists = ["Standard Selling", pl_name] @@ -425,17 +499,23 @@ def create_price_list(): for code, rate in item_prices: if not exists("Item", code): continue - if not exists_f("Item Price", {"item_code": code, "price_list": pl, "selling": 1}): - ip = frappe.get_doc({ - "doctype": "Item Price", - "item_code": code, - "price_list": pl, - "selling": 1, - "currency": "INR", - "price_list_rate": rate, - }) - if safe_insert(ip): ok(f"Item Price: {code} ₹{rate} [{pl}]") - else: skip(f"Item Price: {code} [{pl}] (dup)") + if not exists_f( + "Item Price", {"item_code": code, "price_list": pl, "selling": 1} + ): + ip = frappe.get_doc( + { + "doctype": "Item Price", + "item_code": code, + "price_list": pl, + "selling": 1, + "currency": "INR", + "price_list_rate": rate, + } + ) + if safe_insert(ip): + ok(f"Item Price: {code} ₹{rate} [{pl}]") + else: + skip(f"Item Price: {code} [{pl}] (dup)") else: skip(f"Item Price: {code} [{pl}]") @@ -449,81 +529,240 @@ def create_crm_billing_fields(): print("\n[9] CRM & Billing Custom Fields...") fields = [ - # ── CUSTOMER ───────────────────────────────────────────── - ("Customer", "whatsapp_number", "WhatsApp Number", - "Data", None, "mobile_no", 0), - ("Customer", "property_address", "Property / Site Address", - "Small Text", None, "whatsapp_number", 0), - ("Customer", "project_type", "Project Type", - "Select", "Residential\nCommercial\nHospitality\nRetail", "property_address", 0), - ("Customer", "property_size_sqft", "Approx. Area (SqFt)", - "Float", None, "project_type", 0), - ("Customer", "budget_range", "Budget Range", - "Select", - "Under ₹5 Lakhs\n₹5–10 Lakhs\n₹10–25 Lakhs\n₹25–50 Lakhs\nAbove ₹50 Lakhs", - "property_size_sqft", 0), - ("Customer", "referred_by", "Referred By", - "Data", None, "budget_range", 0), - + ( + "Customer", + "whatsapp_number", + "WhatsApp Number", + "Data", + None, + "mobile_no", + 0, + ), + ( + "Customer", + "property_address", + "Property / Site Address", + "Small Text", + None, + "whatsapp_number", + 0, + ), + ( + "Customer", + "project_type", + "Project Type", + "Select", + "Residential\nCommercial\nHospitality\nRetail", + "property_address", + 0, + ), + ( + "Customer", + "property_size_sqft", + "Approx. Area (SqFt)", + "Float", + None, + "project_type", + 0, + ), + ( + "Customer", + "budget_range", + "Budget Range", + "Select", + "Under ₹5 Lakhs\n₹5–10 Lakhs\n₹10–25 Lakhs\n₹25–50 Lakhs\nAbove ₹50 Lakhs", + "property_size_sqft", + 0, + ), + ("Customer", "referred_by", "Referred By", "Data", None, "budget_range", 0), # ── LEAD ───────────────────────────────────────────────── - ("Lead", "whatsapp_number", "WhatsApp Number", - "Data", None, "mobile_no", 0), - ("Lead", "project_type_lead", "Project Type", - "Select", "Residential\nCommercial\nHospitality\nRetail", - "whatsapp_number", 0), - ("Lead", "property_address", "Property / Site Address", - "Small Text", None, "project_type_lead", 0), - ("Lead", "area_sqft", "Approx. Area (SqFt)", - "Float", None, "property_address", 0), - ("Lead", "budget_range", "Budget Range", - "Select", - "Under ₹5 Lakhs\n₹5–10 Lakhs\n₹10–25 Lakhs\n₹25–50 Lakhs\nAbove ₹50 Lakhs", - "area_sqft", 0), - ("Lead", "site_visit_done", "Site Visit Done", - "Check", None, "budget_range", 0), - ("Lead", "site_visit_date", "Site Visit Date", - "Date", None, "site_visit_done", 0), - ("Lead", "estimated_value", "Estimated Project Value (₹)", - "Currency", None, "site_visit_date", 0), - + ("Lead", "whatsapp_number", "WhatsApp Number", "Data", None, "mobile_no", 0), + ( + "Lead", + "project_type_lead", + "Project Type", + "Select", + "Residential\nCommercial\nHospitality\nRetail", + "whatsapp_number", + 0, + ), + ( + "Lead", + "property_address", + "Property / Site Address", + "Small Text", + None, + "project_type_lead", + 0, + ), + ( + "Lead", + "area_sqft", + "Approx. Area (SqFt)", + "Float", + None, + "property_address", + 0, + ), + ( + "Lead", + "budget_range", + "Budget Range", + "Select", + "Under ₹5 Lakhs\n₹5–10 Lakhs\n₹10–25 Lakhs\n₹25–50 Lakhs\nAbove ₹50 Lakhs", + "area_sqft", + 0, + ), + ( + "Lead", + "site_visit_done", + "Site Visit Done", + "Check", + None, + "budget_range", + 0, + ), + ( + "Lead", + "site_visit_date", + "Site Visit Date", + "Date", + None, + "site_visit_done", + 0, + ), + ( + "Lead", + "estimated_value", + "Estimated Project Value (₹)", + "Currency", + None, + "site_visit_date", + 0, + ), # ── QUOTATION ──────────────────────────────────────────── - ("Quotation", "site_address", "Site / Delivery Address", - "Small Text", None, "customer_address", 0), - ("Quotation", "scope_of_work", "Scope of Work", - "Small Text", None, "site_address", 0), - ("Quotation", "site_visit_date", "Site Visit Date", - "Date", None, "scope_of_work", 0), - ("Quotation", "expected_start_date_q", "Expected Start Date", - "Date", None, "site_visit_date", 0), - ("Quotation", "expected_handover_date","Expected Handover Date", - "Date", None, "expected_start_date_q", 0), - ("Quotation", "design_style", "Design Style", - "Select", - "Modern / Contemporary\nTraditional / Classic\nMinimalist\nIndustrial\nBohemian\nMediterranean", - "expected_handover_date", 0), - + ( + "Quotation", + "site_address", + "Site / Delivery Address", + "Small Text", + None, + "customer_address", + 0, + ), + ( + "Quotation", + "scope_of_work", + "Scope of Work", + "Small Text", + None, + "site_address", + 0, + ), + ( + "Quotation", + "site_visit_date", + "Site Visit Date", + "Date", + None, + "scope_of_work", + 0, + ), + ( + "Quotation", + "expected_start_date_q", + "Expected Start Date", + "Date", + None, + "site_visit_date", + 0, + ), + ( + "Quotation", + "expected_handover_date", + "Expected Handover Date", + "Date", + None, + "expected_start_date_q", + 0, + ), + ( + "Quotation", + "design_style", + "Design Style", + "Select", + "Modern / Contemporary\nTraditional / Classic\nMinimalist\nIndustrial\nBohemian\nMediterranean", + "expected_handover_date", + 0, + ), # ── SALES INVOICE ──────────────────────────────────────── # 'project' field is native on Sales Invoice in v16 — no custom field needed. # Adding billing-specific extras after 'customer_name' instead. - ("Sales Invoice", "milestone_description", "Milestone Description", - "Small Text", None, "customer_name", 0), - ("Sales Invoice", "payment_mode_note", "Payment Mode Note", - "Data", None, "milestone_description", 0), - + ( + "Sales Invoice", + "milestone_description", + "Milestone Description", + "Small Text", + None, + "customer_name", + 0, + ), + ( + "Sales Invoice", + "payment_mode_note", + "Payment Mode Note", + "Data", + None, + "milestone_description", + 0, + ), # ── OPPORTUNITY ────────────────────────────────────────── - ("Opportunity", "property_address", "Property / Site Address", - "Small Text", None, "customer_name", 0), - ("Opportunity", "area_sqft", "Area (SqFt)", - "Float", None, "property_address", 0), - ("Opportunity", "design_style", "Design Style", - "Select", - "Modern / Contemporary\nTraditional / Classic\nMinimalist\nIndustrial\nBohemian\nMediterranean", - "area_sqft", 0), - ("Opportunity", "site_visit_done", "Site Visit Done", - "Check", None, "design_style", 0), - ("Opportunity", "site_visit_date", "Site Visit Date", - "Date", None, "site_visit_done", 0), + ( + "Opportunity", + "property_address", + "Property / Site Address", + "Small Text", + None, + "customer_name", + 0, + ), + ( + "Opportunity", + "area_sqft", + "Area (SqFt)", + "Float", + None, + "property_address", + 0, + ), + ( + "Opportunity", + "design_style", + "Design Style", + "Select", + "Modern / Contemporary\nTraditional / Classic\nMinimalist\nIndustrial\nBohemian\nMediterranean", + "area_sqft", + 0, + ), + ( + "Opportunity", + "site_visit_done", + "Site Visit Done", + "Check", + None, + "design_style", + 0, + ), + ( + "Opportunity", + "site_visit_date", + "Site Visit Date", + "Date", + None, + "site_visit_done", + 0, + ), ] for dt, fn, label, ft, opts, after, in_list in [ @@ -532,19 +771,21 @@ def create_crm_billing_fields(): cf_name = f"{dt}-{fn}" if not exists("Custom Field", cf_name): d_dict = { - "doctype": "Custom Field", - "dt": dt, - "fieldname": fn, - "label": label, - "fieldtype": ft, + "doctype": "Custom Field", + "dt": dt, + "fieldname": fn, + "label": label, + "fieldtype": ft, "insert_after": after, "in_list_view": in_list, } if opts: d_dict["options"] = opts d = frappe.get_doc(d_dict) - if safe_insert(d): ok(f"Custom Field: {dt}.{fn}") - else: skip(f"{dt}.{fn} (dup)") + if safe_insert(d): + ok(f"Custom Field: {dt}.{fn}") + else: + skip(f"{dt}.{fn} (dup)") else: skip(f"Custom Field: {dt}.{fn}") @@ -559,29 +800,31 @@ def create_sample_customers(): customers = [ { - "customer_name": "Sharma Residence - Salt Lake", + "customer_name": "Sharma Residence - Salt Lake", "customer_group": "Residential - Individual", - "territory": "Salt Lake", - "customer_type": "Individual", + "territory": "Salt Lake", + "customer_type": "Individual", }, { - "customer_name": "Mukherjee Apartment - Behala", + "customer_name": "Mukherjee Apartment - Behala", "customer_group": "Residential - Builder Flat", - "territory": "South Kolkata", - "customer_type": "Individual", + "territory": "South Kolkata", + "customer_type": "Individual", }, { - "customer_name": "Bansal Office - Park Street", + "customer_name": "Bansal Office - Park Street", "customer_group": "Commercial - Office", - "territory": "Kolkata", - "customer_type": "Company", + "territory": "Kolkata", + "customer_type": "Company", }, ] for c in customers: if not exists("Customer", c["customer_name"]): d = frappe.get_doc({"doctype": "Customer", **c}) - if safe_insert(d): ok(f"Customer: {c['customer_name']}") - else: skip(f"{c['customer_name']} (dup)") + if safe_insert(d): + ok(f"Customer: {c['customer_name']}") + else: + skip(f"{c['customer_name']} (dup)") else: skip(f"Customer: {c['customer_name']}") frappe.db.commit() @@ -594,11 +837,11 @@ def configure_selling_settings(): print("\n[11] Selling & CRM Settings...") try: s = frappe.get_single("Selling Settings") - s.cust_master_name = "Customer Name" - s.customer_group = "Residential - Individual" - s.territory = "Kolkata" - s.price_list = "Standard Selling" - s.selling_price_list = "Standard Selling" + s.cust_master_name = "Customer Name" + s.customer_group = "Residential - Individual" + s.territory = "Kolkata" + s.price_list = "Standard Selling" + s.selling_price_list = "Standard Selling" s.flags.ignore_permissions = True s.save() ok("Selling Settings updated") @@ -607,8 +850,8 @@ def configure_selling_settings(): try: b = frappe.get_single("Buying Settings") - b.supp_master_name = "Supplier Name" - b.supplier_group = "Local Market Vendor (Unregistered)" + b.supp_master_name = "Supplier Name" + b.supplier_group = "Local Market Vendor (Unregistered)" b.flags.ignore_permissions = True b.save() ok("Buying Settings updated") @@ -639,12 +882,16 @@ def create_crm_stages(): for stage in furnitex_stages: if not exists("Sales Stage", stage): try: - d = frappe.get_doc({ - "doctype": "Sales Stage", - "stage_name": stage, - }) - if safe_insert(d): ok(f"Sales Stage: {stage}") - else: skip(f"{stage} (dup)") + d = frappe.get_doc( + { + "doctype": "Sales Stage", + "stage_name": stage, + } + ) + if safe_insert(d): + ok(f"Sales Stage: {stage}") + else: + skip(f"{stage} (dup)") except Exception as e: skip(f"Sales Stage {stage}: {e}") else: @@ -663,19 +910,23 @@ def create_crm_stages(): "Wardrobe / Storage Only", "Flooring Only", ] - for ot in opp_types: - if not exists("Opportunity Type", ot): + for opp_type in opp_types: + if not exists("Opportunity Type", opp_type): try: - d = frappe.get_doc({ - "doctype": "Opportunity Type", - "name": ot, - }) - if safe_insert(d): ok(f"Opportunity Type: {ot}") - else: skip(f"{ot} (dup)") + d = frappe.get_doc( + { + "doctype": "Opportunity Type", + "name": opp_type, + } + ) + if safe_insert(d): + ok(f"Opportunity Type: {opp_type}") + else: + skip(f"{opp_type} (dup)") except Exception as e: - skip(f"Opportunity Type {ot}: {e}") + skip(f"Opportunity Type {opp_type}: {e}") else: - skip(f"Opportunity Type: {ot}") + skip(f"Opportunity Type: {opp_type}") frappe.db.commit() @@ -706,12 +957,13 @@ def create_letter_head(): print("\n[14] Letter Head...") lh_name = "Furnitex" if not exists("Letter Head", lh_name): - d = frappe.get_doc({ - "doctype": "Letter Head", - "letter_head_name": lh_name, - "is_default": 1, - "source": "Rich Text", - "content": """ + d = frappe.get_doc( + { + "doctype": "Letter Head", + "letter_head_name": lh_name, + "is_default": 1, + "source": "Rich Text", + "content": """
@@ -730,13 +982,16 @@ def create_letter_head():
""", - "footer": """ + "footer": """
Furnitex | Interior Design & Furniture | Kolkata | GSTIN: [Your GSTIN] | CIN: [If applicable]
""", - }) - if safe_insert(d): ok(f"Letter Head: {lh_name}") - else: skip(f"Letter Head {lh_name} (dup)") + } + ) + if safe_insert(d): + ok(f"Letter Head: {lh_name}") + else: + skip(f"Letter Head {lh_name} (dup)") else: skip(f"Letter Head: {lh_name}") frappe.db.commit() diff --git a/update_furnitex_info.py b/update_furnitex_info.py index 42937b92..44820139 100644 --- a/update_furnitex_info.py +++ b/update_furnitex_info.py @@ -8,20 +8,20 @@ import frappe COMPANY = "Furnitex" -ADDRESS = "6/A/108 Mukundapur" -CITY = "Kolkata" -STATE = "West Bengal" -PINCODE = "700099" -COUNTRY = "India" -PHONE = "+91 62905 91422" -EMAIL = "info.furnitex@gmail.com" -WEBSITE = "https://furnitex.co.in" -INSTAGRAM = "https://www.instagram.com/frunitex" -LEGAL_NAME = "Furnitex Atelier Pvt. Ltd." -TAGLINE = "Redefine What Surrounds You" -FULL_ADDR = f"{ADDRESS}, {CITY} - {PINCODE}, {STATE}, {COUNTRY}" -REP_NAME = "Subhankar Dhar" -OFFICE_HOURS = "Monday – Saturday, 10:00 AM – 7:00 PM IST" +ADDRESS = "6/A/108 Mukundapur" +CITY = "Kolkata" +STATE = "West Bengal" +PINCODE = "700099" +COUNTRY = "India" +PHONE = "+91 62905 91422" +EMAIL = "info.furnitex@gmail.com" +WEBSITE = "https://furnitex.co.in" +INSTAGRAM = "https://www.instagram.com/frunitex" +LEGAL_NAME = "Furnitex Atelier Pvt. Ltd." +TAGLINE = "Redefine What Surrounds You" +FULL_ADDR = f"{ADDRESS}, {CITY} - {PINCODE}, {STATE}, {COUNTRY}" +REP_NAME = "Subhankar Dhar" +OFFICE_HOURS = "Monday – Saturday, 10:00 AM – 7:00 PM IST" def run(): @@ -39,12 +39,13 @@ def run(): # ── 1. Company record ───────────────────────────────────────────────────────── + def update_company(): co = frappe.get_doc("Company", COMPANY) - co.phone_no = PHONE - co.email = EMAIL - co.website = WEBSITE - co.company_name = COMPANY # keep short name as primary + co.phone_no = PHONE + co.email = EMAIL + co.website = WEBSITE + co.company_name = COMPANY # keep short name as primary co.flags.ignore_permissions = True co.save() print(f" ✓ Company record updated: phone={PHONE}, email={EMAIL}") @@ -52,30 +53,26 @@ def update_company(): # ── 2. Address record ───────────────────────────────────────────────────────── + def update_address(): # Check if a Furnitex address already exists existing = frappe.db.get_value( - "Address", - {"address_title": COMPANY, "address_type": "Billing"}, - "name" + "Address", {"address_title": COMPANY, "address_type": "Billing"}, "name" ) addr_doc = { - "doctype": "Address", - "address_title": COMPANY, - "address_type": "Billing", - "address_line1": ADDRESS, - "city": CITY, - "state": STATE, - "pincode": PINCODE, - "country": COUNTRY, - "phone": PHONE, - "email_id": EMAIL, + "doctype": "Address", + "address_title": COMPANY, + "address_type": "Billing", + "address_line1": ADDRESS, + "city": CITY, + "state": STATE, + "pincode": PINCODE, + "country": COUNTRY, + "phone": PHONE, + "email_id": EMAIL, "is_primary_address": 1, - "links": [{ - "link_doctype": "Company", - "link_name": COMPANY - }] + "links": [{"link_doctype": "Company", "link_name": COMPANY}], } if existing: @@ -120,6 +117,7 @@ LETTER_HEAD_HTML = f""" """ + def update_letter_head(): lh_name = "Furnitex" if frappe.db.exists("Letter Head", lh_name): @@ -130,12 +128,14 @@ def update_letter_head(): doc.save() print(" ✓ Letter Head updated with real contact details") else: - doc = frappe.get_doc({ - "doctype": "Letter Head", - "letter_head_name": lh_name, - "content": LETTER_HEAD_HTML, - "is_default": 1, - }) + doc = frappe.get_doc( + { + "doctype": "Letter Head", + "letter_head_name": lh_name, + "content": LETTER_HEAD_HTML, + "is_default": 1, + } + ) doc.flags.ignore_permissions = True doc.insert() print(" ✓ Letter Head created") @@ -186,11 +186,12 @@ PO_TC = f"""
""" TC_MAP = { - "Furnitex - Quotation T&C": QUOTATION_TC, - "Furnitex - Invoice T&C": INVOICE_TC, + "Furnitex - Quotation T&C": QUOTATION_TC, + "Furnitex - Invoice T&C": INVOICE_TC, "Furnitex - Purchase Order T&C": PO_TC, } + def update_terms_conditions(): for title, content in TC_MAP.items(): if frappe.db.exists("Terms and Conditions", title): @@ -200,14 +201,16 @@ def update_terms_conditions(): doc.save() print(f" ✓ Updated T&C: {title}") else: - doc = frappe.get_doc({ - "doctype": "Terms and Conditions", - "title": title, - "terms": content, - "selling": 1, - "buying": 1, - "hr": 0, - }) + doc = frappe.get_doc( + { + "doctype": "Terms and Conditions", + "title": title, + "terms": content, + "selling": 1, + "buying": 1, + "hr": 0, + } + ) doc.flags.ignore_permissions = True doc.insert() print(f" ✓ Created T&C: {title}")