mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-17 13:55:08 +00:00
Merge 6b3be33e9f into 91fc59a134
This commit is contained in:
commit
e07012e3a0
5 changed files with 2472 additions and 0 deletions
244
delete_streetwok.py
Normal file
244
delete_streetwok.py
Normal 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")
|
||||
61
fix_server_script_commit.py
Normal file
61
fix_server_script_commit.py
Normal 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
922
setup_furnitex.py
Normal 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()
|
||||
1029
setup_furnitex_crm_billing.py
Normal file
1029
setup_furnitex_crm_billing.py
Normal file
File diff suppressed because it is too large
Load diff
216
update_furnitex_info.py
Normal file
216
update_furnitex_info.py
Normal 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 & Conditions</strong><br><br>
|
||||
|
||||
1. <strong>Validity:</strong> This quotation is valid for 15 days from the date of issue.<br>
|
||||
2. <strong>Payment Terms:</strong> 30% advance on order confirmation, 30% at 50% completion, 30% at completion, 10% at handover.<br>
|
||||
3. <strong>Delivery:</strong> Timelines are estimated and subject to site readiness and material availability. Furnitex is not liable for delays caused by site conditions.<br>
|
||||
4. <strong>Design Changes:</strong> Any changes post order confirmation may attract additional charges and revised timelines.<br>
|
||||
5. <strong>Material:</strong> All materials as specified. Substitutions may be made with equivalent or superior alternatives with prior intimation.<br>
|
||||
6. <strong>Site Access:</strong> Client to ensure uninterrupted site access during agreed working hours: {OFFICE_HOURS}.<br>
|
||||
7. <strong>Warranty:</strong> 1-year manufacturing warranty on all Furnitex-fabricated items. Hardware and third-party products carry manufacturer warranty.<br>
|
||||
8. <strong>Dispute Resolution:</strong> All disputes subject to Kolkata jurisdiction.<br><br>
|
||||
|
||||
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
|
||||
</div>"""
|
||||
|
||||
INVOICE_TC = f"""<div style="font-size:10.5px; line-height:1.8; color:#333;">
|
||||
<strong>FURNITEX — Invoice Terms & Conditions</strong><br><br>
|
||||
|
||||
1. <strong>Payment Due:</strong> Payment is due within 7 days of invoice date unless otherwise agreed in writing.<br>
|
||||
2. <strong>Late Payment:</strong> Overdue amounts attract interest at 18% per annum.<br>
|
||||
3. <strong>GST:</strong> GST as applicable under Indian law is charged additionally where mentioned.<br>
|
||||
4. <strong>Delivery & Installation:</strong> Goods remain property of Furnitex until full payment is received.<br>
|
||||
5. <strong>Returns:</strong> Custom-manufactured furniture is non-returnable. Defects to be reported within 48 hours of delivery.<br>
|
||||
6. <strong>Warranty:</strong> 1-year manufacturing defect warranty. Normal wear, misuse, or site-caused damage not covered.<br>
|
||||
7. <strong>Disputes:</strong> Subject to Kolkata jurisdiction.<br><br>
|
||||
|
||||
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
|
||||
</div>"""
|
||||
|
||||
PO_TC = f"""<div style="font-size:10.5px; line-height:1.8; color:#333;">
|
||||
<strong>FURNITEX — Purchase Order Terms & Conditions</strong><br><br>
|
||||
|
||||
1. <strong>Acceptance:</strong> Supply of goods/services against this PO constitutes acceptance of these terms.<br>
|
||||
2. <strong>Quality:</strong> All materials must conform to the specifications mentioned. Substandard materials will be rejected at supplier's cost.<br>
|
||||
3. <strong>Delivery:</strong> Deliver to the address specified on the PO by the agreed date. Delays must be communicated 48 hours in advance.<br>
|
||||
4. <strong>Invoice:</strong> Raise GST-compliant invoice (or cash memo for URD) with PO reference. Payment processed within 7 days of invoice receipt and material acceptance.<br>
|
||||
5. <strong>Warranty:</strong> Supplier warrants materials against defects for minimum 6 months from delivery.<br>
|
||||
6. <strong>Jurisdiction:</strong> Kolkata courts have exclusive jurisdiction.<br><br>
|
||||
|
||||
<em>Furnitex Atelier Pvt. Ltd. · {ADDRESS}, {CITY} - {PINCODE} · {PHONE} · {EMAIL}</em>
|
||||
</div>"""
|
||||
|
||||
TC_MAP = {
|
||||
"Furnitex - Quotation T&C": QUOTATION_TC,
|
||||
"Furnitex - Invoice T&C": INVOICE_TC,
|
||||
"Furnitex - Purchase Order T&C": PO_TC,
|
||||
}
|
||||
|
||||
|
||||
def update_terms_conditions():
|
||||
for title, content in TC_MAP.items():
|
||||
if frappe.db.exists("Terms and Conditions", title):
|
||||
doc = frappe.get_doc("Terms and Conditions", title)
|
||||
doc.terms = content
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.save()
|
||||
print(f" ✓ Updated T&C: {title}")
|
||||
else:
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Terms and Conditions",
|
||||
"title": title,
|
||||
"terms": content,
|
||||
"selling": 1,
|
||||
"buying": 1,
|
||||
"hr": 0,
|
||||
}
|
||||
)
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.insert()
|
||||
print(f" ✓ Created T&C: {title}")
|
||||
Loading…
Reference in a new issue