style: fix black/isort formatting and codespell typo to pass pre-commit lint

- 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 <noreply@anthropic.com>
This commit is contained in:
SUBHANKAR DHAR 2026-06-16 15:41:54 +05:30
parent 4678d517ae
commit c40e7992cb
4 changed files with 1008 additions and 586 deletions

View file

@ -6,7 +6,6 @@ Run via: bench --site frontend execute frappe.delete_streetwok.run
import frappe import frappe
COMPANY = "streetwok (Demo)" COMPANY = "streetwok (Demo)"
@ -47,7 +46,8 @@ def run():
# Get submitted docs for this company # Get submitted docs for this company
docs = frappe.db.sql( docs = frappe.db.sql(
f"SELECT name FROM `tab{dt}` WHERE company=%s AND docstatus=1", f"SELECT name FROM `tab{dt}` WHERE company=%s AND docstatus=1",
COMPANY, as_dict=1 COMPANY,
as_dict=1,
) )
if docs: if docs:
print(f" Cancelling {len(docs)} submitted {dt}(s)...") print(f" Cancelling {len(docs)} submitted {dt}(s)...")
@ -116,27 +116,28 @@ def run():
for wh in wh_list: for wh in wh_list:
try: try:
frappe.delete_doc("Warehouse", wh.name, frappe.delete_doc(
ignore_permissions=True, force=True, "Warehouse",
ignore_on_trash=True) wh.name,
except Exception as e: ignore_permissions=True,
frappe.db.sql( force=True,
"DELETE FROM `tabWarehouse` WHERE name=%s", wh.name ignore_on_trash=True,
) )
except Exception as e:
frappe.db.sql("DELETE FROM `tabWarehouse` WHERE name=%s", wh.name)
frappe.db.commit() frappe.db.commit()
print(f" Deleted {len(wh_list)} Warehouse(s)") print(f" Deleted {len(wh_list)} Warehouse(s)")
# ── STEP 7: Delete Cost Centers ─────────────────────────────── # ── STEP 7: Delete Cost Centers ───────────────────────────────
cc_list = frappe.db.sql( cc_list = frappe.db.sql(
"SELECT name FROM `tabCost Center` WHERE company=%s ORDER BY lft DESC", "SELECT name FROM `tabCost Center` WHERE company=%s ORDER BY lft DESC",
COMPANY, as_dict=1 COMPANY,
as_dict=1,
) )
if cc_list: if cc_list:
for cc in cc_list: for cc in cc_list:
try: try:
frappe.db.sql( frappe.db.sql("DELETE FROM `tabCost Center` WHERE name=%s", cc.name)
"DELETE FROM `tabCost Center` WHERE name=%s", cc.name
)
except Exception: except Exception:
pass pass
frappe.db.commit() frappe.db.commit()
@ -149,21 +150,21 @@ def run():
frappe.db.sql( frappe.db.sql(
"DELETE FROM `tabAccount` WHERE company=%s AND is_group=0", COMPANY "DELETE FROM `tabAccount` WHERE company=%s AND is_group=0", COMPANY
) )
frappe.db.sql( frappe.db.sql("DELETE FROM `tabAccount` WHERE company=%s", COMPANY)
"DELETE FROM `tabAccount` WHERE company=%s", COMPANY
)
frappe.db.commit() frappe.db.commit()
print(f" Deleted {acct_count} Account(s)") print(f" Deleted {acct_count} Account(s)")
# ── STEP 9: Delete Fiscal Years linked only to this company ─── # ── STEP 9: Delete Fiscal Years linked only to this company ───
fy_links = frappe.db.sql( fy_links = frappe.db.sql(
"""SELECT parent FROM `tabFiscal Year Company` """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: for fy in fy_links:
frappe.db.sql( frappe.db.sql(
"DELETE FROM `tabFiscal Year Company` WHERE company=%s AND parent=%s", "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 # If this fiscal year has no other company links, delete it too
remaining = frappe.db.count("Fiscal Year Company", {"parent": fy.parent}) remaining = frappe.db.count("Fiscal Year Company", {"parent": fy.parent})
@ -186,7 +187,8 @@ def run():
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM `tabSales Order` SELECT 1 FROM `tabSales Order`
WHERE customer=c.name AND company='Furnitex' AND docstatus < 2 WHERE customer=c.name AND company='Furnitex' AND docstatus < 2
)""", as_dict=1 )""",
as_dict=1,
) )
if stale_customers: if stale_customers:
for c in stale_customers: for c in stale_customers:
@ -199,9 +201,13 @@ def run():
# ── STEP 11: Delete the Company record itself ───────────────── # ── STEP 11: Delete the Company record itself ─────────────────
try: try:
frappe.delete_doc("Company", COMPANY, frappe.delete_doc(
ignore_permissions=True, force=True, "Company",
ignore_on_trash=True) COMPANY,
ignore_permissions=True,
force=True,
ignore_on_trash=True,
)
except Exception: except Exception:
frappe.db.sql("DELETE FROM `tabCompany` WHERE name=%s", COMPANY) frappe.db.sql("DELETE FROM `tabCompany` WHERE name=%s", COMPANY)
frappe.db.commit() frappe.db.commit()
@ -219,7 +225,8 @@ def run():
"""SELECT name FROM `tabItem` """SELECT name FROM `tabItem`
WHERE item_name LIKE '%streetwok%' WHERE item_name LIKE '%streetwok%'
OR item_code LIKE '%streetwok%' OR item_code LIKE '%streetwok%'
OR description LIKE '%streetwok%'""", as_dict=1 OR description LIKE '%streetwok%'""",
as_dict=1,
) )
if stale_items: if stale_items:
for item in stale_items: for item in stale_items:

View file

