This commit is contained in:
SUBHANKAR DHAR 2026-06-16 10:13:47 +00:00 committed by GitHub
commit e07012e3a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 2472 additions and 0 deletions

244
delete_streetwok.py Normal file
View file

@ -0,0 +1,244 @@
"""
delete_streetwok.py
Removes all "streetwok (Demo)" company data from Furnitex ERPNext instance.
Run via: bench --site frontend execute frappe.delete_streetwok.run
"""
import frappe
COMPANY = "streetwok (Demo)"
def _sql(q, commit=False):
frappe.db.sql(q)
if commit:
frappe.db.commit()
def run():
frappe.set_user("Administrator")
if not frappe.db.exists("Company", COMPANY):
print(f" Company '{COMPANY}' not found — nothing to delete.")
return
print(f"\n{'='*54}")
print(f" DELETING: {COMPANY}")
print(f"{'='*54}\n")
# ── STEP 1: Cancel + delete all submitted documents ──────────
submitted_doctypes = [
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
"Stock Entry",
"Delivery Note",
"Purchase Receipt",
"Sales Order",
"Purchase Order",
"Quotation",
"Material Request",
"Stock Reconciliation",
]
for dt in submitted_doctypes:
# Get submitted docs for this company
docs = frappe.db.sql(
f"SELECT name FROM `tab{dt}` WHERE company=%s AND docstatus=1",
COMPANY,
as_dict=1,
)
if docs:
print(f" Cancelling {len(docs)} submitted {dt}(s)...")
for d in docs:
try:
doc = frappe.get_doc(dt, d.name)
doc.flags.ignore_permissions = True
doc.flags.ignore_links = True
doc.cancel()
frappe.db.commit()
except Exception as e:
# Force cancel via direct DB update if normal cancel fails
frappe.db.sql(
f"UPDATE `tab{dt}` SET docstatus=2 WHERE name=%s", d.name
)
frappe.db.commit()
# ── STEP 2: Delete all draft + cancelled docs ─────────────────
all_doctypes = submitted_doctypes + [
"Landed Cost Voucher",
"Asset",
"Salary Slip",
"Timesheet",
]
for dt in all_doctypes:
try:
count = frappe.db.count(dt, {"company": COMPANY})
if count:
frappe.db.delete(dt, {"company": COMPANY})
frappe.db.commit()
print(f" Deleted {count} {dt}(s)")
except Exception as e:
print(f" [WARN] Could not delete {dt}: {e}")
# ── STEP 3: Delete GL Entries ─────────────────────────────────
gl_count = frappe.db.count("GL Entry", {"company": COMPANY})
if gl_count:
frappe.db.delete("GL Entry", {"company": COMPANY})
frappe.db.commit()
print(f" Deleted {gl_count} GL Entries")
# ── STEP 4: Delete Stock Ledger Entries ───────────────────────
sle_count = frappe.db.count("Stock Ledger Entry", {"company": COMPANY})
if sle_count:
frappe.db.delete("Stock Ledger Entry", {"company": COMPANY})
frappe.db.commit()
print(f" Deleted {sle_count} Stock Ledger Entries")
# ── STEP 5: Delete child tables that reference the company ────
child_cleanups = [
("Sales Invoice Item", "company"),
("Purchase Invoice Item", "company"),
("Payment Entry Reference", None), # handled via parent delete
]
# ── STEP 6: Delete Warehouses ─────────────────────────────────
# Disable stock bins first
wh_list = frappe.db.sql(
"SELECT name FROM `tabWarehouse` WHERE company=%s", COMPANY, as_dict=1
)
if wh_list:
for wh in wh_list:
frappe.db.delete("Bin", {"warehouse": wh.name})
frappe.db.commit()
for wh in wh_list:
try:
frappe.delete_doc(
"Warehouse",
wh.name,
ignore_permissions=True,
force=True,
ignore_on_trash=True,
)
except Exception as e:
frappe.db.sql("DELETE FROM `tabWarehouse` WHERE name=%s", wh.name)
frappe.db.commit()
print(f" Deleted {len(wh_list)} Warehouse(s)")
# ── STEP 7: Delete Cost Centers ───────────────────────────────
cc_list = frappe.db.sql(
"SELECT name FROM `tabCost Center` WHERE company=%s ORDER BY lft DESC",
COMPANY,
as_dict=1,
)
if cc_list:
for cc in cc_list:
try:
frappe.db.sql("DELETE FROM `tabCost Center` WHERE name=%s", cc.name)
except Exception:
pass
frappe.db.commit()
print(f" Deleted {len(cc_list)} Cost Center(s)")
# ── STEP 8: Delete Accounts ───────────────────────────────────
acct_count = frappe.db.count("Account", {"company": COMPANY})
if acct_count:
# Delete leaf accounts first (is_group=0), then groups
frappe.db.sql(
"DELETE FROM `tabAccount` WHERE company=%s AND is_group=0", COMPANY
)
frappe.db.sql("DELETE FROM `tabAccount` WHERE company=%s", COMPANY)
frappe.db.commit()
print(f" Deleted {acct_count} Account(s)")
# ── STEP 9: Delete Fiscal Years linked only to this company ───
fy_links = frappe.db.sql(
"""SELECT parent FROM `tabFiscal Year Company`
WHERE company=%s""",
COMPANY,
as_dict=1,
)
for fy in fy_links:
frappe.db.sql(
"DELETE FROM `tabFiscal Year Company` WHERE company=%s AND parent=%s",
(COMPANY, fy.parent),
)
# If this fiscal year has no other company links, delete it too
remaining = frappe.db.count("Fiscal Year Company", {"parent": fy.parent})
if remaining == 0:
try:
frappe.db.delete("Fiscal Year", {"name": fy.parent})
except Exception:
pass
frappe.db.commit()
# ── STEP 10: Nuke Customers/Suppliers that belong only to ─────
# streetwok (no transactions in Furnitex)
# Only delete if they have no Furnitex transactions
stale_customers = frappe.db.sql(
"""SELECT c.name FROM `tabCustomer` c
WHERE NOT EXISTS (
SELECT 1 FROM `tabSales Invoice`
WHERE customer=c.name AND company='Furnitex' AND docstatus < 2
)
AND NOT EXISTS (
SELECT 1 FROM `tabSales Order`
WHERE customer=c.name AND company='Furnitex' AND docstatus < 2
)""",
as_dict=1,
)
if stale_customers:
for c in stale_customers:
try:
frappe.db.delete("Customer", {"name": c.name})
except Exception:
pass
frappe.db.commit()
print(f" Deleted {len(stale_customers)} orphan Customer(s)")
# ── STEP 11: Delete the Company record itself ─────────────────
try:
frappe.delete_doc(
"Company",
COMPANY,
ignore_permissions=True,
force=True,
ignore_on_trash=True,
)
except Exception:
frappe.db.sql("DELETE FROM `tabCompany` WHERE name=%s", COMPANY)
frappe.db.commit()
print(f"\n Company '{COMPANY}' deleted.")
# ── STEP 12: Update Default Company if it was streetwok ───────
default_co = frappe.db.get_default("company")
if default_co and "streetwok" in default_co.lower():
frappe.db.set_default("company", "Furnitex")
frappe.db.commit()
print(" Default company reset to: Furnitex")
# ── STEP 13: Nuke any leftover streetwok items ────────────────
stale_items = frappe.db.sql(
"""SELECT name FROM `tabItem`
WHERE item_name LIKE '%streetwok%'
OR item_code LIKE '%streetwok%'
OR description LIKE '%streetwok%'""",
as_dict=1,
)
if stale_items:
for item in stale_items:
try:
frappe.db.delete("Item", {"name": item.name})
except Exception:
pass
frappe.db.commit()
print(f" Deleted {len(stale_items)} streetwok-tagged Item(s)")
frappe.clear_cache()
print(f"\n{'='*54}")
print(" DONE — streetwok removed. Refresh your browser.")
print(f"{'='*54}\n")

