frappe_docker/setup_furnitex.py
SUBHANKAR DHAR c40e7992cb 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>
2026-06-16 15:41:54 +05:30

922 lines
32 KiB
Python

"""
Furnitex ERPNext v16 Setup Script
Run inside container via:
bench --site frontend execute setup_furnitex.run_all
Or directly:
python /home/frappe/setup_furnitex.py
"""
import frappe
import frappe.defaults
COMPANY = "Furnitex"
SITE = "frontend"
ABBR = None # resolved at runtime via get_abbr()
def get_abbr():
global ABBR
if not ABBR:
ABBR = frappe.db.get_value("Company", COMPANY, "abbr") or "F"
return ABBR
# ─────────────────────────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────────────────────────
def exists(doctype, name):
return frappe.db.exists(doctype, name)
def exists_filter(doctype, filters):
"""Existence check by filters (for docs where name includes company abbr)."""
return frappe.db.get_value(doctype, filters, "name")
def safe_insert(doc):
"""Insert and return True, or skip silently on duplicate and return False."""
try:
doc.flags.ignore_permissions = True
doc.insert()
return True
except frappe.DuplicateEntryError:
return False
except Exception as e:
if "Duplicate entry" in str(e):
return False
raise
def ok(msg):
print(f" [OK] {msg}")
def skip(msg):
print(f" [SKIP] {msg}")
def warn(msg):
print(f" [WARN] {msg}")
# ─────────────────────────────────────────────────────────────
# 1. UOMs
# ─────────────────────────────────────────────────────────────
def create_uoms():
print("\n[1/9] Creating UOMs...")
uoms = [
("SqFt", 0),
("Rft", 0),
("Bag", 1),
("Sheet", 1),
("Bundle", 1),
("Cubic Ft", 0),
]
for uom_name, whole in uoms:
if not exists("UOM", uom_name):
d = frappe.get_doc(
{
"doctype": "UOM",
"uom_name": uom_name,
"must_be_whole_number": whole,
}
)
d.flags.ignore_mandatory = True
if safe_insert(d):
ok(f"UOM: {uom_name}")
else:
skip(f"UOM: {uom_name} (duplicate)")
else:
skip(f"UOM: {uom_name}")
frappe.db.commit()
# ─────────────────────────────────────────────────────────────
# 2. ITEM GROUPS
# ─────────────────────────────────────────────────────────────
def create_item_groups():
print("\n[2/9] Creating Item Groups...")
groups = [
("Raw Materials - Furnitex", "All Item Groups"),
("Plywood & Board", "Raw Materials - Furnitex"),
("Laminates & Veneer", "Raw Materials - Furnitex"),
("Hardware & Fittings", "Raw Materials - Furnitex"),
("Civil & Surface Materials", "Raw Materials - Furnitex"),
("Execution Services", "All Item Groups"),
("Loose Furniture", "All Item Groups"),
]
for name, parent in groups:
if not exists("Item Group", name):
d = frappe.get_doc(
{
"doctype": "Item Group",
"item_group_name": name,
"parent_item_group": parent,
"is_group": 0,
}
)
if safe_insert(d):
ok(f"Item Group: {name}")
else:
skip(f"Item Group: {name} (duplicate)")
else:
skip(f"Item Group: {name}")
frappe.db.commit()
# ─────────────────────────────────────────────────────────────
# 3. SUPPLIER GROUPS
# ─────────────────────────────────────────────────────────────
def create_supplier_groups():
print("\n[3/9] Creating Supplier Groups...")
groups = [
("Local Market Vendor (Unregistered)", "All Supplier Groups"),
("GST Registered Vendor", "All Supplier Groups"),
("Labour Contractor", "All Supplier Groups"),
]
for name, parent in groups:
if not exists("Supplier Group", name):
d = frappe.get_doc(
{
"doctype": "Supplier Group",
"supplier_group_name": name,
"parent_supplier_group": parent,
}
)
if safe_insert(d):
ok(f"Supplier Group: {name}")
else:
skip(f"Supplier Group: {name} (duplicate)")
else:
skip(f"Supplier Group: {name}")
frappe.db.commit()
# ─────────────────────────────────────────────────────────────
# 4. WAREHOUSES
# ─────────────────────────────────────────────────────────────
def create_warehouses():
print("\n[4/9] Creating Warehouses...")
abbr = get_abbr()
# Find the company's root warehouse group
root_wh = frappe.db.get_value(
"Warehouse", {"company": COMPANY, "is_group": 1}, "name"
)
if not root_wh:
root_wh = f"All Warehouses - {abbr}"
warehouses = [
("Main Store", root_wh, 0),
("Rejected Stock", root_wh, 0),
]
for w_short, parent, is_group in warehouses:
# ERPNext appends company abbr: "Main Store - F"
w_full = f"{w_short} - {abbr}"
if not exists("Warehouse", w_full):
d = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": w_short,
"parent_warehouse": parent,
"company": COMPANY,
"is_group": is_group,
}
)
if safe_insert(d):
ok(f"Warehouse: {w_full}")
else:
skip(f"Warehouse: {w_full} (duplicate)")
else:
skip(f"Warehouse: {w_full}")
frappe.db.commit()
# ─────────────────────────────────────────────────────────────
# 5. TAX TEMPLATES
# ─────────────────────────────────────────────────────────────
def _find_account(account_name_fragment, root_type=None, account_type=None):
"""Find an account by partial name match under the company."""
filters = {"company": COMPANY, "is_group": 0}
if root_type:
filters["root_type"] = root_type
if account_type:
filters["account_type"] = account_type
# Try exact name match first
result = frappe.db.get_value(
"Account", dict(filters, account_name=account_name_fragment), "name"
)
if result:
return result
# Try LIKE match
like_pattern = f"%{account_name_fragment}%"
result = frappe.db.sql(
"""SELECT name FROM `tabAccount`
WHERE company=%s AND is_group=0
AND account_name LIKE %s
LIMIT 1""",
(COMPANY, like_pattern),
as_dict=0,
)
return result[0][0] if result else None
def create_tax_templates():
print("\n[5/9] Creating Tax Templates...")
# ERPNext stores tax templates as "{title} - {abbr}", so we check by title+company
# ── No GST (URD Purchase) ──
urd_title = "No GST - URD Purchase"
if not exists_filter(
"Purchase Taxes and Charges Template", {"title": urd_title, "company": COMPANY}
):
d = frappe.get_doc(
{
"doctype": "Purchase Taxes and Charges Template",
"title": urd_title,
"company": COMPANY,
"is_default": 0,
"taxes": [],
}
)
if safe_insert(d):
ok(f"Purchase Tax Template: {urd_title}")
else:
skip(f"Purchase Tax Template: {urd_title} (duplicate)")
else:
skip(f"Purchase Tax Template: {urd_title}")
# ── GST 18% Purchase ──
gst18_p_title = "GST 18% - Purchase"
if not exists_filter(
"Purchase Taxes and Charges Template",
{"title": gst18_p_title, "company": COMPANY},
):
cgst = _find_account("CGST")
sgst = _find_account("SGST")
taxes = []
if cgst:
taxes.append(
{
"charge_type": "On Net Total",
"account_head": cgst,
"rate": 9,
"description": "CGST @ 9%",
}
)
if sgst:
taxes.append(
{
"charge_type": "On Net Total",
"account_head": sgst,
"rate": 9,
"description": "SGST @ 9%",
}
)
d = frappe.get_doc(
{
"doctype": "Purchase Taxes and Charges Template",
"title": gst18_p_title,
"company": COMPANY,
"is_default": 0,
"taxes": taxes,
}
)
if safe_insert(d):
ok(
f"Purchase Tax Template: {gst18_p_title}"
+ (" (no GST accounts in CoA, left empty)" if not taxes else "")
)
else:
skip(f"Purchase Tax Template: {gst18_p_title} (duplicate)")
else:
skip(f"Purchase Tax Template: {gst18_p_title}")
# ── GST 18% Sales ──
gst18_s_title = "GST 18% - Sales"
if not exists_filter(
"Sales Taxes and Charges Template", {"title": gst18_s_title, "company": COMPANY}
):
cgst = _find_account("CGST")
sgst = _find_account("SGST")
taxes = []
if cgst:
taxes.append(
{
"charge_type": "On Net Total",
"account_head": cgst,
"rate": 9,
"description": "CGST @ 9%",
}
)
if sgst:
taxes.append(
{
"charge_type": "On Net Total",
"account_head": sgst,
"rate": 9,
"description": "SGST @ 9%",
}
)
d = frappe.get_doc(
{
"doctype": "Sales Taxes and Charges Template",
"title": gst18_s_title,
"company": COMPANY,
"is_default": 0,
"taxes": taxes,
}
)
if safe_insert(d):
ok(
f"Sales Tax Template: {gst18_s_title}"
+ (" (no GST accounts in CoA, left empty)" if not taxes else "")
)
else:
skip(f"Sales Tax Template: {gst18_s_title} (duplicate)")
else:
skip(f"Sales Tax Template: {gst18_s_title}")
frappe.db.commit()
# ─────────────────────────────────────────────────────────────
# 6. SERVICE ITEMS (Non-stock billing items)
# ─────────────────────────────────────────────────────────────
def create_service_items():
print("\n[6/9] Creating Service Items...")
income_acct = _find_account("Sales", root_type="Income") or _find_account(
"Service", root_type="Income"
)
items = [
# (code, name, uom, description)
(
"SVC-FC-EXEC",
"False Ceiling Execution",
"SqFt",
"Labour + material for false ceiling. Bill per SqFt OR as lumpsum.",
),
(
"SVC-WAR-LAM",
"Laminate Wardrobe Fabrication",
"SqFt",
"Laminate wardrobe fabrication per SqFt.",
),
(
"SVC-KIT-EXEC",
"Modular Kitchen Execution",
"SqFt",
"Modular kitchen fabrication and installation.",
),
(
"SVC-FLOOR",
"Flooring Execution",
"SqFt",
"Vinyl/Wood/Tile flooring supply and installation.",
),
(
"SVC-LUMP",
"Lumpsum Contract Work",
"Nos",
"Fixed-price lumpsum milestone. Change qty to 1 always.",
),
(
"SVC-DESIGN",
"Interior Design Consultation",
"Nos",
"Design + drawing fee — lumpsum.",
),
(
"SVC-CONVEY",
"Site Conveyance & Transport",
"Nos",
"Transport charges to/from site.",
),
("SVC-LABOUR", "Direct Site Labour", "Nos", "Daily-wage labour charges."),
(
"SVC-ELECTRIC",
"Electrical Work Execution",
"Nos",
"Electrical points, wiring, fitting.",
),
(
"SVC-PAINT",
"Painting & Polish Execution",
"SqFt",
"Wall painting / wood polish per SqFt.",
),
]
for code, name, uom, desc in items:
if not exists("Item", code):
# Non-stock service items: no warehouse needed in item_defaults
d = frappe.get_doc(
{
"doctype": "Item",
"item_code": code,
"item_name": name,
"item_group": "Execution Services",
"description": desc,
"stock_uom": uom,
"sales_uom": uom,
"is_stock_item": 0,
"is_purchase_item": 0,
"is_sales_item": 1,
"standard_rate": 0,
# No item_defaults row — avoids warehouse company-mismatch validation
}
)
if safe_insert(d):
ok(f"Service Item: {code} [{uom}]")
else:
skip(f"Service Item: {code} (duplicate)")
else:
skip(f"Service Item: {code}")
frappe.db.commit()
# ─────────────────────────────────────────────────────────────
# 7. RAW MATERIAL ITEMS (Stock items)
# ─────────────────────────────────────────────────────────────
def create_raw_material_items():
print("\n[7/9] Creating Raw Material Items...")
cogs_acct = (
_find_account("Cost of Goods Sold", root_type="Expense")
or _find_account("Stock Expenses", root_type="Expense")
or _find_account("Expenses Included", root_type="Expense")
)
default_wh = f"Main Store - {get_abbr()}"
if not exists("Warehouse", default_wh):
default_wh = frappe.db.get_value(
"Warehouse", {"company": COMPANY, "is_group": 0}, "name"
)
items = [
# (code, name, uom, group)
("RM-PLY-19MM", "Plywood 19mm BWR 8x4", "Sheet", "Plywood & Board"),
("RM-PLY-12MM", "Plywood 12mm BWR 8x4", "Sheet", "Plywood & Board"),
("RM-PLY-6MM", "Plywood 6mm 8x4", "Sheet", "Plywood & Board"),
("RM-MDF-18MM", "MDF Board 18mm 8x4", "Sheet", "Plywood & Board"),
("RM-HDF-3MM", "HDF 3mm 8x4", "Sheet", "Plywood & Board"),
("RM-LAM-1MM", "Laminate Sheet 1mm 8x4", "Sheet", "Laminates & Veneer"),
("RM-VEN-NAT", "Veneer Sheet Natural Wood", "Sheet", "Laminates & Veneer"),
("RM-LAM-ACRY", "Acrylic Laminate Sheet", "Sheet", "Laminates & Veneer"),
("RM-HW-HINGE", "Concealed Hinge (pair)", "Nos", "Hardware & Fittings"),
("RM-HW-CHAN18", "Drawer Channel 18 inch", "Nos", "Hardware & Fittings"),
("RM-HW-CHAN24", "Drawer Channel 24 inch", "Nos", "Hardware & Fittings"),
("RM-HW-HANDLE", "Cabinet Handle", "Nos", "Hardware & Fittings"),
("RM-HW-LOCK", "Drawer Lock", "Nos", "Hardware & Fittings"),
("RM-HW-SCREW", "Wood Screw Assorted", "Bundle", "Hardware & Fittings"),
("RM-CIV-CEM", "OPC Cement 53 Grade", "Bag", "Civil & Surface Materials"),
("RM-CIV-PUTTY", "Wall Putty White", "Bag", "Civil & Surface Materials"),
("RM-CIV-PRIMER", "Primer Interior", "Nos", "Civil & Surface Materials"),
(
"RM-CIV-GYPS",
"Gypsum Board 8x4 12.5mm",
"Sheet",
"Civil & Surface Materials",
),
("RM-CIV-GYPS-C", "Gypsum Cornice / Grid", "Rft", "Civil & Surface Materials"),
]
for code, name, uom, group in items:
if not exists("Item", code):
d_dict = {
"doctype": "Item",
"item_code": code,
"item_name": name,
"item_group": group,
"stock_uom": uom,
"is_stock_item": 1,
"is_purchase_item": 1,
"is_sales_item": 0,
"valuation_method": "FIFO",
}
# Only add item_defaults if we have a valid Furnitex warehouse
if default_wh or cogs_acct:
defaults = {"company": COMPANY}
if default_wh:
defaults["default_warehouse"] = default_wh
if cogs_acct:
defaults["expense_account"] = cogs_acct
d_dict["item_defaults"] = [defaults]
d = frappe.get_doc(d_dict)
if safe_insert(d):
ok(f"Raw Material: {code} [{uom}]")
else:
skip(f"Raw Material: {code} (duplicate)")
else:
skip(f"Raw Material: {code}")
frappe.db.commit()
# ─────────────────────────────────────────────────────────────
# 8. SAMPLE SUPPLIERS
# ─────────────────────────────────────────────────────────────
def create_suppliers():
print("\n[8/9] Creating Sample Suppliers...")
suppliers = [
# (name, group, gst_category)
(
"Local Hardware Market - Kolkata",
"Local Market Vendor (Unregistered)",
"Unregistered",
),
(
"Burrabazar Plywood Supplier",
"Local Market Vendor (Unregistered)",
"Unregistered",
),
(
"Fancy Laminates - Howrah",
"Local Market Vendor (Unregistered)",
"Unregistered",
),
(
"Modern Furniture Hardware - BBD Bag",
"Local Market Vendor (Unregistered)",
"Unregistered",
),
(
"Registered Hardware Supplier Ltd",
"GST Registered Vendor",
"Registered Regular",
),
("Site Labour Contractor - Ramesh", "Labour Contractor", "Unregistered"),
]
for name, group, gst_cat in suppliers:
if not exists("Supplier", name):
d = frappe.get_doc(
{
"doctype": "Supplier",
"supplier_name": name,
"supplier_group": group,
"country": "India",
"gst_category": gst_cat,
"default_currency": "INR",
}
)
if safe_insert(d):
ok(f"Supplier: {name} [{gst_cat}]")
else:
skip(f"Supplier: {name} (duplicate)")
else:
skip(f"Supplier: {name}")
frappe.db.commit()
# ERPNext v16 removed the per-supplier default tax template field.
# URD tax bypass is handled instead by the server script:
# "Furnitex - Clear GST on URD Purchase" (Before Save on Purchase Invoice)
# Tick the "URD Purchase (No GST)" checkbox on any invoice to auto-clear taxes.
urd_suppliers_count = frappe.db.count(
"Supplier", {"supplier_group": "Local Market Vendor (Unregistered)"}
)
ok(f"URD tax via server script — {urd_suppliers_count} URD suppliers registered")
# ─────────────────────────────────────────────────────────────
# 9. CUSTOM FIELDS
# ─────────────────────────────────────────────────────────────
def create_custom_fields():
print("\n[9/9] Creating Custom Fields...")
# NOTE: Purchase Invoice, Purchase Order, Stock Entry, and Delivery Note
# already have a native 'project' field in ERPNext v16 — skip those.
# We only add:
# 1. is_urd_purchase (Check) on Purchase Invoice
# 2. project (Link) on Journal Entry (only one missing it)
# (dt, fieldname, label, fieldtype, options, insert_after, in_list_view)
fields = [
(
"Purchase Invoice",
"is_urd_purchase",
"URD Purchase (No GST)",
"Check",
None,
"supplier",
1,
),
("Journal Entry", "project", "Project", "Link", "Project", "voucher_type", 0),
]
for dt, fn, label, ft, opts, after, in_list in fields:
cf_name = f"{dt}-{fn}"
if not exists("Custom Field", cf_name):
d_dict = {
"doctype": "Custom Field",
"dt": dt,
"fieldname": fn,
"label": label,
"fieldtype": ft,
"insert_after": after,
"in_list_view": in_list,
"in_standard_filter": 1,
"search_index": 1,
}
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"Custom Field: {dt}.{fn} (duplicate)")
else:
skip(f"Custom Field: {dt}.{fn}")
frappe.db.commit()
ok(
"Native 'project' field already present on PI, PO, Stock Entry, DN — no custom fields needed there"
)
# ─────────────────────────────────────────────────────────────
# 10. SERVER SCRIPTS (Automation)
# ─────────────────────────────────────────────────────────────
def create_server_scripts():
print("\n[+] Creating Server Scripts...")
# Script 1: Auto-create OnSite WIP Warehouse on Project insert
wip_script_name = "Furnitex - Auto Create OnSite WIP Warehouse"
wip_script_code = """
# Auto-fires after a new Project is saved
# Creates "{Project Name} - OnSite WIP" warehouse automatically
project_name = doc.project_name or doc.name
company = doc.company or "Furnitex"
abbr = frappe.db.get_value("Company", company, "abbr") or "F"
# OnSite WIP name uses company abbr so ERPNext accepts it
wh_short_name = project_name + " - OnSite WIP"
warehouse_name = wh_short_name + " - " + abbr
# Find root warehouse group for this company
root_wh = frappe.db.get_value(
"Warehouse",
{"company": company, "is_group": 1},
"name"
) or ("All Warehouses - " + abbr)
if not frappe.db.exists("Warehouse", warehouse_name):
wh = frappe.get_doc({
"doctype": "Warehouse",
"warehouse_name": wh_short_name,
"parent_warehouse": root_wh,
"company": company,
"is_group": 0,
})
wh.flags.ignore_permissions = True
wh.insert()
# No frappe.db.commit() — Frappe manages the transaction in server scripts
frappe.msgprint(
"OnSite WIP Warehouse created: <b>" + warehouse_name + "</b>",
alert=True, indicator="green"
)
"""
if not exists("Server Script", wip_script_name):
d = frappe.get_doc(
{
"doctype": "Server Script",
"name": wip_script_name,
"script_type": "DocType Event",
"reference_doctype": "Project",
"doctype_event": "After Insert",
"enabled": 1,
"script": wip_script_code,
}
)
d.flags.ignore_permissions = True
d.insert()
ok(f"Server Script: {wip_script_name}")
else:
skip(f"Server Script: {wip_script_name}")
# Script 2: Auto-clear taxes on URD Purchase Invoice (Before Save)
urd_script_name = "Furnitex - Clear GST on URD Purchase"
urd_script_code = """
# Fires Before Save on Purchase Invoice
# If flagged as URD Purchase, wipes all tax rows and expense accounts = COGS
if doc.is_urd_purchase:
doc.taxes = []
doc.taxes_and_charges = ""
cogs_acct = frappe.db.get_value(
"Account",
{"account_name": "Cost of Goods Sold",
"company": doc.company, "is_group": 0},
"name"
)
if cogs_acct:
for item in doc.items:
item.expense_account = cogs_acct
"""
if not exists("Server Script", urd_script_name):
d = frappe.get_doc(
{
"doctype": "Server Script",
"name": urd_script_name,
"script_type": "DocType Event",
"reference_doctype": "Purchase Invoice",
"doctype_event": "Before Save",
"enabled": 1,
"script": urd_script_code,
}
)
d.flags.ignore_permissions = True
d.insert()
ok(f"Server Script: {urd_script_name}")
else:
skip(f"Server Script: {urd_script_name}")
frappe.db.commit()
# ─────────────────────────────────────────────────────────────
# 11. PROJECT PROFITABILITY CUSTOM REPORT (Page Script)
# ─────────────────────────────────────────────────────────────
PROFIT_REPORT_SCRIPT = '''
import frappe
from frappe import _
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 160},
{"label": _("Customer"), "fieldname": "customer", "fieldtype": "Data", "width": 160},
{"label": _("Revenue Billed (₹)"), "fieldname": "revenue", "fieldtype": "Currency", "width": 130},
{"label": _("Raw Mat. Cost (₹)"), "fieldname": "raw_mat_cost", "fieldtype": "Currency", "width": 130},
{"label": _("Mat. Consumed (₹)"), "fieldname": "mat_consumed", "fieldtype": "Currency", "width": 130},
{"label": _("Labour/Conv. (₹)"), "fieldname": "labour_cost", "fieldtype": "Currency", "width": 130},
{"label": _("Total Cost (₹)"), "fieldname": "total_cost", "fieldtype": "Currency", "width": 130},
{"label": _("Net Margin (₹)"), "fieldname": "net_margin", "fieldtype": "Currency", "width": 130},
{"label": _("Margin %"), "fieldname": "margin_pct", "fieldtype": "Percent", "width": 90},
]
def get_data(filters):
company = filters.get("company") if filters else None
project_filter = filters.get("project") if filters else None
proj_sql = ""
args = []
if company:
proj_sql += " AND p.company = %s"
args.append(company)
if project_filter:
proj_sql += " AND p.name = %s"
args.append(project_filter)
projects = frappe.db.sql(
f"SELECT name, project_name, customer FROM `tabProject` WHERE 1=1 {proj_sql}",
args, as_dict=1
)
rows = []
for p in projects:
pname = p.name
revenue = (frappe.db.sql(
"SELECT COALESCE(SUM(base_grand_total),0) v FROM `tabSales Invoice` WHERE project=%s AND docstatus=1",
pname, as_dict=1)[0].v or 0)
raw_mat = (frappe.db.sql(
"SELECT COALESCE(SUM(base_net_total),0) v FROM `tabPurchase Invoice` WHERE project=%s AND docstatus=1",
pname, as_dict=1)[0].v or 0)
consumed = (frappe.db.sql(
"""SELECT COALESCE(SUM(sed.amount),0) v
FROM `tabStock Entry Detail` sed
JOIN `tabStock Entry` se ON se.name=sed.parent
WHERE se.project=%s AND se.stock_entry_type='Material Issue' AND se.docstatus=1""",
pname, as_dict=1)[0].v or 0)
labour = (frappe.db.sql(
"""SELECT COALESCE(SUM(jvd.debit),0) v
FROM `tabJournal Entry Account` jvd
JOIN `tabJournal Entry` jv ON jv.name=jvd.parent
WHERE jv.project=%s AND jv.docstatus=1
AND jvd.account LIKE '%Labour%'""",
pname, as_dict=1)[0].v or 0)
total_cost = raw_mat + consumed + labour
net_margin = revenue - total_cost
margin_pct = round((net_margin / revenue * 100), 2) if revenue else 0
rows.append({
"project": pname,
"customer": p.customer,
"revenue": revenue,
"raw_mat_cost":raw_mat,
"mat_consumed":consumed,
"labour_cost": labour,
"total_cost": total_cost,
"net_margin": net_margin,
"margin_pct": margin_pct,
})
rows.sort(key=lambda r: r["net_margin"])
return rows
'''
def create_custom_report():
print("\n[+] Creating Custom Report: Furnitex Project Profitability...")
report_name = "Furnitex Project Profitability"
if not exists("Report", report_name):
d = frappe.get_doc(
{
"doctype": "Report",
"report_name": report_name,
"ref_doctype": "Project",
"report_type": "Script Report",
"is_standard": "No",
"module": "Projects",
"script": PROFIT_REPORT_SCRIPT,
}
)
d.flags.ignore_permissions = True
d.insert()
ok(f"Custom Report: {report_name}")
frappe.db.commit()
else:
skip(f"Custom Report: {report_name}")
# ─────────────────────────────────────────────────────────────
# MASTER RUNNER
# ─────────────────────────────────────────────────────────────
def run_all():
frappe.set_user("Administrator")
print("\n" + "=" * 58)
print(" FURNITEX ERPNEXT v16 — AUTOMATED SETUP")
print(" Company: Furnitex | India | INR")
print("=" * 58)
create_uoms()
create_item_groups()
create_supplier_groups()
create_warehouses()
create_tax_templates()
create_service_items()
create_raw_material_items()
create_suppliers()
create_custom_fields()
create_server_scripts()
create_custom_report()
frappe.clear_cache()
print("\n" + "=" * 58)
print(" SETUP COMPLETE — refresh your browser")
print("=" * 58 + "\n")
if __name__ == "__main__":
import sys
frappe.init(site="frontend")
frappe.connect()
run_all()
frappe.destroy()