@ -9,11 +9,11 @@ Or directly:
import frappe import frappe
import frappe.defaults import frappe.defaults
COMPANY = "Furnitex" COMPANY = "Furnitex"
SITE = "frontend" SITE = "frontend"
ABBR = None # resolved at runtime via get_abbr() ABBR = None # resolved at runtime via get_abbr()
def get_abbr(): def get_abbr():
global ABBR global ABBR
if not ABBR: if not ABBR:
@ -25,13 +25,16 @@ def get_abbr():
# HELPERS # HELPERS
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def exists(doctype, name): def exists(doctype, name):
return frappe.db.exists(doctype, name) return frappe.db.exists(doctype, name)
def exists_filter(doctype, filters): def exists_filter(doctype, filters):
"""Existence check by filters (for docs where name includes company abbr).""" """Existence check by filters (for docs where name includes company abbr)."""
return frappe.db.get_value(doctype, filters, "name") return frappe.db.get_value(doctype, filters, "name")
def safe_insert(doc): def safe_insert(doc):
"""Insert and return True, or skip silently on duplicate and return False.""" """Insert and return True, or skip silently on duplicate and return False."""
try: try:
@ -45,12 +48,15 @@ def safe_insert(doc):
return False return False
raise raise
def ok(msg): def ok(msg):
print(f" [OK] {msg}") print(f" [OK] {msg}")
def skip(msg): def skip(msg):
print(f" [SKIP] {msg}") print(f" [SKIP] {msg}")
def warn(msg): def warn(msg):
print(f" [WARN] {msg}") print(f" [WARN] {msg}")
@ -59,6 +65,7 @@ def warn(msg):
# 1. UOMs # 1. UOMs
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_uoms(): def create_uoms():
print("\n[1/9] Creating UOMs...") print("\n[1/9] Creating UOMs...")
uoms = [ uoms = [
@ -71,11 +78,13 @@ def create_uoms():
] ]
for uom_name, whole in uoms: for uom_name, whole in uoms:
if not exists("UOM", uom_name): if not exists("UOM", uom_name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "UOM", "doctype": "UOM",
"uom_name": uom_name, "uom_name": uom_name,
"must_be_whole_number": whole, "must_be_whole_number": whole,
}) }
)
d.flags.ignore_mandatory = True d.flags.ignore_mandatory = True
if safe_insert(d): if safe_insert(d):
ok(f"UOM: {uom_name}") ok(f"UOM: {uom_name}")
@ -90,6 +99,7 @@ def create_uoms():
# 2. ITEM GROUPS # 2. ITEM GROUPS
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_item_groups(): def create_item_groups():
print("\n[2/9] Creating Item Groups...") print("\n[2/9] Creating Item Groups...")
groups = [ groups = [
@ -103,12 +113,14 @@ def create_item_groups():
] ]
for name, parent in groups: for name, parent in groups:
if not exists("Item Group", name): if not exists("Item Group", name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Item Group", "doctype": "Item Group",
"item_group_name": name, "item_group_name": name,
"parent_item_group": parent, "parent_item_group": parent,
"is_group": 0, "is_group": 0,
}) }
)
if safe_insert(d): if safe_insert(d):
ok(f"Item Group: {name}") ok(f"Item Group: {name}")
else: else:
@ -122,6 +134,7 @@ def create_item_groups():
# 3. SUPPLIER GROUPS # 3. SUPPLIER GROUPS
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_supplier_groups(): def create_supplier_groups():
print("\n[3/9] Creating Supplier Groups...") print("\n[3/9] Creating Supplier Groups...")
groups = [ groups = [
@ -131,11 +144,13 @@ def create_supplier_groups():
] ]
for name, parent in groups: for name, parent in groups:
if not exists("Supplier Group", name): if not exists("Supplier Group", name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Supplier Group", "doctype": "Supplier Group",
"supplier_group_name": name, "supplier_group_name": name,
"parent_supplier_group": parent, "parent_supplier_group": parent,
}) }
)
if safe_insert(d): if safe_insert(d):
ok(f"Supplier Group: {name}") ok(f"Supplier Group: {name}")
else: else:
@ -149,15 +164,14 @@ def create_supplier_groups():
# 4. WAREHOUSES # 4. WAREHOUSES
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_warehouses(): def create_warehouses():
print("\n[4/9] Creating Warehouses...") print("\n[4/9] Creating Warehouses...")
abbr = get_abbr() abbr = get_abbr()
# Find the company's root warehouse group # Find the company's root warehouse group
root_wh = frappe.db.get_value( root_wh = frappe.db.get_value(
"Warehouse", "Warehouse", {"company": COMPANY, "is_group": 1}, "name"
{"company": COMPANY, "is_group": 1},
"name"
) )
if not root_wh: if not root_wh:
root_wh = f"All Warehouses - {abbr}" root_wh = f"All Warehouses - {abbr}"
@ -170,13 +184,15 @@ def create_warehouses():
# ERPNext appends company abbr: "Main Store - F" # ERPNext appends company abbr: "Main Store - F"
w_full = f"{w_short} - {abbr}" w_full = f"{w_short} - {abbr}"
if not exists("Warehouse", w_full): if not exists("Warehouse", w_full):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Warehouse", "doctype": "Warehouse",
"warehouse_name": w_short, "warehouse_name": w_short,
"parent_warehouse": parent, "parent_warehouse": parent,
"company": COMPANY, "company": COMPANY,
"is_group": is_group, "is_group": is_group,
}) }
)
if safe_insert(d): if safe_insert(d):
ok(f"Warehouse: {w_full}") ok(f"Warehouse: {w_full}")
else: else:
@ -190,6 +206,7 @@ def create_warehouses():
# 5. TAX TEMPLATES # 5. TAX TEMPLATES
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def _find_account(account_name_fragment, root_type=None, account_type=None): def _find_account(account_name_fragment, root_type=None, account_type=None):
"""Find an account by partial name match under the company.""" """Find an account by partial name match under the company."""
filters = {"company": COMPANY, "is_group": 0} 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 filters["account_type"] = account_type
# Try exact name match first # Try exact name match first
result = frappe.db.get_value("Account", result = frappe.db.get_value(
dict(filters, account_name=account_name_fragment), "name") "Account", dict(filters, account_name=account_name_fragment), "name"
)
if result: if result:
return 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 WHERE company=%s AND is_group=0
AND account_name LIKE %s AND account_name LIKE %s
LIMIT 1""", LIMIT 1""",
(COMPANY, like_pattern), as_dict=0 (COMPANY, like_pattern),
as_dict=0,
) )
return result[0][0] if result else None return result[0][0] if result else None
@ -222,15 +241,18 @@ def create_tax_templates():
# ── No GST (URD Purchase) ── # ── No GST (URD Purchase) ──
urd_title = "No GST - URD Purchase" urd_title = "No GST - URD Purchase"
if not exists_filter("Purchase Taxes and Charges Template", if not exists_filter(
{"title": urd_title, "company": COMPANY}): "Purchase Taxes and Charges Template", {"title": urd_title, "company": COMPANY}
d = frappe.get_doc({ ):
d = frappe.get_doc(
{
"doctype": "Purchase Taxes and Charges Template", "doctype": "Purchase Taxes and Charges Template",
"title": urd_title, "title": urd_title,
"company": COMPANY, "company": COMPANY,
"is_default": 0, "is_default": 0,
"taxes": [], "taxes": [],
}) }
)
if safe_insert(d): if safe_insert(d):
ok(f"Purchase Tax Template: {urd_title}") ok(f"Purchase Tax Template: {urd_title}")
else: else:
@ -240,27 +262,45 @@ def create_tax_templates():
# ── GST 18% Purchase ── # ── GST 18% Purchase ──
gst18_p_title = "GST 18% - Purchase" gst18_p_title = "GST 18% - Purchase"
if not exists_filter("Purchase Taxes and Charges Template", if not exists_filter(
{"title": gst18_p_title, "company": COMPANY}): "Purchase Taxes and Charges Template",
{"title": gst18_p_title, "company": COMPANY},
):
cgst = _find_account("CGST") cgst = _find_account("CGST")
sgst = _find_account("SGST") sgst = _find_account("SGST")
taxes = [] taxes = []
if cgst: if cgst:
taxes.append({"charge_type": "On Net Total", "account_head": cgst, taxes.append(
"rate": 9, "description": "CGST @ 9%"}) {
"charge_type": "On Net Total",
"account_head": cgst,
"rate": 9,
"description": "CGST @ 9%",
}
)
if sgst: if sgst:
taxes.append({"charge_type": "On Net Total", "account_head": sgst, taxes.append(
"rate": 9, "description": "SGST @ 9%"}) {
d = frappe.get_doc({ "charge_type": "On Net Total",
"account_head": sgst,
"rate": 9,
"description": "SGST @ 9%",
}
)
d = frappe.get_doc(
{
"doctype": "Purchase Taxes and Charges Template", "doctype": "Purchase Taxes and Charges Template",
"title": gst18_p_title, "title": gst18_p_title,
"company": COMPANY, "company": COMPANY,
"is_default": 0, "is_default": 0,
"taxes": taxes, "taxes": taxes,
}) }
)
if safe_insert(d): if safe_insert(d):
ok(f"Purchase Tax Template: {gst18_p_title}" + ok(
(" (no GST accounts in CoA, left empty)" if not taxes else "")) f"Purchase Tax Template: {gst18_p_title}"
+ (" (no GST accounts in CoA, left empty)" if not taxes else "")
)
else: else:
skip(f"Purchase Tax Template: {gst18_p_title} (duplicate)") skip(f"Purchase Tax Template: {gst18_p_title} (duplicate)")
else: else:
@ -268,27 +308,44 @@ def create_tax_templates():
# ── GST 18% Sales ── # ── GST 18% Sales ──
gst18_s_title = "GST 18% - Sales" gst18_s_title = "GST 18% - Sales"
if not exists_filter("Sales Taxes and Charges Template", if not exists_filter(
{"title": gst18_s_title, "company": COMPANY}): "Sales Taxes and Charges Template", {"title": gst18_s_title, "company": COMPANY}
):
cgst = _find_account("CGST") cgst = _find_account("CGST")
sgst = _find_account("SGST") sgst = _find_account("SGST")
taxes = [] taxes = []
if cgst: if cgst:
taxes.append({"charge_type": "On Net Total", "account_head": cgst, taxes.append(
"rate": 9, "description": "CGST @ 9%"}) {
"charge_type": "On Net Total",
"account_head": cgst,
"rate": 9,
"description": "CGST @ 9%",
}
)
if sgst: if sgst:
taxes.append({"charge_type": "On Net Total", "account_head": sgst, taxes.append(
"rate": 9, "description": "SGST @ 9%"}) {
d = frappe.get_doc({ "charge_type": "On Net Total",
"account_head": sgst,
"rate": 9,
"description": "SGST @ 9%",
}
)
d = frappe.get_doc(
{
"doctype": "Sales Taxes and Charges Template", "doctype": "Sales Taxes and Charges Template",
"title": gst18_s_title, "title": gst18_s_title,
"company": COMPANY, "company": COMPANY,
"is_default": 0, "is_default": 0,
"taxes": taxes, "taxes": taxes,
}) }
)
if safe_insert(d): if safe_insert(d):
ok(f"Sales Tax Template: {gst18_s_title}" + ok(
(" (no GST accounts in CoA, left empty)" if not taxes else "")) f"Sales Tax Template: {gst18_s_title}"
+ (" (no GST accounts in CoA, left empty)" if not taxes else "")
)
else: else:
skip(f"Sales Tax Template: {gst18_s_title} (duplicate)") skip(f"Sales Tax Template: {gst18_s_title} (duplicate)")
else: else:
@ -301,30 +358,78 @@ def create_tax_templates():
# 6. SERVICE ITEMS (Non-stock billing items) # 6. SERVICE ITEMS (Non-stock billing items)
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_service_items(): def create_service_items():
print("\n[6/9] Creating Service Items...") print("\n[6/9] Creating Service Items...")
income_acct = _find_account("Sales", root_type="Income") or \ income_acct = _find_account("Sales", root_type="Income") or _find_account(
_find_account("Service", root_type="Income") "Service", root_type="Income"
)
items = [ items = [
# (code, name, uom, description) # (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-FC-EXEC",
("SVC-KIT-EXEC", "Modular Kitchen Execution", "SqFt", "Modular kitchen fabrication and installation."), "False Ceiling Execution",
("SVC-FLOOR", "Flooring Execution", "SqFt", "Vinyl/Wood/Tile flooring supply and installation."), "SqFt",
("SVC-LUMP", "Lumpsum Contract Work", "Nos", "Fixed-price lumpsum milestone. Change qty to 1 always."), "Labour + material for false ceiling. Bill per SqFt OR as lumpsum.",
("SVC-DESIGN", "Interior Design Consultation", "Nos", "Design + drawing fee — lumpsum."), ),
("SVC-CONVEY", "Site Conveyance & Transport", "Nos", "Transport charges to/from site."), (
"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-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-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: for code, name, uom, desc in items:
if not exists("Item", code): if not exists("Item", code):
# Non-stock service items: no warehouse needed in item_defaults # Non-stock service items: no warehouse needed in item_defaults
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Item", "doctype": "Item",
"item_code": code, "item_code": code,
"item_name": name, "item_name": name,
@ -337,7 +442,8 @@ def create_service_items():
"is_sales_item": 1, "is_sales_item": 1,
"standard_rate": 0, "standard_rate": 0,
# No item_defaults row — avoids warehouse company-mismatch validation # No item_defaults row — avoids warehouse company-mismatch validation
}) }
)
if safe_insert(d): if safe_insert(d):
ok(f"Service Item: {code} [{uom}]") ok(f"Service Item: {code} [{uom}]")
else: else:
@ -352,12 +458,15 @@ def create_service_items():
# 7. RAW MATERIAL ITEMS (Stock items) # 7. RAW MATERIAL ITEMS (Stock items)
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_raw_material_items(): def create_raw_material_items():
print("\n[7/9] Creating Raw Material Items...") print("\n[7/9] Creating Raw Material Items...")
cogs_acct = (_find_account("Cost of Goods Sold", root_type="Expense") or cogs_acct = (
_find_account("Stock Expenses", root_type="Expense") or _find_account("Cost of Goods Sold", root_type="Expense")
_find_account("Expenses Included", 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()}" default_wh = f"Main Store - {get_abbr()}"
if not exists("Warehouse", default_wh): if not exists("Warehouse", default_wh):
@ -383,9 +492,14 @@ def create_raw_material_items():
("RM-HW-SCREW", "Wood Screw Assorted", "Bundle", "Hardware & Fittings"), ("RM-HW-SCREW", "Wood Screw Assorted", "Bundle", "Hardware & Fittings"),
("RM-CIV-CEM", "OPC Cement 53 Grade", "Bag", "Civil & Surface Materials"), ("RM-CIV-CEM", "OPC Cement 53 Grade", "Bag", "Civil & Surface Materials"),
("RM-CIV-PUTTY", "Wall Putty White", "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-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-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: for code, name, uom, group in items:
@ -425,29 +539,52 @@ def create_raw_material_items():
# 8. SAMPLE SUPPLIERS # 8. SAMPLE SUPPLIERS
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_suppliers(): def create_suppliers():
print("\n[8/9] Creating Sample Suppliers...") print("\n[8/9] Creating Sample Suppliers...")
suppliers = [ suppliers = [
# (name, group, gst_category) # (name, group, gst_category)
("Local Hardware Market - Kolkata", "Local Market Vendor (Unregistered)", "Unregistered"), (
("Burrabazar Plywood Supplier", "Local Market Vendor (Unregistered)", "Unregistered"), "Local Hardware Market - Kolkata",
("Fancy Laminates - Howrah", "Local Market Vendor (Unregistered)", "Unregistered"), "Local Market Vendor (Unregistered)",
("Modern Furniture Hardware - BBD Bag","Local Market Vendor (Unregistered)", "Unregistered"), "Unregistered",
("Registered Hardware Supplier Ltd", "GST Registered Vendor", "Registered Regular"), ),
(
"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"), ("Site Labour Contractor - Ramesh", "Labour Contractor", "Unregistered"),
] ]
for name, group, gst_cat in suppliers: for name, group, gst_cat in suppliers:
if not exists("Supplier", name): if not exists("Supplier", name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Supplier", "doctype": "Supplier",
"supplier_name": name, "supplier_name": name,
"supplier_group": group, "supplier_group": group,
"country": "India", "country": "India",
"gst_category": gst_cat, "gst_category": gst_cat,
"default_currency": "INR", "default_currency": "INR",
}) }
)
if safe_insert(d): if safe_insert(d):
ok(f"Supplier: {name} [{gst_cat}]") ok(f"Supplier: {name} [{gst_cat}]")
else: else:
@ -461,8 +598,9 @@ def create_suppliers():
# URD tax bypass is handled instead by the server script: # URD tax bypass is handled instead by the server script:
# "Furnitex - Clear GST on URD Purchase" (Before Save on Purchase Invoice) # "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. # Tick the "URD Purchase (No GST)" checkbox on any invoice to auto-clear taxes.
urd_suppliers_count = frappe.db.count("Supplier", urd_suppliers_count = frappe.db.count(
{"supplier_group": "Local Market Vendor (Unregistered)"}) "Supplier", {"supplier_group": "Local Market Vendor (Unregistered)"}
)
ok(f"URD tax via server script — {urd_suppliers_count} URD suppliers registered") ok(f"URD tax via server script — {urd_suppliers_count} URD suppliers registered")
@ -470,6 +608,7 @@ def create_suppliers():
# 9. CUSTOM FIELDS # 9. CUSTOM FIELDS
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_custom_fields(): def create_custom_fields():
print("\n[9/9] Creating Custom Fields...") print("\n[9/9] Creating Custom Fields...")
@ -481,10 +620,16 @@ def create_custom_fields():
# (dt, fieldname, label, fieldtype, options, insert_after, in_list_view) # (dt, fieldname, label, fieldtype, options, insert_after, in_list_view)
fields = [ fields = [
("Purchase Invoice", "is_urd_purchase", "URD Purchase (No GST)", (
"Check", None, "supplier", 1), "Purchase Invoice",
("Journal Entry", "project", "Project", "is_urd_purchase",
"Link", "Project", "voucher_type", 0), "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: for dt, fn, label, ft, opts, after, in_list in fields:
@ -512,13 +657,16 @@ def create_custom_fields():
skip(f"Custom Field: {dt}.{fn}") skip(f"Custom Field: {dt}.{fn}")
frappe.db.commit() 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) # 10. SERVER SCRIPTS (Automation)
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def create_server_scripts(): def create_server_scripts():
print("\n[+] Creating Server Scripts...") print("\n[+] Creating Server Scripts...")
@ -560,7 +708,8 @@ if not frappe.db.exists("Warehouse", warehouse_name):
""" """
if not exists("Server Script", wip_script_name): if not exists("Server Script", wip_script_name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Server Script", "doctype": "Server Script",
"name": wip_script_name, "name": wip_script_name,
"script_type": "DocType Event", "script_type": "DocType Event",
@ -568,7 +717,8 @@ if not frappe.db.exists("Warehouse", warehouse_name):
"doctype_event": "After Insert", "doctype_event": "After Insert",
"enabled": 1, "enabled": 1,
"script": wip_script_code, "script": wip_script_code,
}) }
)
d.flags.ignore_permissions = True d.flags.ignore_permissions = True
d.insert() d.insert()
ok(f"Server Script: {wip_script_name}") ok(f"Server Script: {wip_script_name}")
@ -597,7 +747,8 @@ if doc.is_urd_purchase:
""" """
if not exists("Server Script", urd_script_name): if not exists("Server Script", urd_script_name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Server Script", "doctype": "Server Script",
"name": urd_script_name, "name": urd_script_name,
"script_type": "DocType Event", "script_type": "DocType Event",
@ -605,7 +756,8 @@ if doc.is_urd_purchase:
"doctype_event": "Before Save", "doctype_event": "Before Save",
"enabled": 1, "enabled": 1,
"script": urd_script_code, "script": urd_script_code,
}) }
)
d.flags.ignore_permissions = True d.flags.ignore_permissions = True
d.insert() d.insert()
ok(f"Server Script: {urd_script_name}") ok(f"Server Script: {urd_script_name}")
@ -706,11 +858,13 @@ def get_data(filters):
return rows return rows
''' '''
def create_custom_report(): def create_custom_report():
print("\n[+] Creating Custom Report: Furnitex Project Profitability...") print("\n[+] Creating Custom Report: Furnitex Project Profitability...")
report_name = "Furnitex Project Profitability" report_name = "Furnitex Project Profitability"
if not exists("Report", report_name): if not exists("Report", report_name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Report", "doctype": "Report",
"report_name": report_name, "report_name": report_name,
"ref_doctype": "Project", "ref_doctype": "Project",
@ -718,7 +872,8 @@ def create_custom_report():
"is_standard": "No", "is_standard": "No",
"module": "Projects", "module": "Projects",
"script": PROFIT_REPORT_SCRIPT, "script": PROFIT_REPORT_SCRIPT,
}) }
)
d.flags.ignore_permissions = True d.flags.ignore_permissions = True
d.insert() d.insert()
ok(f"Custom Report: {report_name}") ok(f"Custom Report: {report_name}")
@ -731,6 +886,7 @@ def create_custom_report():
# MASTER RUNNER # MASTER RUNNER
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
def run_all(): def run_all():
frappe.set_user("Administrator") frappe.set_user("Administrator")
print("\n" + "=" * 58) print("\n" + "=" * 58)
@ -759,6 +915,7 @@ def run_all():
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
frappe.init(site="frontend") frappe.init(site="frontend")
frappe.connect() frappe.connect()
run_all() run_all()

