mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-17 13:55:08 +00:00
feat(furnitex): add CRM/billing setup, company cleanup, and business info scripts
- setup_furnitex_crm_billing.py: creates customer groups, territories, lead sources (custom Select field), sales persons, payment modes, payment terms (4 Furnitex-specific templates), T&C templates, price list, CRM custom fields, sample customers, selling settings, sales stages, opportunity types, and letter head - delete_streetwok.py: removes all streetwok (Demo) company data (invoices, GL entries, warehouses, accounts, cost centres, customers) and resets default company to Furnitex - update_furnitex_info.py: updates ERPNext with real business details scraped from furnitex.co.in — company phone/email, billing address, branded letter head, and Quotation/Invoice/PO T&C templates with legal contact footer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a186cd057b
commit
7c52d9b84b
3 changed files with 1224 additions and 0 deletions
237
delete_streetwok.py
Normal file
237
delete_streetwok.py
Normal file
|
|
@ -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")
|
||||||
774
setup_furnitex_crm_billing.py
Normal file
774
setup_furnitex_crm_billing.py
Normal file
|
|
@ -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": """<h3>Terms & Conditions — Furnitex Interior</h3>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Validity:</strong> This quotation is valid for <strong>15 days</strong> from the date of issue.</li>
|
||||||
|
<li><strong>Scope:</strong> Only items listed in this quotation are included. Any additional work will be quoted separately.</li>
|
||||||
|
<li><strong>Payment Schedule:</strong> 30% advance on confirmation, 30% at 50% completion, 30% at completion, 10% on handover.</li>
|
||||||
|
<li><strong>Timeline:</strong> Work commences within 7 working days of advance payment and site clearance. Timeline communicated separately.</li>
|
||||||
|
<li><strong>Material:</strong> All materials as specified. Substitution only with client approval.</li>
|
||||||
|
<li><strong>Civil Work:</strong> Civil, electrical, plumbing work by client unless specifically included above.</li>
|
||||||
|
<li><strong>Site Access:</strong> Client to ensure unobstructed site access during working hours (9 AM – 6 PM).</li>
|
||||||
|
<li><strong>Warranty:</strong> 1-year workmanship warranty. Hardware warranty as per manufacturer.</li>
|
||||||
|
<li><strong>Disputes:</strong> Subject to Kolkata jurisdiction.</li>
|
||||||
|
</ol>
|
||||||
|
<p><em>Furnitex | Interior Design & Furniture | Kolkata</em></p>""",
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Sales Invoice T&C ─────────────────────────────────────
|
||||||
|
{
|
||||||
|
"title": "Furnitex - Invoice Terms",
|
||||||
|
"terms": """<h3>Invoice Terms — Furnitex Interior</h3>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Payment Due:</strong> As per agreed payment schedule. Late payment attracts 2% per month interest.</li>
|
||||||
|
<li><strong>Goods:</strong> Materials remain property of Furnitex until full payment is received.</li>
|
||||||
|
<li><strong>Defects:</strong> Any defects must be reported within 7 days of delivery/installation.</li>
|
||||||
|
<li><strong>Warranty:</strong> 1-year warranty on workmanship from date of handover. Excludes wear, misuse, and civil damage.</li>
|
||||||
|
<li><strong>Disputes:</strong> Subject to Kolkata jurisdiction only.</li>
|
||||||
|
</ol>
|
||||||
|
<p>Bank: [Your Bank] | A/c: [Account No] | IFSC: [IFSC] | UPI: [UPI ID]</p>
|
||||||
|
<p><em>Thank you for choosing Furnitex!</em></p>""",
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Purchase Order T&C ────────────────────────────────────
|
||||||
|
{
|
||||||
|
"title": "Furnitex - Purchase Order Terms",
|
||||||
|
"terms": """<h3>Purchase Order Terms — Furnitex</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Delivery as per schedule agreed. Delays will be notified promptly.</li>
|
||||||
|
<li>Materials must match specifications. Furnitex reserves the right to reject substandard material.</li>
|
||||||
|
<li>Invoice must reference this PO number.</li>
|
||||||
|
<li>Furnitex is not liable for GST on unregistered supplier purchases.</li>
|
||||||
|
</ol>""",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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": """
|
||||||
|
<div style="font-family: 'Segoe UI', Arial, sans-serif; border-bottom: 3px solid #2c3e50; padding-bottom: 12px; margin-bottom: 8px;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h1 style="margin:0; font-size:28px; color:#2c3e50; letter-spacing:2px;">FURNITEX</h1>
|
||||||
|
<p style="margin:2px 0 0; font-size:12px; color:#7f8c8d; letter-spacing:1px;">
|
||||||
|
INTERIOR DESIGN & FURNITURE MANUFACTURING
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td style="text-align:right; font-size:11px; color:#555; line-height:1.6;">
|
||||||
|
Kolkata, West Bengal — India<br>
|
||||||
|
📞 +91 XXXXX XXXXX<br>
|
||||||
|
✉ info@furnitex.in<br>
|
||||||
|
🌐 www.furnitex.in
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>""",
|
||||||
|
"footer": """
|
||||||
|
<div style="font-family: Arial, sans-serif; border-top: 1px solid #ccc; padding-top: 6px; font-size: 10px; color: #888; text-align: center;">
|
||||||
|
Furnitex | Interior Design & Furniture | Kolkata | GSTIN: [Your GSTIN] | CIN: [If applicable]
|
||||||
|
</div>""",
|
||||||
|
})
|
||||||
|
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")
|
||||||
213
update_furnitex_info.py
Normal file
213
update_furnitex_info.py
Normal file
|
|
@ -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"""
|
||||||
|
<div style="font-family:'Segoe UI',Arial,sans-serif; padding:0; margin:0;">
|
||||||
|
<table width="100%" style="border-bottom:2px solid #1a1a1a; padding-bottom:14px; margin-bottom:8px;">
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align:top; width:60%;">
|
||||||
|
<div style="font-size:26px; font-weight:700; letter-spacing:3px; color:#1a1a1a; text-transform:uppercase;">
|
||||||
|
FURNITEX
|
||||||
|
</div>
|
||||||
|
<div style="font-size:10px; letter-spacing:2px; color:#555; text-transform:uppercase; margin-top:2px;">
|
||||||
|
{TAGLINE}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:9px; color:#888; margin-top:4px;">
|
||||||
|
{LEGAL_NAME}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="vertical-align:top; text-align:right; width:40%; font-size:9.5px; color:#444; line-height:1.7;">
|
||||||
|
<div>{ADDRESS}</div>
|
||||||
|
<div>{CITY} - {PINCODE}, {STATE}</div>
|
||||||
|
<div>Phone: {PHONE}</div>
|
||||||
|
<div>Email: {EMAIL}</div>
|
||||||
|
<div>Web: furnitex.co.in</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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"""<div style="font-size:10.5px; line-height:1.8; color:#333;">
|
||||||
|
<strong>FURNITEX — Quotation Terms & Conditions</strong><br><br>
|
||||||
|
|
||||||
|
1. <strong>Validity:</strong> This quotation is valid for 15 days from the date of issue.<br>
|
||||||
|
2. <strong>Payment Terms:</strong> 30% advance on order confirmation, 30% at 50% completion, 30% at completion, 10% at handover.<br>
|
||||||
|
3. <strong>Delivery:</strong> Timelines are estimated and subject to site readiness and material availability. Furnitex is not liable for delays caused by site conditions.<br>
|
||||||
|
4. <strong>Design Changes:</strong> Any changes post order confirmation may attract additional charges and revised timelines.<br>
|
||||||
|
5. <strong>Material:</strong> All materials as specified. Substitutions may be made with equivalent or superior alternatives with prior intimation.<br>
|
||||||
|
6. <strong>Site Access:</strong> Client to ensure uninterrupted site access during agreed working hours: {OFFICE_HOURS}.<br>
|
||||||
|
7. <strong>Warranty:</strong> 1-year manufacturing warranty on all Furnitex-fabricated items. Hardware and third-party products carry manufacturer warranty.<br>
|
||||||
|
8. <strong>Dispute Resolution:</strong> All disputes subject to Kolkata jurisdiction.<br><br>
|
||||||
|
|
||||||
|
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
|
||||||
|
</div>"""
|
||||||
|
|
||||||
|
INVOICE_TC = f"""<div style="font-size:10.5px; line-height:1.8; color:#333;">
|
||||||
|
<strong>FURNITEX — Invoice Terms & Conditions</strong><br><br>
|
||||||
|
|
||||||
|
1. <strong>Payment Due:</strong> Payment is due within 7 days of invoice date unless otherwise agreed in writing.<br>
|
||||||
|
2. <strong>Late Payment:</strong> Overdue amounts attract interest at 18% per annum.<br>
|
||||||
|
3. <strong>GST:</strong> GST as applicable under Indian law is charged additionally where mentioned.<br>
|
||||||
|
4. <strong>Delivery & Installation:</strong> Goods remain property of Furnitex until full payment is received.<br>
|
||||||
|
5. <strong>Returns:</strong> Custom-manufactured furniture is non-returnable. Defects to be reported within 48 hours of delivery.<br>
|
||||||
|
6. <strong>Warranty:</strong> 1-year manufacturing defect warranty. Normal wear, misuse, or site-caused damage not covered.<br>
|
||||||
|
7. <strong>Disputes:</strong> Subject to Kolkata jurisdiction.<br><br>
|
||||||
|
|
||||||
|
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
|
||||||
|
</div>"""
|
||||||
|
|
||||||
|
PO_TC = f"""<div style="font-size:10.5px; line-height:1.8; color:#333;">
|
||||||
|
<strong>FURNITEX — Purchase Order Terms & Conditions</strong><br><br>
|
||||||
|
|
||||||
|
1. <strong>Acceptance:</strong> Supply of goods/services against this PO constitutes acceptance of these terms.<br>
|
||||||
|
2. <strong>Quality:</strong> All materials must conform to the specifications mentioned. Substandard materials will be rejected at supplier's cost.<br>
|
||||||
|
3. <strong>Delivery:</strong> Deliver to the address specified on the PO by the agreed date. Delays must be communicated 48 hours in advance.<br>
|
||||||
|
4. <strong>Invoice:</strong> Raise GST-compliant invoice (or cash memo for URD) with PO reference. Payment processed within 7 days of invoice receipt and material acceptance.<br>
|
||||||
|
5. <strong>Warranty:</strong> Supplier warrants materials against defects for minimum 6 months from delivery.<br>
|
||||||
|
6. <strong>Jurisdiction:</strong> Kolkata courts have exclusive jurisdiction.<br><br>
|
||||||
|
|
||||||
|
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
|
||||||
|
</div>"""
|
||||||
|
|
||||||
|
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}")
|
||||||
Loading…
Reference in a new issue