View file

@ -0,0 +1,61 @@
"""
fix_server_script_commit.py
Removes frappe.db.commit() from the OnSite WIP warehouse server script.
In DocType Event server scripts, Frappe manages the transaction automatically
calling frappe.db.commit() inside the script throws AttributeError in the sandbox.
Run via: bench --site frontend execute frappe.fix_server_script_commit.run
"""
import frappe
SCRIPT_NAME = "Furnitex - Auto Create OnSite WIP Warehouse"
FIXED_SCRIPT = """\
# 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"
wh_short_name = project_name + " - OnSite WIP"
warehouse_name = wh_short_name + " - " + abbr
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()
# NOTE: no frappe.db.commit() here — Frappe handles the transaction
frappe.msgprint(
"OnSite WIP Warehouse created: <b>" + warehouse_name + "</b>",
alert=True, indicator="green"
)
"""
def run():
frappe.set_user("Administrator")
if not frappe.db.exists("Server Script", SCRIPT_NAME):
print(f" [WARN] Server Script '{SCRIPT_NAME}' not found — nothing to fix.")
return
doc = frappe.get_doc("Server Script", SCRIPT_NAME)
doc.script = FIXED_SCRIPT
doc.flags.ignore_permissions = True
doc.save()
frappe.db.commit()
print(f" [OK] Removed frappe.db.commit() from: {SCRIPT_NAME}")
print(" Warehouse auto-creation will now work without AttributeError.")

