mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-17 13:55:08 +00:00
765 lines
31 KiB
Python
765 lines
31 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()
|
|
frappe.db.commit()
|
|
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()
|