diff --git a/delete_streetwok.py b/delete_streetwok.py
new file mode 100644
index 00000000..1aaf43d5
--- /dev/null
+++ b/delete_streetwok.py
@@ -0,0 +1,237 @@
+"""
+delete_streetwok.py
+Removes all "streetwok (Demo)" company data from Furnitex ERPNext instance.
+Run via: bench --site frontend execute frappe.delete_streetwok.run
+"""
+
+import frappe
+
+
+COMPANY = "streetwok (Demo)"
+
+
+def _sql(q, commit=False):
+ frappe.db.sql(q)
+ if commit:
+ frappe.db.commit()
+
+
+def run():
+ frappe.set_user("Administrator")
+
+ if not frappe.db.exists("Company", COMPANY):
+ print(f" Company '{COMPANY}' not found — nothing to delete.")
+ return
+
+ print(f"\n{'='*54}")
+ print(f" DELETING: {COMPANY}")
+ print(f"{'='*54}\n")
+
+ # ── STEP 1: Cancel + delete all submitted documents ──────────
+ submitted_doctypes = [
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Payment Entry",
+ "Journal Entry",
+ "Stock Entry",
+ "Delivery Note",
+ "Purchase Receipt",
+ "Sales Order",
+ "Purchase Order",
+ "Quotation",
+ "Material Request",
+ "Stock Reconciliation",
+ ]
+
+ for dt in submitted_doctypes:
+ # 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
+ )
+ if docs:
+ print(f" Cancelling {len(docs)} submitted {dt}(s)...")
+ for d in docs:
+ try:
+ doc = frappe.get_doc(dt, d.name)
+ doc.flags.ignore_permissions = True
+ doc.flags.ignore_links = True
+ doc.cancel()
+ frappe.db.commit()
+ except Exception as e:
+ # Force cancel via direct DB update if normal cancel fails
+ frappe.db.sql(
+ f"UPDATE `tab{dt}` SET docstatus=2 WHERE name=%s", d.name
+ )
+ frappe.db.commit()
+
+ # ── STEP 2: Delete all draft + cancelled docs ─────────────────
+ all_doctypes = submitted_doctypes + [
+ "Landed Cost Voucher",
+ "Asset",
+ "Salary Slip",
+ "Timesheet",
+ ]
+
+ for dt in all_doctypes:
+ try:
+ count = frappe.db.count(dt, {"company": COMPANY})
+ if count:
+ frappe.db.delete(dt, {"company": COMPANY})
+ frappe.db.commit()
+ print(f" Deleted {count} {dt}(s)")
+ except Exception as e:
+ print(f" [WARN] Could not delete {dt}: {e}")
+
+ # ── STEP 3: Delete GL Entries ─────────────────────────────────
+ gl_count = frappe.db.count("GL Entry", {"company": COMPANY})
+ if gl_count:
+ frappe.db.delete("GL Entry", {"company": COMPANY})
+ frappe.db.commit()
+ print(f" Deleted {gl_count} GL Entries")
+
+ # ── STEP 4: Delete Stock Ledger Entries ───────────────────────
+ sle_count = frappe.db.count("Stock Ledger Entry", {"company": COMPANY})
+ if sle_count:
+ frappe.db.delete("Stock Ledger Entry", {"company": COMPANY})
+ frappe.db.commit()
+ print(f" Deleted {sle_count} Stock Ledger Entries")
+
+ # ── STEP 5: Delete child tables that reference the company ────
+ child_cleanups = [
+ ("Sales Invoice Item", "company"),
+ ("Purchase Invoice Item", "company"),
+ ("Payment Entry Reference", None), # handled via parent delete
+ ]
+
+ # ── STEP 6: Delete Warehouses ─────────────────────────────────
+ # Disable stock bins first
+ wh_list = frappe.db.sql(
+ "SELECT name FROM `tabWarehouse` WHERE company=%s", COMPANY, as_dict=1
+ )
+ if wh_list:
+ for wh in wh_list:
+ frappe.db.delete("Bin", {"warehouse": wh.name})
+ frappe.db.commit()
+
+ 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.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
+ )
+ if cc_list:
+ for cc in cc_list:
+ try:
+ frappe.db.sql(
+ "DELETE FROM `tabCost Center` WHERE name=%s", cc.name
+ )
+ except Exception:
+ pass
+ frappe.db.commit()
+ print(f" Deleted {len(cc_list)} Cost Center(s)")
+
+ # ── STEP 8: Delete Accounts ───────────────────────────────────
+ acct_count = frappe.db.count("Account", {"company": COMPANY})
+ if acct_count:
+ # Delete leaf accounts first (is_group=0), then groups
+ 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.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
+ )
+ for fy in fy_links:
+ frappe.db.sql(
+ "DELETE FROM `tabFiscal Year Company` WHERE company=%s AND parent=%s",
+ (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})
+ if remaining == 0:
+ try:
+ frappe.db.delete("Fiscal Year", {"name": fy.parent})
+ except Exception:
+ pass
+ frappe.db.commit()
+
+ # ── STEP 10: Nuke Customers/Suppliers that belong only to ─────
+ # streetwok (no transactions in Furnitex)
+ # Only delete if they have no Furnitex transactions
+ stale_customers = frappe.db.sql(
+ """SELECT c.name FROM `tabCustomer` c
+ WHERE NOT EXISTS (
+ SELECT 1 FROM `tabSales Invoice`
+ WHERE customer=c.name AND company='Furnitex' AND docstatus < 2
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM `tabSales Order`
+ WHERE customer=c.name AND company='Furnitex' AND docstatus < 2
+ )""", as_dict=1
+ )
+ if stale_customers:
+ for c in stale_customers:
+ try:
+ frappe.db.delete("Customer", {"name": c.name})
+ except Exception:
+ pass
+ frappe.db.commit()
+ print(f" Deleted {len(stale_customers)} orphan Customer(s)")
+
+ # ── STEP 11: Delete the Company record itself ─────────────────
+ try:
+ 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()
+ print(f"\n Company '{COMPANY}' deleted.")
+
+ # ── STEP 12: Update Default Company if it was streetwok ───────
+ default_co = frappe.db.get_default("company")
+ if default_co and "streetwok" in default_co.lower():
+ frappe.db.set_default("company", "Furnitex")
+ frappe.db.commit()
+ print(" Default company reset to: Furnitex")
+
+ # ── STEP 13: Nuke any leftover streetwok items ────────────────
+ stale_items = frappe.db.sql(
+ """SELECT name FROM `tabItem`
+ WHERE item_name LIKE '%streetwok%'
+ OR item_code LIKE '%streetwok%'
+ OR description LIKE '%streetwok%'""", as_dict=1
+ )
+ if stale_items:
+ for item in stale_items:
+ try:
+ frappe.db.delete("Item", {"name": item.name})
+ except Exception:
+ pass
+ frappe.db.commit()
+ print(f" Deleted {len(stale_items)} streetwok-tagged Item(s)")
+
+ frappe.clear_cache()
+
+ print(f"\n{'='*54}")
+ print(" DONE — streetwok removed. Refresh your browser.")
+ print(f"{'='*54}\n")
diff --git a/setup_furnitex_crm_billing.py b/setup_furnitex_crm_billing.py
new file mode 100644
index 00000000..62720f40
--- /dev/null
+++ b/setup_furnitex_crm_billing.py
@@ -0,0 +1,774 @@
+"""
+setup_furnitex_crm_billing.py
+Full CRM + Billing setup for Furnitex (ERPNext v16)
+Run: bench --site frontend execute frappe.setup_furnitex_crm_billing.run
+"""
+
+import frappe
+
+COMPANY = "Furnitex"
+
+
+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.insert()
+ return True
+ except frappe.DuplicateEntryError:
+ return False
+ except Exception as e:
+ if "Duplicate entry" in str(e):
+ 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")
+
+
+# ──────────────────────────────────────────────────────────────
+# 1. CUSTOMER GROUPS (Interior-specific)
+# ──────────────────────────────────────────────────────────────
+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"),
+ ]
+ 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)")
+ else:
+ skip(f"Customer Group: {name}")
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 2. TERRITORIES (Kolkata geography)
+# ──────────────────────────────────────────────────────────────
+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"),
+ ]
+ 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)")
+ else:
+ skip(f"Territory: {name}")
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 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",
+])
+
+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")]:
+ 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)")
+ else:
+ skip(f"Lead Source field exists on {dt}")
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 4. SALES PERSONS
+# ──────────────────────────────────────────────────────────────
+def create_sales_persons():
+ print("\n[4] Sales Persons...")
+ persons = [
+ ("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)")
+ else:
+ skip(f"Sales Person: {name}")
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 5. MODE OF PAYMENT (Furnitex extras)
+# ──────────────────────────────────────────────────────────────
+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"),
+ ]
+
+ # 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")
+
+ for name, mtype in modes:
+ if not exists("Mode of Payment", name):
+ d_dict = {
+ "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 = frappe.get_doc(d_dict)
+ 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()
+
+
+# ──────────────────────────────────────────────────────────────
+# 6. PAYMENT TERMS TEMPLATES
+# ──────────────────────────────────────────────────────────────
+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"},
+ ],
+ },
+
+ # ── 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"},
+ ],
+ },
+
+ # ── 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"},
+ ],
+ },
+
+ # ── 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"},
+ ],
+ },
+ ]
+
+ for t in templates:
+ tname = t["name"]
+ if not exists_f("Payment Terms Template", {"template_name": tname}):
+ # Ensure Payment Term records exist for each row
+ term_rows = []
+ 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"],
+ })
+ 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"],
+ })
+
+ 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}")
+
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 7. TERMS AND CONDITIONS TEMPLATES
+# ──────────────────────────────────────────────────────────────
+def create_terms_conditions():
+ print("\n[7] Terms & Conditions Templates...")
+
+ templates = [
+
+ # ── Quotation T&C ─────────────────────────────────────────
+ {
+ "title": "Furnitex - Quotation Terms",
+ "terms": """
Terms & Conditions — Furnitex Interior
+
+- Validity: This quotation is valid for 15 days from the date of issue.
+- Scope: Only items listed in this quotation are included. Any additional work will be quoted separately.
+- Payment Schedule: 30% advance on confirmation, 30% at 50% completion, 30% at completion, 10% on handover.
+- Timeline: Work commences within 7 working days of advance payment and site clearance. Timeline communicated separately.
+- Material: All materials as specified. Substitution only with client approval.
+- Civil Work: Civil, electrical, plumbing work by client unless specifically included above.
+- Site Access: Client to ensure unobstructed site access during working hours (9 AM – 6 PM).
+- Warranty: 1-year workmanship warranty. Hardware warranty as per manufacturer.
+- Disputes: Subject to Kolkata jurisdiction.
+
+Furnitex | Interior Design & Furniture | Kolkata
""",
+ },
+
+ # ── Sales Invoice T&C ─────────────────────────────────────
+ {
+ "title": "Furnitex - Invoice Terms",
+ "terms": """Invoice Terms — Furnitex Interior
+
+- Payment Due: As per agreed payment schedule. Late payment attracts 2% per month interest.
+- Goods: Materials remain property of Furnitex until full payment is received.
+- Defects: Any defects must be reported within 7 days of delivery/installation.
+- Warranty: 1-year warranty on workmanship from date of handover. Excludes wear, misuse, and civil damage.
+- Disputes: Subject to Kolkata jurisdiction only.
+
+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",
+ "terms": """Purchase Order Terms — Furnitex
+
+- Delivery as per schedule agreed. Delays will be notified promptly.
+- Materials must match specifications. Furnitex reserves the right to reject substandard material.
+- Invoice must reference this PO number.
+- Furnitex is not liable for GST on unregistered supplier purchases.
+
""",
+ },
+ ]
+
+ 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)")
+ else:
+ skip(f"T&C: {t['title']}")
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 8. PRICE LIST — Furnitex Interior Rate Card
+# ──────────────────────────────────────────────────────────────
+def create_price_list():
+ print("\n[8] Price Lists & Item Prices...")
+
+ 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)")
+ else:
+ skip(f"Price List: {pl_name}")
+
+ frappe.db.commit()
+
+ # ── 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
+ ]
+
+ price_lists = ["Standard Selling", pl_name]
+ for pl in price_lists:
+ if not exists("Price List", pl):
+ continue
+ 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)")
+ else:
+ skip(f"Item Price: {code} [{pl}]")
+
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 9. CUSTOM FIELDS — CRM & BILLING
+# ──────────────────────────────────────────────────────────────
+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),
+
+ # ── 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),
+
+ # ── 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),
+
+ # ── 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),
+
+ # ── 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),
+ ]
+
+ for dt, fn, label, ft, opts, after, in_list in [
+ (*f, 0) if len(f) == 6 else f for f 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,
+ }
+ 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)")
+ else:
+ skip(f"Custom Field: {dt}.{fn}")
+
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 10. SAMPLE CUSTOMERS (Kolkata residential)
+# ──────────────────────────────────────────────────────────────
+def create_sample_customers():
+ print("\n[10] Sample Customers...")
+
+ customers = [
+ {
+ "customer_name": "Sharma Residence - Salt Lake",
+ "customer_group": "Residential - Individual",
+ "territory": "Salt Lake",
+ "customer_type": "Individual",
+ },
+ {
+ "customer_name": "Mukherjee Apartment - Behala",
+ "customer_group": "Residential - Builder Flat",
+ "territory": "South Kolkata",
+ "customer_type": "Individual",
+ },
+ {
+ "customer_name": "Bansal Office - Park Street",
+ "customer_group": "Commercial - Office",
+ "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)")
+ else:
+ skip(f"Customer: {c['customer_name']}")
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 11. SELLING SETTINGS — set Furnitex defaults
+# ──────────────────────────────────────────────────────────────
+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.flags.ignore_permissions = True
+ s.save()
+ ok("Selling Settings updated")
+ except Exception as e:
+ skip(f"Selling Settings: {e}")
+
+ try:
+ b = frappe.get_single("Buying Settings")
+ b.supp_master_name = "Supplier Name"
+ b.supplier_group = "Local Market Vendor (Unregistered)"
+ b.flags.ignore_permissions = True
+ b.save()
+ ok("Buying Settings updated")
+ except Exception as e:
+ skip(f"Buying Settings: {e}")
+
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 12. CRM PIPELINE — Sales Stages + Opportunity Types
+# ──────────────────────────────────────────────────────────────
+def create_crm_stages():
+ print("\n[12] CRM Pipeline (Sales Stages + Opportunity Types)...")
+
+ # ── Sales Stages ─────────────────────────────────────────
+ furnitex_stages = [
+ "New Enquiry",
+ "Site Visit Scheduled",
+ "Site Visit Done",
+ "Design Presentation",
+ "Quotation Sent",
+ "Negotiation",
+ "PO / Work Order Received",
+ "Work In Progress",
+ "Handover Done",
+ ]
+ 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)")
+ except Exception as e:
+ skip(f"Sales Stage {stage}: {e}")
+ else:
+ skip(f"Sales Stage: {stage}")
+
+ # ── Opportunity Types ─────────────────────────────────────
+ opp_types = [
+ "Interior Design - Full Home",
+ "Interior Design - Bedroom",
+ "Interior Design - Kitchen",
+ "Interior Design - Office",
+ "Interior Design - Restaurant",
+ "Loose Furniture Supply",
+ "Renovation / Refurbishment",
+ "False Ceiling Only",
+ "Wardrobe / Storage Only",
+ "Flooring Only",
+ ]
+ for ot in opp_types:
+ if not exists("Opportunity Type", ot):
+ try:
+ d = frappe.get_doc({
+ "doctype": "Opportunity Type",
+ "name": ot,
+ })
+ if safe_insert(d): ok(f"Opportunity Type: {ot}")
+ else: skip(f"{ot} (dup)")
+ except Exception as e:
+ skip(f"Opportunity Type {ot}: {e}")
+ else:
+ skip(f"Opportunity Type: {ot}")
+
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 13. PRINT FORMAT — Quotation (set default T&C)
+# ──────────────────────────────────────────────────────────────
+def configure_print_defaults():
+ print("\n[13] Setting default T&C on Quotation & Sales Invoice...")
+
+ # Set default terms on Quotation doctype
+ try:
+ meta = frappe.get_meta("Quotation")
+ default_tc = "Furnitex - Quotation Terms"
+ if exists("Terms and Conditions", default_tc):
+ frappe.db.set_default("terms_and_conditions_quotation", default_tc)
+ ok(f"Default T&C for Quotation: {default_tc}")
+ except Exception as e:
+ skip(f"Quotation T&C default: {e}")
+
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# 14. LETTER HEAD
+# ──────────────────────────────────────────────────────────────
+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": """
+
+
+
+
+ FURNITEX
+
+ INTERIOR DESIGN & FURNITURE MANUFACTURING
+
+ |
+
+ Kolkata, West Bengal — India
+ 📞 +91 XXXXX XXXXX
+ ✉ info@furnitex.in
+ 🌐 www.furnitex.in
+ |
+
+
+
""",
+ "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)")
+ else:
+ skip(f"Letter Head: {lh_name}")
+ frappe.db.commit()
+
+
+# ──────────────────────────────────────────────────────────────
+# MASTER RUNNER
+# ──────────────────────────────────────────────────────────────
+def run():
+ frappe.set_user("Administrator")
+
+ print("\n" + "=" * 58)
+ print(" FURNITEX — CRM + BILLING SETUP")
+ print("=" * 58)
+
+ create_customer_groups()
+ create_territories()
+ create_lead_sources()
+ create_sales_persons()
+ create_payment_modes()
+ create_payment_terms()
+ create_terms_conditions()
+ create_price_list()
+ create_crm_billing_fields()
+ create_sample_customers()
+ configure_selling_settings()
+ create_crm_stages()
+ configure_print_defaults()
+ create_letter_head()
+
+ frappe.clear_cache()
+
+ print("\n" + "=" * 58)
+ print(" CRM + BILLING SETUP COMPLETE — refresh your browser")
+ print("=" * 58 + "\n")
diff --git a/update_furnitex_info.py b/update_furnitex_info.py
new file mode 100644
index 00000000..42937b92
--- /dev/null
+++ b/update_furnitex_info.py
@@ -0,0 +1,213 @@
+"""
+update_furnitex_info.py
+Updates ERPNext with real Furnitex business details from furnitex.co.in
+Run via: bench --site frontend execute frappe.update_furnitex_info.run
+"""
+
+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"
+
+
+def run():
+ frappe.set_user("Administrator")
+
+ update_company()
+ update_address()
+ update_letter_head()
+ update_terms_conditions()
+
+ frappe.db.commit()
+ frappe.clear_cache()
+ print("\n✓ Furnitex business info updated successfully.\n")
+
+
+# ── 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.flags.ignore_permissions = True
+ co.save()
+ print(f" ✓ Company record updated: phone={PHONE}, email={EMAIL}")
+
+
+# ── 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"
+ )
+
+ 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,
+ "is_primary_address": 1,
+ "links": [{
+ "link_doctype": "Company",
+ "link_name": COMPANY
+ }]
+ }
+
+ if existing:
+ doc = frappe.get_doc("Address", existing)
+ doc.update(addr_doc)
+ doc.flags.ignore_permissions = True
+ doc.save()
+ print(f" ✓ Address updated: {FULL_ADDR}")
+ else:
+ doc = frappe.get_doc(addr_doc)
+ doc.flags.ignore_permissions = True
+ doc.insert()
+ print(f" ✓ Address created: {FULL_ADDR}")
+
+
+# ── 3. Letter Head ────────────────────────────────────────────────────────────
+
+LETTER_HEAD_HTML = f"""
+
+
+
+ |
+
+ FURNITEX
+
+
+ {TAGLINE}
+
+
+ {LEGAL_NAME}
+
+ |
+
+ {ADDRESS}
+ {CITY} - {PINCODE}, {STATE}
+ Phone: {PHONE}
+ Email: {EMAIL}
+ Web: furnitex.co.in
+ |
+
+
+
+"""
+
+def update_letter_head():
+ lh_name = "Furnitex"
+ if frappe.db.exists("Letter Head", lh_name):
+ doc = frappe.get_doc("Letter Head", lh_name)
+ doc.content = LETTER_HEAD_HTML
+ doc.is_default = 1
+ doc.flags.ignore_permissions = True
+ 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.flags.ignore_permissions = True
+ doc.insert()
+ print(" ✓ Letter Head created")
+
+
+# ── 4. Terms & Conditions ─────────────────────────────────────────────────────
+
+QUOTATION_TC = f"""
+FURNITEX — Quotation Terms & Conditions
+
+1. Validity: This quotation is valid for 15 days from the date of issue.
+2. Payment Terms: 30% advance on order confirmation, 30% at 50% completion, 30% at completion, 10% at handover.
+3. Delivery: Timelines are estimated and subject to site readiness and material availability. Furnitex is not liable for delays caused by site conditions.
+4. Design Changes: Any changes post order confirmation may attract additional charges and revised timelines.
+5. Material: All materials as specified. Substitutions may be made with equivalent or superior alternatives with prior intimation.
+6. Site Access: Client to ensure uninterrupted site access during agreed working hours: {OFFICE_HOURS}.
+7. Warranty: 1-year manufacturing warranty on all Furnitex-fabricated items. Hardware and third-party products carry manufacturer warranty.
+8. Dispute Resolution: All disputes subject to Kolkata jurisdiction.
+
+Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}
+
"""
+
+INVOICE_TC = f"""
+FURNITEX — Invoice Terms & Conditions
+
+1. Payment Due: Payment is due within 7 days of invoice date unless otherwise agreed in writing.
+2. Late Payment: Overdue amounts attract interest at 18% per annum.
+3. GST: GST as applicable under Indian law is charged additionally where mentioned.
+4. Delivery & Installation: Goods remain property of Furnitex until full payment is received.
+5. Returns: Custom-manufactured furniture is non-returnable. Defects to be reported within 48 hours of delivery.
+6. Warranty: 1-year manufacturing defect warranty. Normal wear, misuse, or site-caused damage not covered.
+7. Disputes: Subject to Kolkata jurisdiction.
+
+Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}
+
"""
+
+PO_TC = f"""
+FURNITEX — Purchase Order Terms & Conditions
+
+1. Acceptance: Supply of goods/services against this PO constitutes acceptance of these terms.
+2. Quality: All materials must conform to the specifications mentioned. Substandard materials will be rejected at supplier's cost.
+3. Delivery: Deliver to the address specified on the PO by the agreed date. Delays must be communicated 48 hours in advance.
+4. Invoice: Raise GST-compliant invoice (or cash memo for URD) with PO reference. Payment processed within 7 days of invoice receipt and material acceptance.
+5. Warranty: Supplier warrants materials against defects for minimum 6 months from delivery.
+6. Jurisdiction: Kolkata courts have exclusive jurisdiction.
+
+Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}
+
"""
+
+TC_MAP = {
+ "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):
+ doc = frappe.get_doc("Terms and Conditions", title)
+ doc.terms = content
+ doc.flags.ignore_permissions = True
+ 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.flags.ignore_permissions = True
+ doc.insert()
+ print(f" ✓ Created T&C: {title}")