922
setup_furnitex.py Normal file
View file

@ -0,0 +1,922 @@
"""
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()

File diff suppressed because it is too large Load diff

216
update_furnitex_info.py Normal file
View file

@ -0,0 +1,216 @@
"""
update_furnitex_info.py
Updates ERPNext with real Furnitex business details from furnitex.co.in
Run via: bench --site frontend execute frappe.update_furnitex_info.run
"""
import frappe
COMPANY = "Furnitex"
ADDRESS = "6/A/108 Mukundapur"
CITY = "Kolkata"
STATE = "West Bengal"
PINCODE = "700099"
COUNTRY = "India"
PHONE = "+91 62905 91422"
EMAIL = "info.furnitex@gmail.com"
WEBSITE = "https://furnitex.co.in"
INSTAGRAM = "https://www.instagram.com/frunitex"
LEGAL_NAME = "Furnitex Atelier Pvt. Ltd."
TAGLINE = "Redefine What Surrounds You"
FULL_ADDR = f"{ADDRESS}, {CITY} - {PINCODE}, {STATE}, {COUNTRY}"
REP_NAME = "Subhankar Dhar"
OFFICE_HOURS = "Monday Saturday, 10:00 AM 7:00 PM (India Time)"
def run():
frappe.set_user("Administrator")
update_company()
update_address()
update_letter_head()
update_terms_conditions()
frappe.db.commit()
frappe.clear_cache()
print("\n✓ Furnitex business info updated successfully.\n")
# ── 1. Company record ─────────────────────────────────────────────────────────
def update_company():
co = frappe.get_doc("Company", COMPANY)
co.phone_no = PHONE
co.email = EMAIL
co.website = WEBSITE
co.company_name = COMPANY # keep short name as primary
co.flags.ignore_permissions = True
co.save()
print(f" ✓ Company record updated: phone={PHONE}, email={EMAIL}")
# ── 2. Address record ─────────────────────────────────────────────────────────
def update_address():
# Check if a Furnitex address already exists
existing = frappe.db.get_value(
"Address", {"address_title": COMPANY, "address_type": "Billing"}, "name"
)
addr_doc = {
"doctype": "Address",
"address_title": COMPANY,
"address_type": "Billing",
"address_line1": ADDRESS,
"city": CITY,
"state": STATE,
"pincode": PINCODE,
"country": COUNTRY,
"phone": PHONE,
"email_id": EMAIL,
"is_primary_address": 1,
"links": [{"link_doctype": "Company", "link_name": COMPANY}],
}
if existing:
doc = frappe.get_doc("Address", existing)
doc.update(addr_doc)
doc.flags.ignore_permissions = True
doc.save()
print(f" ✓ Address updated: {FULL_ADDR}")
else:
doc = frappe.get_doc(addr_doc)
doc.flags.ignore_permissions = True
doc.insert()
print(f" ✓ Address created: {FULL_ADDR}")
# ── 3. Letter Head ────────────────────────────────────────────────────────────
LETTER_HEAD_HTML = f"""
<div style="font-family:'Segoe UI',Arial,sans-serif; padding:0; margin:0;">
<table width="100%" style="border-bottom:2px solid #1a1a1a; padding-bottom:14px; margin-bottom:8px;">
<tr>
<td style="vertical-align:top; width:60%;">
<div style="font-size:26px; font-weight:700; letter-spacing:3px; color:#1a1a1a; text-transform:uppercase;">
FURNITEX
</div>
<div style="font-size:10px; letter-spacing:2px; color:#555; text-transform:uppercase; margin-top:2px;">
{TAGLINE}
</div>
<div style="font-size:9px; color:#888; margin-top:4px;">
{LEGAL_NAME}
</div>
</td>
<td style="vertical-align:top; text-align:right; width:40%; font-size:9.5px; color:#444; line-height:1.7;">
<div>{ADDRESS}</div>
<div>{CITY} - {PINCODE}, {STATE}</div>
<div>Phone: {PHONE}</div>
<div>Email: {EMAIL}</div>
<div>Web: furnitex.co.in</div>
</td>
</tr>
</table>
</div>
"""
def update_letter_head():
lh_name = "Furnitex"
if frappe.db.exists("Letter Head", lh_name):
doc = frappe.get_doc("Letter Head", lh_name)
doc.content = LETTER_HEAD_HTML
doc.is_default = 1
doc.flags.ignore_permissions = True
doc.save()
print(" ✓ Letter Head updated with real contact details")
else:
doc = frappe.get_doc(
{
"doctype": "Letter Head",
"letter_head_name": lh_name,
"content": LETTER_HEAD_HTML,
"is_default": 1,
}
)
doc.flags.ignore_permissions = True
doc.insert()
print(" ✓ Letter Head created")
# ── 4. Terms & Conditions ─────────────────────────────────────────────────────
QUOTATION_TC = f"""<div style="font-size:10.5px; line-height:1.8; color:#333;">
<strong>FURNITEX Quotation Terms &amp; Conditions</strong><br><br>
1. <strong>Validity:</strong> This quotation is valid for 15 days from the date of issue.<br>
2. <strong>Payment Terms:</strong> 30% advance on order confirmation, 30% at 50% completion, 30% at completion, 10% at handover.<br>
3. <strong>Delivery:</strong> Timelines are estimated and subject to site readiness and material availability. Furnitex is not liable for delays caused by site conditions.<br>
4. <strong>Design Changes:</strong> Any changes post order confirmation may attract additional charges and revised timelines.<br>
5. <strong>Material:</strong> All materials as specified. Substitutions may be made with equivalent or superior alternatives with prior intimation.<br>
6. <strong>Site Access:</strong> Client to ensure uninterrupted site access during agreed working hours: {OFFICE_HOURS}.<br>
7. <strong>Warranty:</strong> 1-year manufacturing warranty on all Furnitex-fabricated items. Hardware and third-party products carry manufacturer warranty.<br>
8. <strong>Dispute Resolution:</strong> All disputes subject to Kolkata jurisdiction.<br><br>
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
</div>"""
INVOICE_TC = f"""<div style="font-size:10.5px; line-height:1.8; color:#333;">
<strong>FURNITEX Invoice Terms &amp; Conditions</strong><br><br>
1. <strong>Payment Due:</strong> Payment is due within 7 days of invoice date unless otherwise agreed in writing.<br>
2. <strong>Late Payment:</strong> Overdue amounts attract interest at 18% per annum.<br>
3. <strong>GST:</strong> GST as applicable under Indian law is charged additionally where mentioned.<br>
4. <strong>Delivery &amp; Installation:</strong> Goods remain property of Furnitex until full payment is received.<br>
5. <strong>Returns:</strong> Custom-manufactured furniture is non-returnable. Defects to be reported within 48 hours of delivery.<br>
6. <strong>Warranty:</strong> 1-year manufacturing defect warranty. Normal wear, misuse, or site-caused damage not covered.<br>
7. <strong>Disputes:</strong> Subject to Kolkata jurisdiction.<br><br>
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
</div>"""
PO_TC = f"""<div style="font-size:10.5px; line-height:1.8; color:#333;">
<strong>FURNITEX Purchase Order Terms &amp; Conditions</strong><br><br>
1. <strong>Acceptance:</strong> Supply of goods/services against this PO constitutes acceptance of these terms.<br>
2. <strong>Quality:</strong> All materials must conform to the specifications mentioned. Substandard materials will be rejected at supplier's cost.<br>
3. <strong>Delivery:</strong> Deliver to the address specified on the PO by the agreed date. Delays must be communicated 48 hours in advance.<br>
4. <strong>Invoice:</strong> Raise GST-compliant invoice (or cash memo for URD) with PO reference. Payment processed within 7 days of invoice receipt and material acceptance.<br>
5. <strong>Warranty:</strong> Supplier warrants materials against defects for minimum 6 months from delivery.<br>
6. <strong>Jurisdiction:</strong> Kolkata courts have exclusive jurisdiction.<br><br>
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
</div>"""
TC_MAP = {
"Furnitex - Quotation T&C": QUOTATION_TC,
"Furnitex - Invoice T&C": INVOICE_TC,
"Furnitex - Purchase Order T&C": PO_TC,
}
def update_terms_conditions():
for title, content in TC_MAP.items():
if frappe.db.exists("Terms and Conditions", title):
doc = frappe.get_doc("Terms and Conditions", title)
doc.terms = content
doc.flags.ignore_permissions = True
doc.save()
print(f" ✓ Updated T&C: {title}")
else:
doc = frappe.get_doc(
{
"doctype": "Terms and Conditions",
"title": title,
"terms": content,
"selling": 1,
"buying": 1,
"hr": 0,
}
)
doc.flags.ignore_permissions = True
doc.insert()
print(f" ✓ Created T&C: {title}")