View file

@ -9,8 +9,13 @@ import frappe
COMPANY = "Furnitex" COMPANY = "Furnitex"
def ok(msg): print(f" [OK] {msg}") def ok(msg):
def skip(msg): print(f" [SKIP] {msg}") print(f" [OK] {msg}")
def skip(msg):
print(f" [SKIP] {msg}")
def safe_insert(doc): def safe_insert(doc):
try: try:
@ -25,9 +30,11 @@ def safe_insert(doc):
return False return False
raise raise
def exists(dt, name): def exists(dt, name):
return frappe.db.exists(dt, name) return frappe.db.exists(dt, name)
def exists_f(dt, filters): def exists_f(dt, filters):
return frappe.db.get_value(dt, filters, "name") return frappe.db.get_value(dt, filters, "name")
@ -39,7 +46,7 @@ def create_customer_groups():
print("\n[1] Customer Groups...") print("\n[1] Customer Groups...")
groups = [ groups = [
("Residential - Individual", "Individual"), ("Residential - Individual", "Individual"),
("Residential - Builder Flat","Individual"), ("Residential - Builder Flat", "Individual"),
("Commercial - Office", "Commercial"), ("Commercial - Office", "Commercial"),
("Commercial - Restaurant", "Commercial"), ("Commercial - Restaurant", "Commercial"),
("Commercial - Hotel", "Commercial"), ("Commercial - Hotel", "Commercial"),
@ -48,14 +55,18 @@ def create_customer_groups():
] ]
for name, parent in groups: for name, parent in groups:
if not exists("Customer Group", name): if not exists("Customer Group", name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Customer Group", "doctype": "Customer Group",
"customer_group_name": name, "customer_group_name": name,
"parent_customer_group": parent, "parent_customer_group": parent,
"is_group": 0, "is_group": 0,
}) }
if safe_insert(d): ok(f"Customer Group: {name}") )
else: skip(f"{name} (dup)") if safe_insert(d):
ok(f"Customer Group: {name}")
else:
skip(f"{name} (dup)")
else: else:
skip(f"Customer Group: {name}") skip(f"Customer Group: {name}")
frappe.db.commit() frappe.db.commit()
@ -80,14 +91,18 @@ def create_territories():
] ]
for name, parent in territories: for name, parent in territories:
if not exists("Territory", name): if not exists("Territory", name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Territory", "doctype": "Territory",
"territory_name": name, "territory_name": name,
"parent_territory": parent, "parent_territory": parent,
"is_group": 0, "is_group": 0,
}) }
if safe_insert(d): ok(f"Territory: {name}") )
else: skip(f"{name} (dup)") if safe_insert(d):
ok(f"Territory: {name}")
else:
skip(f"{name} (dup)")
else: else:
skip(f"Territory: {name}") skip(f"Territory: {name}")
frappe.db.commit() frappe.db.commit()
@ -96,7 +111,8 @@ def create_territories():
# ────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────
# 3. LEAD SOURCES (added as custom Select field on Lead) # 3. LEAD SOURCES (added as custom Select field on Lead)
# ────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────
LEAD_SOURCES = "\n".join([ LEAD_SOURCES = "\n".join(
[
"Instagram", "Instagram",
"Facebook", "Facebook",
"Word of Mouth", "Word of Mouth",
@ -112,17 +128,23 @@ LEAD_SOURCES = "\n".join([
"Exhibition / Home Fair", "Exhibition / Home Fair",
"YouTube", "YouTube",
"WhatsApp Broadcast", "WhatsApp Broadcast",
]) ]
)
def create_lead_sources(): def create_lead_sources():
# Lead Source is a standalone CRM-app doctype not installed here. # Lead Source is a standalone CRM-app doctype not installed here.
# We add it as a custom Select field on Lead + Opportunity instead. # We add it as a custom Select field on Lead + Opportunity instead.
print("\n[3] Lead Sources (as custom Select field)...") 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" cf_name = f"{dt}-furnitex_lead_source"
if not exists("Custom Field", cf_name): if not exists("Custom Field", cf_name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Custom Field", "doctype": "Custom Field",
"dt": dt, "dt": dt,
"fieldname": "furnitex_lead_source", "fieldname": "furnitex_lead_source",
@ -132,9 +154,12 @@ def create_lead_sources():
"insert_after": after_field, "insert_after": after_field,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
}) }
if safe_insert(d): ok(f"Lead Source Select field → {dt}") )
else: skip(f"{dt}.furnitex_lead_source (dup)") if safe_insert(d):
ok(f"Lead Source Select field → {dt}")
else:
skip(f"{dt}.furnitex_lead_source (dup)")
else: else:
skip(f"Lead Source field exists on {dt}") skip(f"Lead Source field exists on {dt}")
frappe.db.commit() frappe.db.commit()
@ -147,20 +172,24 @@ def create_sales_persons():
print("\n[4] Sales Persons...") print("\n[4] Sales Persons...")
persons = [ persons = [
("Furnitex - Site Team", "Sales Team"), ("Furnitex - Site Team", "Sales Team"),
("Furnitex - Design Team","Sales Team"), ("Furnitex - Design Team", "Sales Team"),
("Furnitex - BD Manager", "Sales Team"), ("Furnitex - BD Manager", "Sales Team"),
] ]
for name, parent in persons: for name, parent in persons:
if not exists("Sales Person", name): if not exists("Sales Person", name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Sales Person", "doctype": "Sales Person",
"sales_person_name": name, "sales_person_name": name,
"parent_sales_person": parent, "parent_sales_person": parent,
"is_group": 0, "is_group": 0,
"enabled": 1, "enabled": 1,
}) }
if safe_insert(d): ok(f"Sales Person: {name}") )
else: skip(f"{name} (dup)") if safe_insert(d):
ok(f"Sales Person: {name}")
else:
skip(f"{name} (dup)")
else: else:
skip(f"Sales Person: {name}") skip(f"Sales Person: {name}")
frappe.db.commit() frappe.db.commit()
@ -181,10 +210,12 @@ def create_payment_modes():
] ]
# find default bank account # find default bank account
bank_acct = frappe.db.get_value("Account", bank_acct = frappe.db.get_value(
{"account_type": "Bank", "company": COMPANY, "is_group": 0}, "name") "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") cash_acct = frappe.db.get_value(
"Account", {"account_type": "Cash", "company": COMPANY, "is_group": 0}, "name"
)
for name, mtype in modes: for name, mtype in modes:
if not exists("Mode of Payment", name): if not exists("Mode of Payment", name):
@ -195,13 +226,17 @@ def create_payment_modes():
} }
acct = bank_acct if mtype == "Bank" else cash_acct acct = bank_acct if mtype == "Bank" else cash_acct
if acct: if acct:
d_dict["accounts"] = [{ d_dict["accounts"] = [
{
"company": COMPANY, "company": COMPANY,
"default_account": acct, "default_account": acct,
}] }
]
d = frappe.get_doc(d_dict) d = frappe.get_doc(d_dict)
if safe_insert(d): ok(f"Mode of Payment: {name}") if safe_insert(d):
else: skip(f"{name} (dup)") ok(f"Mode of Payment: {name}")
else:
skip(f"{name} (dup)")
else: else:
skip(f"Mode of Payment: {name}") skip(f"Mode of Payment: {name}")
frappe.db.commit() frappe.db.commit()
@ -214,62 +249,88 @@ def create_payment_terms():
print("\n[6] Payment Terms Templates...") print("\n[6] Payment Terms Templates...")
templates = [ templates = [
# ── A: Standard Interior Project (milestone-based) ─────── # ── A: Standard Interior Project (milestone-based) ───────
{ {
"name": "Furnitex - Standard Interior Project", "name": "Furnitex - Standard Interior Project",
"terms": [ "terms": [
{"payment_term_name": "30% Advance on Agreement", {
"invoice_portion": 30, "credit_days": 0, "payment_term_name": "30% Advance on Agreement",
"description": "Booking advance on signing agreement"}, "invoice_portion": 30,
{"payment_term_name": "30% on 50% Site Completion", "credit_days": 0,
"invoice_portion": 30, "credit_days": 30, "description": "Booking advance on signing agreement",
"description": "Second milestone: 50% work done"}, },
{"payment_term_name": "30% on Work Completion", {
"invoice_portion": 30, "credit_days": 60, "payment_term_name": "30% on 50% Site Completion",
"description": "Third milestone: work complete"}, "invoice_portion": 30,
{"payment_term_name": "10% on Final Handover", "credit_days": 30,
"invoice_portion": 10, "credit_days": 75, "description": "Second milestone: 50% work done",
"description": "Retention released at handover"}, },
{
"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) ────────────────────────── # ── B: 50-50 (smaller projects) ──────────────────────────
{ {
"name": "Furnitex - 50-50 Advance", "name": "Furnitex - 50-50 Advance",
"terms": [ "terms": [
{"payment_term_name": "50% Advance", {
"invoice_portion": 50, "credit_days": 0, "payment_term_name": "50% Advance",
"description": "Advance before work begins"}, "invoice_portion": 50,
{"payment_term_name": "50% on Delivery", "credit_days": 0,
"invoice_portion": 50, "credit_days": 30, "description": "Advance before work begins",
"description": "Balance on delivery / installation"}, },
{
"payment_term_name": "50% on Delivery",
"invoice_portion": 50,
"credit_days": 30,
"description": "Balance on delivery / installation",
},
], ],
}, },
# ── C: 100% Advance (small orders / loose furniture) ───── # ── C: 100% Advance (small orders / loose furniture) ─────
{ {
"name": "Furnitex - 100% Advance", "name": "Furnitex - 100% Advance",
"terms": [ "terms": [
{"payment_term_name": "100% Advance", {
"invoice_portion": 100, "credit_days": 0, "payment_term_name": "100% Advance",
"description": "Full payment before production"}, "invoice_portion": 100,
"credit_days": 0,
"description": "Full payment before production",
},
], ],
}, },
# ── D: Lumpsum 3-stage (commercial projects) ───────────── # ── D: Lumpsum 3-stage (commercial projects) ─────────────
{ {
"name": "Furnitex - Commercial 3-Stage", "name": "Furnitex - Commercial 3-Stage",
"terms": [ "terms": [
{"payment_term_name": "40% Advance - Commercial", {
"invoice_portion": 40, "credit_days": 0, "payment_term_name": "40% Advance - Commercial",
"description": "Mobilisation advance"}, "invoice_portion": 40,
{"payment_term_name": "40% Mid-Stage - Commercial", "credit_days": 0,
"invoice_portion": 40, "credit_days": 45, "description": "Mobilisation advance",
"description": "Mid-project milestone"}, },
{"payment_term_name": "20% Retention - Commercial", {
"invoice_portion": 20, "credit_days": 90, "payment_term_name": "40% Mid-Stage - Commercial",
"description": "Retention on final handover"}, "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"]: for term in t["terms"]:
pt_name = term["payment_term_name"] pt_name = term["payment_term_name"]
if not exists("Payment Term", pt_name): if not exists("Payment Term", pt_name):
pt = frappe.get_doc({ pt = frappe.get_doc(
{
"doctype": "Payment Term", "doctype": "Payment Term",
"payment_term_name": pt_name, "payment_term_name": pt_name,
"invoice_portion": term["invoice_portion"], "invoice_portion": term["invoice_portion"],
"credit_days_based_on": "Day(s) after invoice date", "credit_days_based_on": "Day(s) after invoice date",
"credit_days": term["credit_days"], "credit_days": term["credit_days"],
"description": term["description"], "description": term["description"],
}) }
)
safe_insert(pt) safe_insert(pt)
term_rows.append({ term_rows.append(
{
"payment_term": pt_name, "payment_term": pt_name,
"invoice_portion": term["invoice_portion"], "invoice_portion": term["invoice_portion"],
"credit_days_based_on": "Day(s) after invoice date", "credit_days_based_on": "Day(s) after invoice date",
"credit_days": term["credit_days"], "credit_days": term["credit_days"],
"description": term["description"], "description": term["description"],
}) }
)
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Payment Terms Template", "doctype": "Payment Terms Template",
"template_name": tname, "template_name": tname,
"terms": term_rows, "terms": term_rows,
}) }
if safe_insert(d): ok(f"Payment Terms: {tname}") )
else: skip(f"{tname} (dup)") if safe_insert(d):
ok(f"Payment Terms: {tname}")
else:
skip(f"{tname} (dup)")
else: else:
skip(f"Payment Terms: {tname}") skip(f"Payment Terms: {tname}")
@ -319,7 +388,6 @@ def create_terms_conditions():
print("\n[7] Terms & Conditions Templates...") print("\n[7] Terms & Conditions Templates...")
templates = [ templates = [
# ── Quotation T&C ───────────────────────────────────────── # ── Quotation T&C ─────────────────────────────────────────
{ {
"title": "Furnitex - Quotation Terms", "title": "Furnitex - Quotation Terms",
@ -337,7 +405,6 @@ def create_terms_conditions():
</ol> </ol>
<p><em>Furnitex | Interior Design &amp; Furniture | Kolkata</em></p>""", <p><em>Furnitex | Interior Design &amp; Furniture | Kolkata</em></p>""",
}, },
# ── Sales Invoice T&C ───────────────────────────────────── # ── Sales Invoice T&C ─────────────────────────────────────
{ {
"title": "Furnitex - Invoice Terms", "title": "Furnitex - Invoice Terms",
@ -352,7 +419,6 @@ def create_terms_conditions():
<p>Bank: [Your Bank] | A/c: [Account No] | IFSC: [IFSC] | UPI: [UPI ID]</p> <p>Bank: [Your Bank] | A/c: [Account No] | IFSC: [IFSC] | UPI: [UPI ID]</p>
<p><em>Thank you for choosing Furnitex!</em></p>""", <p><em>Thank you for choosing Furnitex!</em></p>""",
}, },
# ── Purchase Order T&C ──────────────────────────────────── # ── Purchase Order T&C ────────────────────────────────────
{ {
"title": "Furnitex - Purchase Order Terms", "title": "Furnitex - Purchase Order Terms",
@ -368,13 +434,17 @@ def create_terms_conditions():
for t in templates: for t in templates:
if not exists("Terms and Conditions", t["title"]): if not exists("Terms and Conditions", t["title"]):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Terms and Conditions", "doctype": "Terms and Conditions",
"title": t["title"], "title": t["title"],
"terms": t["terms"], "terms": t["terms"],
}) }
if safe_insert(d): ok(f"T&C: {t['title']}") )
else: skip(f"T&C: {t['title']} (dup)") if safe_insert(d):
ok(f"T&C: {t['title']}")
else:
skip(f"T&C: {t['title']} (dup)")
else: else:
skip(f"T&C: {t['title']}") skip(f"T&C: {t['title']}")
frappe.db.commit() frappe.db.commit()
@ -388,16 +458,20 @@ def create_price_list():
pl_name = "Furnitex Interior Rate Card" pl_name = "Furnitex Interior Rate Card"
if not exists("Price List", pl_name): if not exists("Price List", pl_name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Price List", "doctype": "Price List",
"price_list_name": pl_name, "price_list_name": pl_name,
"currency": "INR", "currency": "INR",
"selling": 1, "selling": 1,
"buying": 0, "buying": 0,
"enabled": 1, "enabled": 1,
}) }
if safe_insert(d): ok(f"Price List: {pl_name}") )
else: skip(f"{pl_name} (dup)") if safe_insert(d):
ok(f"Price List: {pl_name}")
else:
skip(f"{pl_name} (dup)")
else: else:
skip(f"Price List: {pl_name}") skip(f"Price List: {pl_name}")
@ -425,17 +499,23 @@ def create_price_list():
for code, rate in item_prices: for code, rate in item_prices:
if not exists("Item", code): if not exists("Item", code):
continue continue
if not exists_f("Item Price", {"item_code": code, "price_list": pl, "selling": 1}): if not exists_f(
ip = frappe.get_doc({ "Item Price", {"item_code": code, "price_list": pl, "selling": 1}
):
ip = frappe.get_doc(
{
"doctype": "Item Price", "doctype": "Item Price",
"item_code": code, "item_code": code,
"price_list": pl, "price_list": pl,
"selling": 1, "selling": 1,
"currency": "INR", "currency": "INR",
"price_list_rate": rate, "price_list_rate": rate,
}) }
if safe_insert(ip): ok(f"Item Price: {code}{rate} [{pl}]") )
else: skip(f"Item Price: {code} [{pl}] (dup)") if safe_insert(ip):
ok(f"Item Price: {code}{rate} [{pl}]")
else:
skip(f"Item Price: {code} [{pl}] (dup)")
else: else:
skip(f"Item Price: {code} [{pl}]") skip(f"Item Price: {code} [{pl}]")
@ -449,81 +529,240 @@ def create_crm_billing_fields():
print("\n[9] CRM & Billing Custom Fields...") print("\n[9] CRM & Billing Custom Fields...")
fields = [ fields = [
# ── CUSTOMER ───────────────────────────────────────────── # ── CUSTOMER ─────────────────────────────────────────────
("Customer", "whatsapp_number", "WhatsApp Number", (
"Data", None, "mobile_no", 0), "Customer",
("Customer", "property_address", "Property / Site Address", "whatsapp_number",
"Small Text", None, "whatsapp_number", 0), "WhatsApp Number",
("Customer", "project_type", "Project Type", "Data",
"Select", "Residential\nCommercial\nHospitality\nRetail", "property_address", 0), None,
("Customer", "property_size_sqft", "Approx. Area (SqFt)", "mobile_no",
"Float", None, "project_type", 0), 0,
("Customer", "budget_range", "Budget Range", ),
(
"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", "Select",
"Under ₹5 Lakhs\n₹510 Lakhs\n₹1025 Lakhs\n₹2550 Lakhs\nAbove ₹50 Lakhs", "Under ₹5 Lakhs\n₹510 Lakhs\n₹1025 Lakhs\n₹2550 Lakhs\nAbove ₹50 Lakhs",
"property_size_sqft", 0), "property_size_sqft",
("Customer", "referred_by", "Referred By", 0,
"Data", None, "budget_range", 0), ),
("Customer", "referred_by", "Referred By", "Data", None, "budget_range", 0),
# ── LEAD ───────────────────────────────────────────────── # ── LEAD ─────────────────────────────────────────────────
("Lead", "whatsapp_number", "WhatsApp Number", ("Lead", "whatsapp_number", "WhatsApp Number", "Data", None, "mobile_no", 0),
"Data", None, "mobile_no", 0), (
("Lead", "project_type_lead", "Project Type", "Lead",
"Select", "Residential\nCommercial\nHospitality\nRetail", "project_type_lead",
"whatsapp_number", 0), "Project Type",
("Lead", "property_address", "Property / Site Address", "Select",
"Small Text", None, "project_type_lead", 0), "Residential\nCommercial\nHospitality\nRetail",
("Lead", "area_sqft", "Approx. Area (SqFt)", "whatsapp_number",
"Float", None, "property_address", 0), 0,
("Lead", "budget_range", "Budget Range", ),
(
"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", "Select",
"Under ₹5 Lakhs\n₹510 Lakhs\n₹1025 Lakhs\n₹2550 Lakhs\nAbove ₹50 Lakhs", "Under ₹5 Lakhs\n₹510 Lakhs\n₹1025 Lakhs\n₹2550 Lakhs\nAbove ₹50 Lakhs",
"area_sqft", 0), "area_sqft",
("Lead", "site_visit_done", "Site Visit Done", 0,
"Check", None, "budget_range", 0), ),
("Lead", "site_visit_date", "Site Visit Date", (
"Date", None, "site_visit_done", 0), "Lead",
("Lead", "estimated_value", "Estimated Project Value (₹)", "site_visit_done",
"Currency", None, "site_visit_date", 0), "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 ────────────────────────────────────────────
("Quotation", "site_address", "Site / Delivery Address", (
"Small Text", None, "customer_address", 0), "Quotation",
("Quotation", "scope_of_work", "Scope of Work", "site_address",
"Small Text", None, "site_address", 0), "Site / Delivery Address",
("Quotation", "site_visit_date", "Site Visit Date", "Small Text",
"Date", None, "scope_of_work", 0), None,
("Quotation", "expected_start_date_q", "Expected Start Date", "customer_address",
"Date", None, "site_visit_date", 0), 0,
("Quotation", "expected_handover_date","Expected Handover Date", ),
"Date", None, "expected_start_date_q", 0), (
("Quotation", "design_style", "Design Style", "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", "Select",
"Modern / Contemporary\nTraditional / Classic\nMinimalist\nIndustrial\nBohemian\nMediterranean", "Modern / Contemporary\nTraditional / Classic\nMinimalist\nIndustrial\nBohemian\nMediterranean",
"expected_handover_date", 0), "expected_handover_date",
0,
),
# ── SALES INVOICE ──────────────────────────────────────── # ── SALES INVOICE ────────────────────────────────────────
# 'project' field is native on Sales Invoice in v16 — no custom field needed. # 'project' field is native on Sales Invoice in v16 — no custom field needed.
# Adding billing-specific extras after 'customer_name' instead. # Adding billing-specific extras after 'customer_name' instead.
("Sales Invoice", "milestone_description", "Milestone Description", (
"Small Text", None, "customer_name", 0), "Sales Invoice",
("Sales Invoice", "payment_mode_note", "Payment Mode Note", "milestone_description",
"Data", None, "milestone_description", 0), "Milestone Description",
"Small Text",
None,
"customer_name",
0,
),
(
"Sales Invoice",
"payment_mode_note",
"Payment Mode Note",
"Data",
None,
"milestone_description",
0,
),
# ── OPPORTUNITY ────────────────────────────────────────── # ── OPPORTUNITY ──────────────────────────────────────────
("Opportunity", "property_address", "Property / Site Address", (
"Small Text", None, "customer_name", 0), "Opportunity",
("Opportunity", "area_sqft", "Area (SqFt)", "property_address",
"Float", None, "property_address", 0), "Property / Site Address",
("Opportunity", "design_style", "Design Style", "Small Text",
None,
"customer_name",
0,
),
(
"Opportunity",
"area_sqft",
"Area (SqFt)",
"Float",
None,
"property_address",
0,
),
(
"Opportunity",
"design_style",
"Design Style",
"Select", "Select",
"Modern / Contemporary\nTraditional / Classic\nMinimalist\nIndustrial\nBohemian\nMediterranean", "Modern / Contemporary\nTraditional / Classic\nMinimalist\nIndustrial\nBohemian\nMediterranean",
"area_sqft", 0), "area_sqft",
("Opportunity", "site_visit_done", "Site Visit Done", 0,
"Check", None, "design_style", 0), ),
("Opportunity", "site_visit_date", "Site Visit Date", (
"Date", None, "site_visit_done", 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 [ for dt, fn, label, ft, opts, after, in_list in [
@ -543,8 +782,10 @@ def create_crm_billing_fields():
if opts: if opts:
d_dict["options"] = opts d_dict["options"] = opts
d = frappe.get_doc(d_dict) d = frappe.get_doc(d_dict)
if safe_insert(d): ok(f"Custom Field: {dt}.{fn}") if safe_insert(d):
else: skip(f"{dt}.{fn} (dup)") ok(f"Custom Field: {dt}.{fn}")
else:
skip(f"{dt}.{fn} (dup)")
else: else:
skip(f"Custom Field: {dt}.{fn}") skip(f"Custom Field: {dt}.{fn}")
@ -580,8 +821,10 @@ def create_sample_customers():
for c in customers: for c in customers:
if not exists("Customer", c["customer_name"]): if not exists("Customer", c["customer_name"]):
d = frappe.get_doc({"doctype": "Customer", **c}) d = frappe.get_doc({"doctype": "Customer", **c})
if safe_insert(d): ok(f"Customer: {c['customer_name']}") if safe_insert(d):
else: skip(f"{c['customer_name']} (dup)") ok(f"Customer: {c['customer_name']}")
else:
skip(f"{c['customer_name']} (dup)")
else: else:
skip(f"Customer: {c['customer_name']}") skip(f"Customer: {c['customer_name']}")
frappe.db.commit() frappe.db.commit()
@ -639,12 +882,16 @@ def create_crm_stages():
for stage in furnitex_stages: for stage in furnitex_stages:
if not exists("Sales Stage", stage): if not exists("Sales Stage", stage):
try: try:
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Sales Stage", "doctype": "Sales Stage",
"stage_name": stage, "stage_name": stage,
}) }
if safe_insert(d): ok(f"Sales Stage: {stage}") )
else: skip(f"{stage} (dup)") if safe_insert(d):
ok(f"Sales Stage: {stage}")
else:
skip(f"{stage} (dup)")
except Exception as e: except Exception as e:
skip(f"Sales Stage {stage}: {e}") skip(f"Sales Stage {stage}: {e}")
else: else:
@ -663,19 +910,23 @@ def create_crm_stages():
"Wardrobe / Storage Only", "Wardrobe / Storage Only",
"Flooring Only", "Flooring Only",
] ]
for ot in opp_types: for opp_type in opp_types:
if not exists("Opportunity Type", ot): if not exists("Opportunity Type", opp_type):
try: try:
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Opportunity Type", "doctype": "Opportunity Type",
"name": ot, "name": opp_type,
}) }
if safe_insert(d): ok(f"Opportunity Type: {ot}") )
else: skip(f"{ot} (dup)") if safe_insert(d):
except Exception as e: ok(f"Opportunity Type: {opp_type}")
skip(f"Opportunity Type {ot}: {e}")
else: else:
skip(f"Opportunity Type: {ot}") skip(f"{opp_type} (dup)")
except Exception as e:
skip(f"Opportunity Type {opp_type}: {e}")
else:
skip(f"Opportunity Type: {opp_type}")
frappe.db.commit() frappe.db.commit()
@ -706,7 +957,8 @@ def create_letter_head():
print("\n[14] Letter Head...") print("\n[14] Letter Head...")
lh_name = "Furnitex" lh_name = "Furnitex"
if not exists("Letter Head", lh_name): if not exists("Letter Head", lh_name):
d = frappe.get_doc({ d = frappe.get_doc(
{
"doctype": "Letter Head", "doctype": "Letter Head",
"letter_head_name": lh_name, "letter_head_name": lh_name,
"is_default": 1, "is_default": 1,
@ -734,9 +986,12 @@ def create_letter_head():
<div style="font-family: Arial, sans-serif; border-top: 1px solid #ccc; padding-top: 6px; font-size: 10px; color: #888; text-align: center;"> <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 &amp; Furniture | Kolkata | GSTIN: [Your GSTIN] | CIN: [If applicable] Furnitex | Interior Design &amp; Furniture | Kolkata | GSTIN: [Your GSTIN] | CIN: [If applicable]
</div>""", </div>""",
}) }
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: else:
skip(f"Letter Head: {lh_name}") skip(f"Letter Head: {lh_name}")
frappe.db.commit() frappe.db.commit()

View file

@ -39,6 +39,7 @@ def run():
# ── 1. Company record ───────────────────────────────────────────────────────── # ── 1. Company record ─────────────────────────────────────────────────────────
def update_company(): def update_company():
co = frappe.get_doc("Company", COMPANY) co = frappe.get_doc("Company", COMPANY)
co.phone_no = PHONE co.phone_no = PHONE
@ -52,12 +53,11 @@ def update_company():
# ── 2. Address record ───────────────────────────────────────────────────────── # ── 2. Address record ─────────────────────────────────────────────────────────
def update_address(): def update_address():
# Check if a Furnitex address already exists # Check if a Furnitex address already exists
existing = frappe.db.get_value( existing = frappe.db.get_value(
"Address", "Address", {"address_title": COMPANY, "address_type": "Billing"}, "name"
{"address_title": COMPANY, "address_type": "Billing"},
"name"
) )
addr_doc = { addr_doc = {
@ -72,10 +72,7 @@ def update_address():
"phone": PHONE, "phone": PHONE,
"email_id": EMAIL, "email_id": EMAIL,
"is_primary_address": 1, "is_primary_address": 1,
"links": [{ "links": [{"link_doctype": "Company", "link_name": COMPANY}],
"link_doctype": "Company",
"link_name": COMPANY
}]
} }
if existing: if existing:
@ -120,6 +117,7 @@ LETTER_HEAD_HTML = f"""
</div> </div>
""" """
def update_letter_head(): def update_letter_head():
lh_name = "Furnitex" lh_name = "Furnitex"
if frappe.db.exists("Letter Head", lh_name): if frappe.db.exists("Letter Head", lh_name):
@ -130,12 +128,14 @@ def update_letter_head():
doc.save() doc.save()
print(" ✓ Letter Head updated with real contact details") print(" ✓ Letter Head updated with real contact details")
else: else:
doc = frappe.get_doc({ doc = frappe.get_doc(
{
"doctype": "Letter Head", "doctype": "Letter Head",
"letter_head_name": lh_name, "letter_head_name": lh_name,
"content": LETTER_HEAD_HTML, "content": LETTER_HEAD_HTML,
"is_default": 1, "is_default": 1,
}) }
)
doc.flags.ignore_permissions = True doc.flags.ignore_permissions = True
doc.insert() doc.insert()
print(" ✓ Letter Head created") print(" ✓ Letter Head created")
@ -191,6 +191,7 @@ TC_MAP = {
"Furnitex - Purchase Order T&C": PO_TC, "Furnitex - Purchase Order T&C": PO_TC,
} }
def update_terms_conditions(): def update_terms_conditions():
for title, content in TC_MAP.items(): for title, content in TC_MAP.items():
if frappe.db.exists("Terms and Conditions", title): if frappe.db.exists("Terms and Conditions", title):
@ -200,14 +201,16 @@ def update_terms_conditions():
doc.save() doc.save()
print(f" ✓ Updated T&C: {title}") print(f" ✓ Updated T&C: {title}")
else: else:
doc = frappe.get_doc({ doc = frappe.get_doc(
{
"doctype": "Terms and Conditions", "doctype": "Terms and Conditions",
"title": title, "title": title,
"terms": content, "terms": content,
"selling": 1, "selling": 1,
"buying": 1, "buying": 1,
"hr": 0, "hr": 0,
}) }
)
doc.flags.ignore_permissions = True doc.flags.ignore_permissions = True
doc.insert() doc.insert()
print(f" ✓ Created T&C: {title}") print(f" ✓ Created T&C: {title}")