livrare website cu erp si crm

This commit is contained in:
DP Software services 2026-07-28 14:47:28 +03:00
parent 28773e3a72
commit 5c7bf7c295
257 changed files with 31929 additions and 0 deletions

View file

@ -0,0 +1,76 @@
"""
Add custom fields to Sales Invoice for analysis tracking.
Run inside ERPNext container:
docker exec -it didi-erpnext bench --site didi.localhost execute add-invoice-fields.add_fields
Or via API (this script uses API approach).
"""
import requests
import sys
ERPNEXT_URL = "http://localhost:8080"
# Login as Administrator (has permission for Custom Field)
session = requests.Session()
login_res = session.post(f"{ERPNEXT_URL}/api/method/login", data={"usr": "Administrator", "pwd": "admin"})
if login_res.status_code != 200:
print(f"Login failed: {login_res.status_code}")
sys.exit(1)
print("Logged in as Administrator")
HEADERS = {"Content-Type": "application/json"}
FIELDS = [
{
"dt": "Sales Invoice",
"fieldname": "analysis_consumed",
"fieldtype": "Check",
"label": "Analysis Consumed",
"insert_after": "amended_from",
"default": "0",
"allow_on_submit": 1,
"description": "Checked when the purchased analysis has been used",
},
{
"dt": "Sales Invoice",
"fieldname": "analysis_session_id",
"fieldtype": "Data",
"label": "Analysis Session ID",
"insert_after": "analysis_consumed",
"read_only": 1,
"allow_on_submit": 1,
"description": "Session ID from DiDi API after analysis was submitted",
},
]
def add_fields():
for field in FIELDS:
# Check if field already exists
check = session.get(
f"{ERPNEXT_URL}/api/resource/Custom Field",
params={
"filters": f'[["dt","=","{field["dt"]}"],["fieldname","=","{field["fieldname"]}"]]',
"fields": '["name"]',
},
headers=HEADERS,
)
data = check.json()
if data.get("data") and len(data["data"]) > 0:
print(f" Field '{field['fieldname']}' already exists, skipping.")
continue
res = session.post(
f"{ERPNEXT_URL}/api/resource/Custom Field",
json=field,
headers=HEADERS,
)
if res.status_code in (200, 201):
print(f" Created field '{field['fieldname']}' on {field['dt']}")
else:
print(f" ERROR creating '{field['fieldname']}': {res.status_code} {res.text}")
if __name__ == "__main__":
print("Adding custom fields to Sales Invoice...")
add_fields()
print("Done.")

View file

@ -0,0 +1,67 @@
import frappe
def execute():
# Hide ALL public workspaces
for ws in frappe.get_all("Workspace", filters={"public": 1}, fields=["name"]):
doc = frappe.get_doc("Workspace", ws.name)
doc.public = 0
doc.save(ignore_permissions=True)
# Delete old DiDi workspaces
for ws in frappe.get_all("Workspace", filters={"name": ["like", "%DiDi%"]}, fields=["name"]):
frappe.delete_doc("Workspace", ws.name, force=True)
# Create sidebar workspaces - each one is a clean page
create_workspace("Facturi", "invoice", 1, "Sales Invoice", [
{"type": "DocType", "link_to": "Sales Invoice", "label": "Lista Facturi"},
])
create_workspace("Clienti", "user", 2, "Customer", [
{"type": "DocType", "link_to": "Customer", "label": "Lista Clienti"},
])
create_workspace("CRM", "lead", 3, "Lead", [
{"type": "DocType", "link_to": "Lead", "label": "Lead-uri"},
{"type": "DocType", "link_to": "Sales Stage", "label": "Etape Pipeline"},
{"type": "DocType", "link_to": "Lead Source", "label": "Surse Lead"},
])
create_workspace("Servicii", "box", 4, "Item", [
{"type": "DocType", "link_to": "Item", "label": "Produse / Servicii"},
{"type": "DocType", "link_to": "Subscription Plan", "label": "Planuri Abonament"},
])
create_workspace("Plati", "credit-card", 5, "Payment Log", [
{"type": "DocType", "link_to": "Payment Log", "label": "Log Plati Stripe"},
{"type": "DocType", "link_to": "Service Agreement", "label": "Acorduri Servicii"},
])
create_workspace("Contabilitate", "calculator", 6, "Account", [
{"type": "DocType", "link_to": "Account", "label": "Plan de Conturi"},
{"type": "DocType", "link_to": "Sales Taxes and Charges Template", "label": "Template TVA"},
{"type": "DocType", "link_to": "Supplier", "label": "Furnizori"},
{"type": "DocType", "link_to": "Purchase Invoice", "label": "Facturi Furnizori"},
])
create_workspace("Website CMS", "globe", 7, "Website Content", [
{"type": "DocType", "link_to": "Website Content", "label": "Continut Pagini"},
{"type": "DocType", "link_to": "Email Template", "label": "Template-uri Email"},
])
frappe.db.commit()
print("\n=== Sidebar creat: Facturi, Clienti, CRM, Servicii, Plati, Contabilitate, Website CMS ===")
def create_workspace(label, icon, seq, module_doctype, shortcuts):
ws = frappe.get_doc({
"doctype": "Workspace",
"label": label,
"title": label,
"module": "Didi Custom",
"icon": icon,
"public": 1,
"sequence_id": seq,
"shortcuts": shortcuts,
})
ws.insert(ignore_permissions=True)
print(f" Created: {label}")

View file

@ -0,0 +1,295 @@
"""Fix ALL remaining untranslated labels in ERPNext."""
import sys, io, json, requests
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
URL = "http://localhost:8080"
s = requests.Session()
s.post(f"{URL}/api/method/login", data={"usr": "Administrator", "pwd": "admin"})
print("Logged in")
T = {
# ── Payment Entry ──
"Party Name": "Nume Client",
"Nume partid": "Nume Client",
"Party": "Client",
"Partener": "Client",
"Party Type": "Tip Client",
"Tip de partid": "Tip Client",
"Party Balance": "Sold Client",
"Balanța Party": "Sold Client",
"Paid From": "Platit Din",
"Contul plătit De la": "Platit Din",
"Paid To": "Platit Catre",
"Contul Plătite": "Platit Catre",
"Account Balance (From)": "Sold Cont (Sursa)",
"Account Balance (To)": "Sold Cont (Destinatie)",
"Account Currency (From)": "Moneda Cont (Sursa)",
"Account Currency (To)": "Moneda Cont (Destinatie)",
"Paid Amount": "Suma Platita",
"Paid Amount After Tax (RON)": "Suma Platita dupa Taxe (RON)",
"Received Amount": "Suma Primita",
"Received Amount After Tax (RON)": "Suma Primita dupa Taxe (RON)",
"Suma primită (RON)": "Suma Primita (RON)",
"Sumă Primită (RON)": "Suma Primita (RON)",
"Reference No": "Nr. Referinta",
"Cecul / de referință nr": "Nr. Referinta Plata",
"Reference Date": "Data Referinta",
"Cec/Dată de Referință": "Data Referinta Plata",
"Payment References": "Referinte Plata",
"Referințe de plată": "Referinte Plata",
"Allocated": "Alocat",
"Unallocated Amount": "Suma Nealocata",
"Suma nealocată (RON)": "Suma Nealocata (RON)",
"Total Allocated Amount": "Total Alocat",
"Suma totală alocată (RON)": "Total Alocat (RON)",
"Difference Amount": "Diferenta",
"Diferența Sumă (RON)": "Diferenta (RON)",
"Outstanding": "Restant",
"remarcabil (RON)": "Restant (RON)",
"outstanding (RON)": "Restant (RON)",
"remarkable (RON)": "Restant (RON)",
"Payment Type": "Tip Plata",
"Tipul de plată": "Tip Plata",
"Plata De la / la": "Plata De la / Catre",
"Contact Person": "Persoana de Contact",
"Persoana de Contact": "Persoana de Contact",
"Transaction ID": "ID Tranzactie",
"ID-ul de tranzacție": "ID Tranzactie",
"Cheque/Reference No": "Nr. Referinta Plata",
"Cheque/Reference Date": "Data Referinta Plata",
"Source Exchange Rate": "Curs Valutar Sursa",
"Target Exchange Rate": "Curs Valutar Destinatie",
"Connections": "Legaturi",
"Account Dimensions": "Dimensiuni Contabile",
"Dimensiuni contabile": "Dimensiuni Contabile",
"More Information": "Mai Multe Informatii",
"Mai multe informatii": "Mai Multe Informatii",
"Remarks": "Observatii",
"Remarci": "Observatii",
"In Words (Company Currency)": "In Litere (Moneda Companie)",
"În cuvinte (Compania valutar)": "In Litere (Moneda Companie)",
"In Words": "In Litere",
"În cuvinte": "In Litere",
"In Words (RON)": "In Litere (RON)",
"În cuvinte (RON)": "In Litere (RON)",
"Taxes and Charges": "Taxe si Impozite",
"Impozite și Taxe": "Taxe si Impozite",
"Total Taxes and Charges": "Total Taxe",
"Total Impozite și Taxe (RON)": "Total Taxe (RON)",
"Tax Withholding": "Retinere Taxe",
"Descărcarea de impozite": "Retinere Taxe",
"Deductions or Losses": "Deduceri sau Pierderi",
"Deduceri sau Pierderi": "Deduceri sau Pierderi",
"Naming Series": "Serie Numerotare",
"Serii": "Serie Numerotare",
"Account Holder": "Titular Cont",
"Titularul Contului": "Titular Cont",
"Section Subscriptions": "Sectiunea Abonamente",
"Secțiunea de abonamente": "Sectiunea Abonamente",
"Is Opening": "Este Deschidere",
"Se deschide": "Este Deschidere",
"Deschiderea este de intrare": "Inregistrare de Deschidere",
"Tax Rate": "Cota Impozit",
"Cota de impozitare": "Cota Impozit",
"Statistics": "Statistici",
"Statistici": "Statistici",
"Graph": "Grafic",
# ── Sales Invoice ──
"Additional Info": "Informatii Suplimentare",
"Billing Address": "Adresa de Facturare",
"Adresa De Facturare": "Adresa de Facturare",
"Shipping Address": "Adresa de Livrare",
"Adresa de livrare": "Adresa de Livrare",
"Company Address": "Adresa Companie",
"Adresă Companie": "Adresa Companie",
"Rounding Adjustment": "Ajustare Rotunjire",
"Ajustare Rotunjire (RON)": "Ajustare Rotunjire (RON)",
"Rotunjire ajustare (RON)": "Ajustare Rotunjire (RON)",
"Write Off Amount": "Suma Casare",
"Anulați suma (RON)": "Suma Casare (RON)",
"Scrie Off Suma (RON)": "Suma Casare (RON)",
"Apply Additional Discount On": "Aplica Discount Suplimentar Pe",
"Aplicați Discount suplimentare La": "Aplica Discount Suplimentar Pe",
"Items": "Articole",
"Articole": "Articole",
"Total Quantity": "Cantitate Totala",
"Cantitatea totala": "Cantitate Totala",
"Commission": "Comision",
"Comision": "Comision",
"Company Tax ID": "CUI Companie",
"Due Date": "Data Scadenta",
"Data scadentă de plată": "Data Scadenta",
"Base Change Amount (RON)": "Suma Modificare Baza (RON)",
"De schimbare a bazei Suma (RON)": "Suma Modificare Baza (RON)",
"Debit To": "Cont Debit",
"Debit Pentru": "Cont Debit",
"Accounting Details": "Detalii Contabilitate",
"Detalii Contabilitate": "Detalii Contabilitate",
"Customer PO Details": "Detalii Comanda Client",
"Detalii PO pentru clienți": "Detalii Comanda Client",
"Additional Discount": "Discount Suplimentar",
"Discount suplimentar": "Discount Suplimentar",
"Additional Discount Amount": "Suma Discount Suplimentar",
"Discount suplimentar Suma (RON)": "Suma Discount Suplimentar (RON)",
"Additional Discount Percentage": "Procent Discount Suplimentar",
"Procent de reducere suplimentară": "Procent Discount Suplimentar",
"Suma de reducere suplimentară (RON)": "Suma Discount Suplimentar (RON)",
"Sales Team": "Echipa Vanzari",
"Echipa de vânzări": "Echipa Vanzari",
"Taxes and Charges Calculation": "Calcul Taxe",
"Impozite și Taxe Calcul": "Calcul Taxe",
"Print Language": "Limba Tiparire",
"Limba de imprimare": "Limba Tiparire",
"Price List": "Lista de Preturi",
"Lista Prețuri": "Lista de Preturi",
"Packing List": "Lista de Ambalare",
"Lista de ambalare": "Lista de Ambalare",
"Price List Currency": "Moneda Lista Preturi",
"Lista de pret Valuta": "Moneda Lista Preturi",
"Price List Exchange Rate": "Curs Valutar Lista Preturi",
"Lista de schimb valutar": "Curs Valutar Lista Preturi",
"Time Sheet List": "Lista Pontaj",
"Listă de timp Sheet": "Lista Pontaj",
"Net Total": "Total Net",
"Net total (RON)": "Total Net (RON)",
"Customer Name": "Nume Client",
"Company Name": "Nume Companie",
"Nume Companie": "Nume Companie",
"Write Off": "Casare Creante",
"Pierderi din Creante": "Casare Creante",
"Advance Payments": "Plati in Avans",
"Plățile în avans": "Plati in Avans",
"Payments": "Plati",
"Plăți": "Plati",
"Posting Time": "Ora Inregistrare",
"PostingTime": "Ora Inregistrare",
"Postarea de timp": "Ora Inregistrare",
"Pricing Rules": "Reguli de Pret",
"Reguli privind prețurile": "Reguli de Pret",
"Rounded Total": "Total Rotunjit",
"Rotunjite total (RON)": "Total Rotunjit (RON)",
"Redeem Loyalty Points": "Rascumparare Puncte Loialitate",
"Răscumpărarea punctelor de loialitate": "Rascumparare Puncte Loialitate",
"Exchange Rate": "Curs Valutar",
"Rata de schimb": "Curs Valutar",
"Print Settings": "Setari Tiparire",
"Setări de imprimare": "Setari Tiparire",
"Outstanding Amount": "Suma Restanta",
"Suma Restanta (RON)": "Suma Restanta (RON)",
"Payment Terms": "Termeni Plata",
"Termeni de plată": "Termeni Plata",
"Terms and Conditions": "Termeni si Conditii",
"Termeni si conditii": "Termeni si Conditii",
"Total Advance": "Total Avans",
"Total de Advance (RON)": "Total Avans (RON)",
"Totals": "Totaluri",
"Totaluri": "Totaluri",
"Currency and Price List": "Moneda si Lista de Preturi",
"Valută și lista de prețuri": "Moneda si Lista de Preturi",
"Subscription": "Abonament",
"Changes": "Modificari",
# ── Customer ──
"Primary Customer Address": "Adresa Principala Client",
"Adresa primară a clientului": "Adresa Principala Client",
"Address and Contact": "Adresa si Contact",
"Adresa și Contact": "Adresa si Contact",
"Tax Category": "Categorie Fiscala",
"Categoria fiscală": "Categorie Fiscala",
"Primary Customer Contact": "Contact Principal Client",
"Contact primar client": "Contact Principal Client",
"Default Accounts": "Conturi Implicite",
"Conturi implicite": "Conturi Implicite",
"Defaults": "Valori Implicite",
"Implicite": "Valori Implicite",
"Internal Customer": "Client Intern",
"Credit Limit and Payment Terms": "Limita Credit si Termeni Plata",
"Limita de credit și termenii de plată": "Limita Credit si Termeni Plata",
"Primary Address and Contact": "Adresa si Contact Principal",
"Loyalty Points": "Puncte Loialitate",
"Puncte de loialitate": "Puncte Loialitate",
"Default Payment Terms Template": "Sablon Termeni Plata Standard",
"Șablonul Termenii de plată standard": "Sablon Termeni Plata Standard",
"Tax ID": "Cod Fiscal (CUI)",
"ID impozit": "Cod Fiscal (CUI)",
"Email ID": "Adresa Email",
"ID-ul de e-mail": "Adresa Email",
# ── Lead ──
"Lead Name": "Nume Lead",
"Email Address": "Adresa Email",
"Lead Source": "Sursa Lead",
"Campaign Name": "Nume Campanie",
# ── Common / Navigation ──
"Activity": "Activitate",
"Activitate": "Activitate",
"Assigned To": "Atribuit Pentru",
"Atribuit pentru": "Atribuit Pentru",
"Attachments": "Atasamente",
"Ataşamente": "Atasamente",
"Tags": "Etichete",
"Etichete": "Etichete",
"Action": "Actiune",
"Acțiune": "Actiune",
"Follow": "Urmareste",
"Urma": "Urmareste",
"Comment": "Comentariu",
"Comentarii": "Comentarii",
"Comments": "Comentarii",
"New Email": "Email Nou",
"Email nou": "Email Nou",
"Type a reply / comment": "Scrie un raspuns / comentariu",
"No new notifications": "Nicio notificare noua",
"Help Dropdown": "Meniu Ajutor",
"Meniul drop-down Ajutor": "Meniu Ajutor",
"User Menu": "Meniu Utilizator",
"Toggle Sidebar": "Comuta Bara Laterala",
"Comutați bara laterală": "Comuta Bara Laterala",
"You last edited this": "Ultima editare",
"You created this": "Creat de tine",
"You submitted this document": "Document confirmat de tine",
"yesterday": "ieri",
"Bank Transaction": "Tranzactie Bancara",
"Tranzacție bancară": "Tranzactie Bancara",
"Settle": "Achita",
"Achita": "Achita",
"General Ledger": "Registru Contabil",
"Registru Contabil": "Registru Contabil",
}
created = 0
skipped = 0
for source, translated in T.items():
check = s.get(f"{URL}/api/resource/Translation", params={
"filters": json.dumps([["language", "=", "ro"], ["source_text", "=", source]]),
"fields": json.dumps(["name", "translated_text"]),
"limit_page_length": 1,
})
existing = check.json().get("data", [])
if existing:
# Update if different
if existing[0].get("translated_text") != translated:
s.put(f"{URL}/api/resource/Translation/{existing[0]['name']}", json={"translated_text": translated})
created += 1
else:
skipped += 1
continue
r = s.post(f"{URL}/api/resource/Translation", json={
"language": "ro",
"source_text": source,
"translated_text": translated,
})
if r.status_code in (200, 201):
created += 1
else:
skipped += 1
print(f"Created/Updated: {created}, Skipped: {skipped}")
# Clear cache
s.post(f"{URL}/api/method/frappe.client.clear_cache")
print("Cache cleared. Ctrl+Shift+R in browser.")

View file

@ -0,0 +1,42 @@
import frappe
def execute():
# Delete old DiDi workspace
if frappe.db.exists("Workspace", "DiDi"):
frappe.delete_doc("Workspace", "DiDi", force=True)
# Hide ALL other workspaces
for ws in frappe.get_all("Workspace", filters={"public": 1}, fields=["name"]):
doc = frappe.get_doc("Workspace", ws.name)
doc.public = 0
doc.save(ignore_permissions=True)
print(f" Hidden: {ws.name}")
# Create clean DiDi workspace
ws = frappe.get_doc({
"doctype": "Workspace",
"name": "DiDi",
"label": "DiDi",
"title": "DiDi - Panou Principal",
"module": "Didi Custom",
"icon": "home",
"public": 1,
"sequence_id": 0,
"shortcuts": [
{"type": "DocType", "link_to": "Sales Invoice", "label": "FACTURI"},
{"type": "DocType", "link_to": "Customer", "label": "CLIENTI"},
{"type": "DocType", "link_to": "Lead", "label": "LEAD-URI CRM"},
{"type": "DocType", "link_to": "Payment Log", "label": "LOG PLATI STRIPE"},
{"type": "DocType", "link_to": "Service Agreement", "label": "ACORDURI SERVICII"},
{"type": "DocType", "link_to": "Item", "label": "SERVICII / PRODUSE"},
{"type": "DocType", "link_to": "Subscription Plan", "label": "PLANURI ABONAMENT"},
{"type": "DocType", "link_to": "Website Content", "label": "CONTINUT WEBSITE (CMS)"},
{"type": "DocType", "link_to": "Supplier", "label": "FURNIZORI"},
{"type": "DocType", "link_to": "Email Template", "label": "TEMPLATE-URI EMAIL"},
{"type": "DocType", "link_to": "Account", "label": "PLAN DE CONTURI"},
],
})
ws.insert(ignore_permissions=True)
print("DiDi workspace created with all shortcuts")
frappe.db.commit()

View file

@ -0,0 +1,291 @@
"""
Create clean sidebar workspaces for DiDi ERPNext.
Run: python setup-sidebar.py
"""
import requests
import json
ERPNEXT_URL = "http://localhost:8080"
# Login as Administrator
session = requests.Session()
login = session.post(f"{ERPNEXT_URL}/api/method/login", data={"usr": "Administrator", "pwd": "admin"})
if login.status_code != 200:
print(f"Login failed: {login.status_code}")
exit(1)
print("Logged in as Administrator")
def api(method, endpoint, data=None):
url = f"{ERPNEXT_URL}{endpoint}"
if method == "GET":
r = session.get(url, params=data)
elif method == "POST":
r = session.post(url, json=data)
elif method == "PUT":
r = session.put(url, json=data)
elif method == "DELETE":
r = session.delete(url)
else:
return None
return r
# ─── 1. Give API user Workspace Manager role ───
print("\n1. Adding Workspace Manager role to API user...")
try:
r = session.post(f"{ERPNEXT_URL}/api/method/frappe.client.get_list", json={
"doctype": "Has Role",
"filters": {"parent": "website_api@didi-erp", "role": "Workspace Manager"},
"fields": ["name"],
})
existing = r.json().get("message", [])
if not existing:
user_doc = session.get(f"{ERPNEXT_URL}/api/resource/User/website_api@didi-erp").json()["data"]
roles = user_doc.get("roles", [])
roles.append({"role": "Workspace Manager"})
session.put(f"{ERPNEXT_URL}/api/resource/User/website_api@didi-erp", json={"roles": roles})
print(" Added Workspace Manager role")
else:
print(" Already has Workspace Manager role")
except Exception as e:
print(f" Warning: {e}")
# ─── 2. Hide old workspaces ───
print("\n2. Hiding old/unused workspaces...")
to_hide = [
"DiDi Platform", "DiDi", "Accounting", "CRM", "Selling",
"Assets", "Build", "Buying", "ERPNext Integrations", "ERPNext Settings",
"Financial Reports", "Integrations", "Manufacturing", "Payables",
"Projects", "Quality", "Receivables", "Stock", "Support",
]
for ws_name in to_hide:
try:
r = api("PUT", f"/api/resource/Workspace/{requests.utils.quote(ws_name)}", {"is_hidden": 1})
if r.status_code == 200:
print(f" Hidden: {ws_name}")
else:
print(f" Skip (not found or error): {ws_name}")
except:
pass
# ─── 3. Create new sidebar workspaces ───
print("\n3. Creating sidebar workspaces...")
WORKSPACES = [
{
"name": "Facturi",
"title": "Facturi",
"icon": "file-text",
"indicator_color": "green",
"sequence_id": 10,
"content": json.dumps([
{"id": "h1", "type": "header", "data": {"text": "<span class=\"h4\"><b>Facturare</b></span>", "col": 12}},
{"id": "s1", "type": "shortcut", "data": {"shortcut_name": "Toate Facturile", "col": 4}},
{"id": "s2", "type": "shortcut", "data": {"shortcut_name": "Facturi Platite", "col": 4}},
{"id": "s3", "type": "shortcut", "data": {"shortcut_name": "Payment Entry", "col": 4}},
]),
"shortcuts": [
{"type": "DocType", "link_to": "Sales Invoice", "label": "Toate Facturile", "color": "Green", "doc_view": "List"},
{"type": "DocType", "link_to": "Sales Invoice", "label": "Facturi Platite", "color": "Blue", "doc_view": "List"},
{"type": "DocType", "link_to": "Payment Entry", "label": "Payment Entry", "color": "Yellow", "doc_view": "List"},
],
"links": [
{"type": "Card Break", "label": "Facturi"},
{"type": "Link", "label": "Sales Invoice", "link_to": "Sales Invoice", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Payment Entry", "link_to": "Payment Entry", "link_type": "DocType"},
{"type": "Link", "label": "Taxe (TVA)", "link_to": "Sales Taxes and Charges Template", "link_type": "DocType"},
],
},
{
"name": "Clienti",
"title": "Clienti",
"icon": "user",
"indicator_color": "blue",
"sequence_id": 20,
"content": json.dumps([
{"id": "h1", "type": "header", "data": {"text": "<span class=\"h4\"><b>Clienti</b></span>", "col": 12}},
{"id": "s1", "type": "shortcut", "data": {"shortcut_name": "Toti Clientii", "col": 6}},
{"id": "s2", "type": "shortcut", "data": {"shortcut_name": "Service Agreement", "col": 6}},
]),
"shortcuts": [
{"type": "DocType", "link_to": "Customer", "label": "Toti Clientii", "color": "Blue", "doc_view": "List"},
{"type": "DocType", "link_to": "Service Agreement", "label": "Service Agreement", "color": "Purple", "doc_view": "List"},
],
"links": [
{"type": "Card Break", "label": "Clienti"},
{"type": "Link", "label": "Customer", "link_to": "Customer", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Service Agreement", "link_to": "Service Agreement", "link_type": "DocType"},
{"type": "Link", "label": "Subscription Plan", "link_to": "Subscription Plan", "link_type": "DocType"},
],
},
{
"name": "CRM DiDi",
"title": "CRM",
"icon": "share",
"indicator_color": "orange",
"sequence_id": 30,
"content": json.dumps([
{"id": "h1", "type": "header", "data": {"text": "<span class=\"h4\"><b>CRM & Lead Management</b></span>", "col": 12}},
{"id": "s1", "type": "shortcut", "data": {"shortcut_name": "Lead-uri", "col": 4}},
{"id": "s2", "type": "shortcut", "data": {"shortcut_name": "Sales Stage", "col": 4}},
{"id": "s3", "type": "shortcut", "data": {"shortcut_name": "Lead Source", "col": 4}},
]),
"shortcuts": [
{"type": "DocType", "link_to": "Lead", "label": "Lead-uri", "color": "Orange", "doc_view": "List"},
{"type": "DocType", "link_to": "Sales Stage", "label": "Sales Stage", "color": "Yellow"},
{"type": "DocType", "link_to": "Lead Source", "label": "Lead Source", "color": "Grey"},
],
"links": [
{"type": "Card Break", "label": "CRM"},
{"type": "Link", "label": "Lead", "link_to": "Lead", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Sales Stage", "link_to": "Sales Stage", "link_type": "DocType"},
{"type": "Link", "label": "Lead Source", "link_to": "Lead Source", "link_type": "DocType"},
],
},
{
"name": "Servicii DiDi",
"title": "Servicii",
"icon": "box",
"indicator_color": "purple",
"sequence_id": 40,
"content": json.dumps([
{"id": "h1", "type": "header", "data": {"text": "<span class=\"h4\"><b>Servicii & Produse</b></span>", "col": 12}},
{"id": "s1", "type": "shortcut", "data": {"shortcut_name": "Articole", "col": 6}},
{"id": "s2", "type": "shortcut", "data": {"shortcut_name": "Planuri Abonament", "col": 6}},
]),
"shortcuts": [
{"type": "DocType", "link_to": "Item", "label": "Articole", "color": "Purple", "doc_view": "List"},
{"type": "DocType", "link_to": "Subscription Plan", "label": "Planuri Abonament", "color": "Cyan"},
],
"links": [
{"type": "Card Break", "label": "Catalog"},
{"type": "Link", "label": "Item", "link_to": "Item", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Item Group", "link_to": "Item Group", "link_type": "DocType"},
{"type": "Link", "label": "Subscription Plan", "link_to": "Subscription Plan", "link_type": "DocType"},
],
},
{
"name": "Plati DiDi",
"title": "Plati",
"icon": "credit-card",
"indicator_color": "yellow",
"sequence_id": 50,
"content": json.dumps([
{"id": "h1", "type": "header", "data": {"text": "<span class=\"h4\"><b>Plati Stripe & Log</b></span>", "col": 12}},
{"id": "s1", "type": "shortcut", "data": {"shortcut_name": "Payment Log", "col": 6}},
{"id": "s2", "type": "shortcut", "data": {"shortcut_name": "Analysis Report", "col": 6}},
]),
"shortcuts": [
{"type": "DocType", "link_to": "Payment Log", "label": "Payment Log", "color": "Yellow", "doc_view": "List"},
{"type": "DocType", "link_to": "Analysis Report", "label": "Analysis Report", "color": "Pink", "doc_view": "List"},
],
"links": [
{"type": "Card Break", "label": "Plati"},
{"type": "Link", "label": "Payment Log", "link_to": "Payment Log", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Analysis Report", "link_to": "Analysis Report", "link_type": "DocType"},
],
},
{
"name": "Contabilitate DiDi",
"title": "Contabilitate",
"icon": "calculator",
"indicator_color": "grey",
"sequence_id": 60,
"content": json.dumps([
{"id": "h1", "type": "header", "data": {"text": "<span class=\"h4\"><b>Contabilitate</b></span>", "col": 12}},
{"id": "s1", "type": "shortcut", "data": {"shortcut_name": "Plan Conturi", "col": 4}},
{"id": "s2", "type": "shortcut", "data": {"shortcut_name": "Furnizori", "col": 4}},
{"id": "s3", "type": "shortcut", "data": {"shortcut_name": "Companie", "col": 4}},
]),
"shortcuts": [
{"type": "DocType", "link_to": "Account", "label": "Plan Conturi", "color": "Grey", "doc_view": "Tree"},
{"type": "DocType", "link_to": "Supplier", "label": "Furnizori", "color": "Blue"},
{"type": "DocType", "link_to": "Company", "label": "Companie", "color": "Green"},
],
"links": [
{"type": "Card Break", "label": "Contabilitate"},
{"type": "Link", "label": "Account", "link_to": "Account", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Company", "link_to": "Company", "link_type": "DocType"},
{"type": "Link", "label": "Supplier", "link_to": "Supplier", "link_type": "DocType"},
{"type": "Link", "label": "Purchase Invoice", "link_to": "Purchase Invoice", "link_type": "DocType"},
{"type": "Link", "label": "Sales Taxes Template", "link_to": "Sales Taxes and Charges Template", "link_type": "DocType"},
],
},
{
"name": "Website CMS DiDi",
"title": "Website CMS",
"icon": "globe",
"indicator_color": "cyan",
"sequence_id": 70,
"content": json.dumps([
{"id": "h1", "type": "header", "data": {"text": "<span class=\"h4\"><b>Continut Website</b></span>", "col": 12}},
{"id": "s1", "type": "shortcut", "data": {"shortcut_name": "Website Content", "col": 6}},
{"id": "s2", "type": "shortcut", "data": {"shortcut_name": "Email Template", "col": 6}},
]),
"shortcuts": [
{"type": "DocType", "link_to": "Website Content", "label": "Website Content", "color": "Cyan", "doc_view": "List"},
{"type": "DocType", "link_to": "Email Template", "label": "Email Template", "color": "Grey"},
],
"links": [
{"type": "Card Break", "label": "CMS"},
{"type": "Link", "label": "Website Content", "link_to": "Website Content", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Email Template", "link_to": "Email Template", "link_type": "DocType"},
],
},
]
for ws in WORKSPACES:
name = ws["name"]
# Check if exists
check = session.get(f"{ERPNEXT_URL}/api/resource/Workspace/{requests.utils.quote(name)}")
if check.status_code == 200:
# Update it
r = api("PUT", f"/api/resource/Workspace/{requests.utils.quote(name)}", {
"title": ws["title"],
"icon": ws["icon"],
"indicator_color": ws["indicator_color"],
"is_hidden": 0,
"public": 1,
"module": "Didi Custom",
"sequence_id": ws["sequence_id"],
"content": ws["content"],
"shortcuts": ws["shortcuts"],
"links": ws["links"],
})
if r.status_code == 200:
print(f" Updated: {name}")
else:
print(f" Error updating {name}: {r.status_code} {r.text[:200]}")
else:
# Create it
r = api("POST", "/api/resource/Workspace", {
"name": name,
"label": name,
"title": ws["title"],
"icon": ws["icon"],
"indicator_color": ws["indicator_color"],
"is_hidden": 0,
"public": 1,
"module": "Didi Custom",
"sequence_id": ws["sequence_id"],
"content": ws["content"],
"shortcuts": ws["shortcuts"],
"links": ws["links"],
})
if r.status_code in (200, 201):
print(f" Created: {name}")
else:
print(f" Error creating {name}: {r.status_code} {r.text[:200]}")
print("\n4. Verifying sidebar...")
r = session.get(f"{ERPNEXT_URL}/api/resource/Workspace", params={
"filters": json.dumps([["public", "=", 1], ["is_hidden", "=", 0]]),
"fields": json.dumps(["name", "title", "icon", "sequence_id"]),
"order_by": "sequence_id asc",
"limit_page_length": 20,
})
for ws in r.json().get("data", []):
print(f" [{ws.get('sequence_id', '?')}] {ws['icon'] or '?'} {ws['title']}")
print("\nDone! Refresh ERPNext in browser (Ctrl+Shift+R).")

View file

@ -0,0 +1,153 @@
"""Rename all ERPNext accounts to Romanian."""
import requests, json, sys, io, urllib.parse
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
URL = "http://localhost:8080"
s = requests.Session()
s.post(f"{URL}/api/method/login", data={"usr": "Administrator", "pwd": "admin"})
print("Logged in")
RENAMES = {
# Root groups (Level 1)
"Application of Funds (Assets)": "Active",
"Source of Funds (Liabilities)": "Pasive (Datorii)",
"Equity": "Capitaluri Proprii",
"Income": "Venituri",
"Expenses": "Cheltuieli",
# Asset sub-groups
"Current Assets": "Active Curente",
"Cash In Hand": "Numerar",
"Cash": "Casa",
"Bank Accounts": "Conturi Bancare",
"Accounts Receivable": "Creante Clienti",
"Debtors": "Debitori",
"Stock Assets": "Active Stocuri",
"Stock In Hand": "Stoc",
"Tax Assets": "Active Fiscale",
"Loans and Advances (Assets)": "Imprumuturi si Avansuri (Active)",
"Employee Advances": "Avansuri Angajati",
"Securities and Deposits": "Garantii si Depozite",
"Earnest Money": "Garantii",
"Fixed Assets": "Imobilizari Corporale",
"Capital Equipments": "Echipamente de Capital",
"Electronic Equipments": "Echipamente Electronice",
"Furnitures and Fixtures": "Mobilier si Dotari",
"Office Equipments": "Echipamente Birou",
"Plants and Machineries": "Utilaje si Masini",
"Buildings": "Cladiri",
"Softwares": "Software",
"Accumulated Depreciation": "Amortizare Cumulata",
"CWIP Account": "Lucrari in Curs",
"Investments": "Investitii",
"Temporary Accounts": "Conturi Temporare",
"Temporary Opening": "Sold Initial Temporar",
# Liability sub-groups
"Current Liabilities": "Datorii Curente",
"Accounts Payable": "Datorii Furnizori",
"Creditors": "Creditori",
"Payroll Payable": "Salarii de Plata",
"Stock Liabilities": "Datorii Stocuri",
"Stock Received But Not Billed": "Stoc Receptionat Nefacturat",
"Asset Received But Not Billed": "Active Receptionate Nefacturate",
"Duties and Taxes": "Taxe si Impozite",
"TDS Payable": "Taxe de Plata",
"Loans (Liabilities)": "Imprumuturi (Datorii)",
"Secured Loans": "Imprumuturi Garantate",
"Unsecured Loans": "Imprumuturi Negarantate",
"Bank Overdraft Account": "Descoperit de Cont",
# Equity
"Capital Stock": "Capital Social",
"Dividends Paid": "Dividende Platite",
"Opening Balance Equity": "Sold Initial Capital",
"Retained Earnings": "Rezultat Reportat",
# Income
"Direct Income": "Venituri Directe",
"Sales": "Vanzari",
"Service": "Servicii",
"Indirect Income": "Venituri Indirecte",
# Expenses
"Direct Expenses": "Cheltuieli Directe",
"Stock Expenses": "Cheltuieli Stocuri",
"Cost of Goods Sold": "Costul Bunurilor Vandute",
"Expenses Included In Asset Valuation": "Cheltuieli Incluse in Evaluarea Activelor",
"Expenses Included In Valuation": "Cheltuieli Incluse in Evaluare",
"Stock Adjustment": "Ajustare Stoc",
"Indirect Expenses": "Cheltuieli Indirecte",
"Administrative Expenses": "Cheltuieli Administrative",
"Commission on Sales": "Comisioane Vanzari",
"Depreciation": "Amortizare",
"Entertainment Expenses": "Cheltuieli Reprezentare",
"Freight and Forwarding Charges": "Cheltuieli Transport",
"Legal Expenses": "Cheltuieli Juridice",
"Marketing Expenses": "Cheltuieli Marketing",
"Office Maintenance Expenses": "Cheltuieli Intretinere Birou",
"Office Rent": "Chirie Birou",
"Postal Expenses": "Cheltuieli Postale",
"Print and Stationery": "Tiparituri si Papetarie",
"Round Off": "Rotunjiri",
"Salary": "Salarii",
"Sales Expenses": "Cheltuieli Vanzari",
"Telephone Expenses": "Cheltuieli Telefon",
"Travel Expenses": "Cheltuieli Deplasare",
"Utility Expenses": "Cheltuieli Utilitati",
"Write Off": "Pierderi din Creante",
"Exchange Gain/Loss": "Diferente de Curs Valutar",
"Gain/Loss on Asset Disposal": "Castiguri/Pierderi din Casare Active",
"Miscellaneous Expenses": "Cheltuieli Diverse",
}
# Get all accounts
r = s.get(f"{URL}/api/resource/Account", params={
"filters": json.dumps([["company", "=", "TOP CLOSSERS SRL"]]),
"fields": json.dumps(["name", "account_name"]),
"limit_page_length": 200,
})
accounts = r.json().get("data", [])
renamed = 0
skipped = 0
errors = 0
for acc in accounts:
old_name = acc["account_name"]
if old_name not in RENAMES:
continue
new_name = RENAMES[old_name]
full_old = acc["name"] # e.g. "1000 - Application of Funds (Assets) - TC"
# Build new full name
parts = full_old.split(" - ")
if len(parts) >= 3:
parts[1] = new_name
full_new = " - ".join(parts)
else:
skipped += 1
continue
# Rename via frappe.client.rename_doc
r = s.post(f"{URL}/api/method/frappe.client.rename_doc", json={
"doctype": "Account",
"old": full_old,
"new": full_new,
"merge": 0,
})
if r.status_code == 200:
renamed += 1
print(f" OK: {old_name} -> {new_name}")
else:
errors += 1
err_msg = r.text[:150] if r.text else str(r.status_code)
print(f" ERR: {old_name}: {err_msg}")
print(f"\nRenamed: {renamed}, Skipped: {skipped}, Errors: {errors}")
# Clear cache
s.post(f"{URL}/api/method/frappe.client.clear_cache")
print("Cache cleared. Refresh browser.")

View file

@ -0,0 +1,152 @@
"""Translate account names by updating account_name field + adding translations."""
import requests, json, sys, io, urllib.parse
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
URL = "http://localhost:8080"
s = requests.Session()
s.post(f"{URL}/api/method/login", data={"usr": "Administrator", "pwd": "admin"})
print("Logged in")
TRANSLATIONS = {
"Application of Funds (Assets)": "Active",
"Source of Funds (Liabilities)": "Pasive (Datorii)",
"Equity": "Capitaluri Proprii",
"Income": "Venituri",
"Expenses": "Cheltuieli",
"Current Assets": "Active Curente",
"Cash In Hand": "Numerar",
"Cash": "Casa",
"Bank Accounts": "Conturi Bancare",
"Accounts Receivable": "Creante Clienti",
"Debtors": "Debitori",
"Stock Assets": "Active Stocuri",
"Stock In Hand": "Stoc",
"Tax Assets": "Active Fiscale",
"Loans and Advances (Assets)": "Imprumuturi si Avansuri",
"Employee Advances": "Avansuri Angajati",
"Securities and Deposits": "Garantii si Depozite",
"Earnest Money": "Garantii",
"Fixed Assets": "Imobilizari Corporale",
"Capital Equipments": "Echipamente de Capital",
"Electronic Equipments": "Echipamente Electronice",
"Furnitures and Fixtures": "Mobilier si Dotari",
"Office Equipments": "Echipamente Birou",
"Plants and Machineries": "Utilaje si Masini",
"Buildings": "Cladiri",
"Softwares": "Software",
"Accumulated Depreciation": "Amortizare Cumulata",
"CWIP Account": "Lucrari in Curs",
"Investments": "Investitii",
"Temporary Accounts": "Conturi Temporare",
"Temporary Opening": "Sold Initial Temporar",
"Current Liabilities": "Datorii Curente",
"Accounts Payable": "Datorii Furnizori",
"Creditors": "Creditori",
"Payroll Payable": "Salarii de Plata",
"Stock Liabilities": "Datorii Stocuri",
"Stock Received But Not Billed": "Stoc Receptionat Nefacturat",
"Asset Received But Not Billed": "Active Receptionate Nefacturate",
"Duties and Taxes": "Taxe si Impozite",
"TDS Payable": "Taxe de Plata",
"Loans (Liabilities)": "Imprumuturi (Datorii)",
"Secured Loans": "Imprumuturi Garantate",
"Unsecured Loans": "Imprumuturi Negarantate",
"Bank Overdraft Account": "Descoperit de Cont",
"Capital Stock": "Capital Social",
"Dividends Paid": "Dividende Platite",
"Opening Balance Equity": "Sold Initial Capital",
"Retained Earnings": "Rezultat Reportat",
"Direct Income": "Venituri Directe",
"Sales": "Vanzari",
"Service": "Servicii",
"Indirect Income": "Venituri Indirecte",
"Direct Expenses": "Cheltuieli Directe",
"Stock Expenses": "Cheltuieli Stocuri",
"Cost of Goods Sold": "Costul Bunurilor Vandute",
"Expenses Included In Asset Valuation": "Cheltuieli in Evaluarea Activelor",
"Expenses Included In Valuation": "Cheltuieli in Evaluare",
"Stock Adjustment": "Ajustare Stoc",
"Indirect Expenses": "Cheltuieli Indirecte",
"Administrative Expenses": "Cheltuieli Administrative",
"Commission on Sales": "Comisioane Vanzari",
"Depreciation": "Amortizare",
"Entertainment Expenses": "Cheltuieli Reprezentare",
"Freight and Forwarding Charges": "Cheltuieli Transport",
"Legal Expenses": "Cheltuieli Juridice",
"Marketing Expenses": "Cheltuieli Marketing",
"Office Maintenance Expenses": "Intretinere Birou",
"Office Rent": "Chirie Birou",
"Postal Expenses": "Cheltuieli Postale",
"Print and Stationery": "Tiparituri si Papetarie",
"Round Off": "Rotunjiri",
"Salary": "Salarii",
"Sales Expenses": "Cheltuieli Vanzari",
"Telephone Expenses": "Cheltuieli Telefon",
"Travel Expenses": "Cheltuieli Deplasare",
"Utility Expenses": "Cheltuieli Utilitati",
"Write Off": "Pierderi din Creante",
"Exchange Gain/Loss": "Diferente Curs Valutar",
"Gain/Loss on Asset Disposal": "Castig/Pierdere Casare Active",
"Miscellaneous Expenses": "Cheltuieli Diverse",
}
# Add all as Translation entries (for the UI)
print("1. Adding account name translations...")
created = 0
for en, ro in TRANSLATIONS.items():
check = s.get(f"{URL}/api/resource/Translation", params={
"filters": json.dumps([["language", "=", "ro"], ["source_text", "=", en]]),
"fields": json.dumps(["name"]),
"limit_page_length": 1,
})
if check.json().get("data"):
continue
r = s.post(f"{URL}/api/resource/Translation", json={
"language": "ro",
"source_text": en,
"translated_text": ro,
})
if r.status_code in (200, 201):
created += 1
print(f" {created} traduceri noi")
# Now rename accounts using bench command via docker exec
# Actually, let's use the proper rename API with positional args
print("\n2. Renaming accounts...")
renamed = 0
errors = 0
r = s.get(f"{URL}/api/resource/Account", params={
"filters": json.dumps([["company", "=", "TOP CLOSSERS SRL"]]),
"fields": json.dumps(["name", "account_name"]),
"limit_page_length": 200,
})
accounts = r.json().get("data", [])
for acc in accounts:
old_account_name = acc["account_name"]
if old_account_name not in TRANSLATIONS:
continue
new_account_name = TRANSLATIONS[old_account_name]
full_name = acc["name"]
# Update account_name field directly
r = s.put(f"{URL}/api/resource/Account/{urllib.parse.quote(full_name, safe='')}", json={
"account_name": new_account_name,
})
if r.status_code == 200:
renamed += 1
else:
errors += 1
# Try to understand error
msg = r.text[:100] if r.text else str(r.status_code)
print(f" ERR {old_account_name}: {msg}")
print(f" Renamed: {renamed}, Errors: {errors}")
# Clear cache
s.post(f"{URL}/api/method/frappe.client.clear_cache")
print("\nDone! Refresh browser (Ctrl+Shift+R).")

View file

@ -0,0 +1,287 @@
"""
ERPNext Configuration Script for DiDi Project
Run with: docker exec didi-erpnext bench --site didi.localhost execute didi_setup
Or copy to container and run via bench.
"""
import frappe
def execute():
"""Main setup function - run all configurations."""
setup_company_details()
setup_tax_template()
setup_invoice_naming()
setup_crm_pipeline()
setup_subscription_items()
setup_email_templates()
frappe.db.commit()
print("\n=== DiDi ERPNext Configuration Complete ===\n")
def setup_company_details():
"""Configure company details - CUI, address, etc."""
print("[1/6] Configuring company details...")
company = frappe.get_doc("Company", "TOP CLOSSERS SRL")
company.tax_id = "36193026"
company.domain = "Services"
company.default_currency = "RON"
company.country = "Romania"
company.save(ignore_permissions=True)
# Create company address
if not frappe.db.exists("Address", {"address_title": "TOP CLOSSERS SRL - Sediu"}):
addr = frappe.get_doc({
"doctype": "Address",
"address_title": "TOP CLOSSERS SRL - Sediu",
"address_type": "Office",
"address_line1": "Str. Targovistei 15",
"address_line2": "Bl. 2 Et. 3 Ap. 22",
"city": "Ploiesti",
"state": "Prahova",
"pincode": "100299",
"country": "Romania",
"phone": "+40721063078",
"email_id": "office@clossers.com",
"is_primary_address": 1,
"links": [{"link_doctype": "Company", "link_name": "TOP CLOSSERS SRL"}]
})
addr.insert(ignore_permissions=True)
print(" Company details configured.")
def setup_tax_template():
"""Configure VAT 19% tax template for Romania."""
print("[2/6] Configuring tax templates...")
# Get or create default income account
company_abbr = "TC"
tax_account = f"TVA Colectata - {company_abbr}"
# Check if tax account exists, create if not
if not frappe.db.exists("Account", tax_account):
parent_account = frappe.db.get_value("Account",
{"company": "TOP CLOSSERS SRL", "account_type": "Tax", "is_group": 1},
"name"
)
if not parent_account:
parent_account = frappe.db.get_value("Account",
{"company": "TOP CLOSSERS SRL", "root_type": "Liability", "is_group": 1},
"name"
)
if parent_account:
tax_acc = frappe.get_doc({
"doctype": "Account",
"account_name": "TVA Colectata",
"parent_account": parent_account,
"account_type": "Tax",
"company": "TOP CLOSSERS SRL",
"tax_rate": 19.0
})
tax_acc.insert(ignore_permissions=True)
# Create Sales Tax Template
if not frappe.db.exists("Sales Taxes and Charges Template", {"title": "TVA 19% Romania"}):
template = frappe.get_doc({
"doctype": "Sales Taxes and Charges Template",
"title": "TVA 19% Romania",
"company": "TOP CLOSSERS SRL",
"is_default": 1,
"taxes": [{
"charge_type": "On Net Total",
"account_head": tax_account if frappe.db.exists("Account", tax_account) else "",
"description": "TVA 19%",
"rate": 19.0
}]
})
try:
template.insert(ignore_permissions=True)
print(" Tax template TVA 19% created.")
except Exception as e:
print(f" Tax template: {e}")
else:
print(" Tax template already exists.")
def setup_invoice_naming():
"""Configure Romanian invoice naming series."""
print("[3/6] Configuring invoice naming series...")
# Set naming series for Sales Invoice
if frappe.db.exists("DocType", "Sales Invoice"):
prop_setter_name = "Sales Invoice-naming_series-options"
if not frappe.db.exists("Property Setter", prop_setter_name):
try:
ps = frappe.get_doc({
"doctype": "Property Setter",
"doctype_or_field": "DocType",
"doc_type": "Sales Invoice",
"field_name": "naming_series",
"property": "options",
"value": "DIDI-INV-.YYYY.-.#####\nACC-SINV-.YYYY.-",
"property_type": "Text"
})
ps.insert(ignore_permissions=True)
print(" Invoice naming series configured: DIDI-INV-YYYY-#####")
except Exception as e:
print(f" Naming series: {e}")
else:
print(" Naming series already configured.")
def setup_crm_pipeline():
"""Configure CRM sales pipeline stages."""
print("[4/6] Configuring CRM pipeline...")
# CRM Sales Stages
stages = [
{"stage_name": "Lead", "department": ""},
{"stage_name": "Calificat", "department": ""},
{"stage_name": "Demo", "department": ""},
{"stage_name": "Client", "department": ""},
]
for stage_data in stages:
if not frappe.db.exists("Sales Stage", stage_data["stage_name"]):
stage = frappe.get_doc({
"doctype": "Sales Stage",
"stage_name": stage_data["stage_name"]
})
stage.insert(ignore_permissions=True)
print(f" Created stage: {stage_data['stage_name']}")
# Lead Source for website forms
sources = ["Website - Contact Form", "Website - Pricing Page", "Website - Demo Request"]
for source_name in sources:
if not frappe.db.exists("Lead Source", source_name):
source = frappe.get_doc({
"doctype": "Lead Source",
"source_name": source_name
})
source.insert(ignore_permissions=True)
print(" CRM pipeline configured: Lead > Calificat > Demo > Client")
def setup_subscription_items():
"""Create subscription plan items."""
print("[5/6] Creating subscription items and plans...")
# Item Group for services
if not frappe.db.exists("Item Group", "DiDi Services"):
ig = frappe.get_doc({
"doctype": "Item Group",
"item_group_name": "DiDi Services",
"parent_item_group": "All Item Groups"
})
ig.insert(ignore_permissions=True)
# Subscription Items
items = [
{
"item_code": "DIDI-FREE",
"item_name": "Abonament DiDi Free",
"description": "Plan gratuit - acces de baza la platforma DiDi pentru analiza dezinformarii",
"item_group": "DiDi Services",
"stock_uom": "Nos",
"is_stock_item": 0,
"standard_rate": 0
},
{
"item_code": "DIDI-PAID",
"item_name": "Abonament DiDi Paid",
"description": "Plan platit - acces complet la platforma DiDi cu credite lunare pentru analize avansate",
"item_group": "DiDi Services",
"stock_uom": "Nos",
"is_stock_item": 0,
"standard_rate": 99
},
{
"item_code": "DIDI-ENTERPRISE",
"item_name": "Abonament DiDi Enterprise",
"description": "Plan enterprise - acces nelimitat la platforma DiDi, suport dedicat, API avansat",
"item_group": "DiDi Services",
"stock_uom": "Nos",
"is_stock_item": 0,
"standard_rate": 499
},
]
for item_data in items:
if not frappe.db.exists("Item", item_data["item_code"]):
item = frappe.get_doc({"doctype": "Item", **item_data})
item.insert(ignore_permissions=True)
print(f" Created item: {item_data['item_code']} ({item_data['standard_rate']} RON)")
# Subscription Plans
plans = [
{"plan_name": "DiDi Free - Lunar", "item": "DIDI-FREE", "cost": 0, "billing_interval": "Month", "billing_interval_count": 1},
{"plan_name": "DiDi Paid - Lunar", "item": "DIDI-PAID", "cost": 99, "billing_interval": "Month", "billing_interval_count": 1},
{"plan_name": "DiDi Paid - Anual", "item": "DIDI-PAID", "cost": 999, "billing_interval": "Year", "billing_interval_count": 1},
{"plan_name": "DiDi Enterprise - Lunar", "item": "DIDI-ENTERPRISE", "cost": 499, "billing_interval": "Month", "billing_interval_count": 1},
{"plan_name": "DiDi Enterprise - Anual", "item": "DIDI-ENTERPRISE", "cost": 4990, "billing_interval": "Year", "billing_interval_count": 1},
]
for plan_data in plans:
if not frappe.db.exists("Subscription Plan", plan_data["plan_name"]):
plan = frappe.get_doc({
"doctype": "Subscription Plan",
"plan_name": plan_data["plan_name"],
"item": plan_data["item"],
"price_determination": "Fixed Rate",
"cost": plan_data["cost"],
"currency": "RON",
"billing_interval": plan_data["billing_interval"],
"billing_interval_count": plan_data["billing_interval_count"]
})
plan.insert(ignore_permissions=True)
print(f" Created plan: {plan_data['plan_name']} ({plan_data['cost']} RON/{plan_data['billing_interval']})")
print(" Subscription items and plans configured.")
def setup_email_templates():
"""Create email notification templates."""
print("[6/6] Creating email templates...")
templates = [
{
"name": "DiDi - Confirmare Plata",
"subject": "Plata confirmata - {{ doc.name }}",
"response": """<p>Stimate {{ doc.customer_name }},</p>
<p>Va confirmam ca plata pentru factura <strong>{{ doc.name }}</strong> in valoare de <strong>{{ doc.grand_total }} {{ doc.currency }}</strong> a fost procesata cu succes.</p>
<p>Puteti descarca factura din dashboard-ul contului dumneavoastra.</p>
<p>Va multumim,<br>Echipa DiDi</p>"""
},
{
"name": "DiDi - Expirare Abonament",
"subject": "Abonamentul dumneavoastra expira in curand",
"response": """<p>Stimate client,</p>
<p>Va informam ca abonamentul dumneavoastra DiDi expira in <strong>7 zile</strong>.</p>
<p>Pentru a continua sa beneficiati de serviciile noastre, va rugam sa verificati metoda de plata in dashboard-ul contului.</p>
<p>Va multumim,<br>Echipa DiDi</p>"""
},
{
"name": "DiDi - Esec Plata",
"subject": "Problema la procesarea platii",
"response": """<p>Stimate client,</p>
<p>Din pacate, nu am reusit sa procesam plata pentru abonamentul dumneavoastra DiDi.</p>
<p>Va rugam sa actualizati metoda de plata in dashboard-ul contului pentru a evita intreruperea serviciului.</p>
<p>Va multumim,<br>Echipa DiDi</p>"""
},
]
for tmpl in templates:
if not frappe.db.exists("Email Template", tmpl["name"]):
doc = frappe.get_doc({
"doctype": "Email Template",
"name": tmpl["name"],
"subject": tmpl["subject"],
"response": tmpl["response"],
"use_html": 1
})
doc.insert(ignore_permissions=True)
print(f" Created template: {tmpl['name']}")
print(" Email templates configured.")

View file

@ -0,0 +1,262 @@
"""
Create custom DocTypes for DiDi Platform.
"""
import frappe
import json
def execute():
create_service_agreement()
create_payment_log()
create_website_content()
add_custom_fields()
frappe.db.commit()
print("\n=== Custom DocTypes Created ===\n")
def create_service_agreement():
"""DocType: Service Agreement - contract auto-generat la activarea abonamentului."""
print("[1/4] Creating Service Agreement DocType...")
if frappe.db.exists("DocType", "Service Agreement"):
print(" Already exists, skipping.")
return
doc = frappe.get_doc({
"doctype": "DocType",
"name": "Service Agreement",
"module": "Didi Custom",
"custom": 0,
"is_submittable": 0,
"autoname": "DIDI-SA-.YYYY.-.#####",
"title_field": "customer_name",
"search_fields": "customer,plan,status",
"fields": [
{"fieldname": "customer", "label": "Customer", "fieldtype": "Link", "options": "Customer", "reqd": 1, "in_list_view": 1},
{"fieldname": "customer_name", "label": "Customer Name", "fieldtype": "Data", "fetch_from": "customer.customer_name", "read_only": 1},
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
{"fieldname": "plan", "label": "Subscription Plan", "fieldtype": "Link", "options": "Subscription Plan", "reqd": 1, "in_list_view": 1},
{"fieldname": "status", "label": "Status", "fieldtype": "Select", "options": "Draft\nAccepted\nExpired\nCancelled", "default": "Draft", "in_list_view": 1, "reqd": 1},
{"fieldname": "section_acceptance", "label": "Acceptance Details", "fieldtype": "Section Break"},
{"fieldname": "acceptance_date", "label": "Acceptance Date", "fieldtype": "Datetime"},
{"fieldname": "client_ip", "label": "Client IP", "fieldtype": "Data", "read_only": 1},
{"fieldname": "column_break_2", "fieldtype": "Column Break"},
{"fieldname": "terms_version", "label": "Terms Version", "fieldtype": "Data"},
{"fieldname": "subscription", "label": "Subscription", "fieldtype": "Link", "options": "Subscription"},
{"fieldname": "section_document", "label": "Agreement Document", "fieldtype": "Section Break"},
{"fieldname": "agreement_html", "label": "Agreement Content", "fieldtype": "Text Editor"},
],
"permissions": [
{"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
{"role": "Sales User", "read": 1, "write": 1, "create": 1},
]
})
doc.insert(ignore_permissions=True)
print(" Service Agreement DocType created.")
def create_payment_log():
"""DocType: Payment Log - audit trail for Stripe transactions."""
print("[2/4] Creating Payment Log DocType...")
if frappe.db.exists("DocType", "Payment Log"):
print(" Already exists, skipping.")
return
doc = frappe.get_doc({
"doctype": "DocType",
"name": "Payment Log",
"module": "Didi Custom",
"custom": 0,
"is_submittable": 0,
"autoname": "DIDI-PL-.YYYY.-.#####",
"title_field": "customer_name",
"search_fields": "customer,stripe_payment_intent_id,status",
"fields": [
{"fieldname": "customer", "label": "Customer", "fieldtype": "Link", "options": "Customer", "in_list_view": 1},
{"fieldname": "customer_name", "label": "Customer Name", "fieldtype": "Data", "fetch_from": "customer.customer_name", "read_only": 1},
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
{"fieldname": "event_type", "label": "Event Type", "fieldtype": "Select",
"options": "payment_intent.succeeded\ninvoice.payment_failed\ncustomer.subscription.updated\ncustomer.subscription.deleted\nrefund.created\nother",
"in_list_view": 1},
{"fieldname": "status", "label": "Status", "fieldtype": "Select",
"options": "Succeeded\nFailed\nRefunded\nPending",
"default": "Pending", "in_list_view": 1, "reqd": 1},
{"fieldname": "section_stripe", "label": "Stripe Details", "fieldtype": "Section Break"},
{"fieldname": "stripe_payment_intent_id", "label": "Stripe Payment Intent ID", "fieldtype": "Data"},
{"fieldname": "stripe_subscription_id", "label": "Stripe Subscription ID", "fieldtype": "Data"},
{"fieldname": "column_break_2", "fieldtype": "Column Break"},
{"fieldname": "amount", "label": "Amount", "fieldtype": "Currency", "in_list_view": 1},
{"fieldname": "currency", "label": "Currency", "fieldtype": "Link", "options": "Currency", "default": "RON"},
{"fieldname": "section_raw", "label": "Raw Data", "fieldtype": "Section Break", "collapsible": 1},
{"fieldname": "raw_webhook_data", "label": "Raw Webhook Data", "fieldtype": "Code", "options": "JSON"},
],
"permissions": [
{"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
{"role": "Accounts User", "read": 1},
]
})
doc.insert(ignore_permissions=True)
print(" Payment Log DocType created.")
def create_website_content():
"""DocType: Website Content - CMS for website pages managed from ERPNext."""
print("[3/4] Creating Website Content DocType...")
if frappe.db.exists("DocType", "Website Content"):
print(" Already exists, skipping.")
return
doc = frappe.get_doc({
"doctype": "DocType",
"name": "Website Content",
"module": "Didi Custom",
"custom": 0,
"is_submittable": 0,
"autoname": "format:{page_slug}-{section_key}",
"title_field": "section_key",
"search_fields": "page_slug,section_key",
"sort_field": "display_order",
"sort_order": "ASC",
"fields": [
{"fieldname": "page_slug", "label": "Page", "fieldtype": "Select",
"options": "homepage\npricing\ncontact\nabout\nprivacy\nterms",
"reqd": 1, "in_list_view": 1},
{"fieldname": "section_key", "label": "Section Key", "fieldtype": "Data",
"reqd": 1, "in_list_view": 1,
"description": "Unique key for this section (e.g. hero_title, feature_1, cta_text)"},
{"fieldname": "display_order", "label": "Display Order", "fieldtype": "Int",
"default": 0, "in_list_view": 1},
{"fieldname": "is_active", "label": "Active", "fieldtype": "Check",
"default": 1, "in_list_view": 1},
{"fieldname": "section_content_ro", "label": "Content (Romanian)", "fieldtype": "Section Break"},
{"fieldname": "content_ro", "label": "Content RO", "fieldtype": "Text Editor"},
{"fieldname": "section_content_en", "label": "Content (English)", "fieldtype": "Section Break"},
{"fieldname": "content_en", "label": "Content EN", "fieldtype": "Text Editor"},
{"fieldname": "section_media", "label": "Media", "fieldtype": "Section Break"},
{"fieldname": "image", "label": "Image", "fieldtype": "Attach Image"},
{"fieldname": "column_break_media", "fieldtype": "Column Break"},
{"fieldname": "extra_data", "label": "Extra Data (JSON)", "fieldtype": "Code", "options": "JSON",
"description": "Additional structured data (e.g. button links, feature lists)"},
],
"permissions": [
{"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
{"role": "Website Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
]
})
doc.insert(ignore_permissions=True)
# Populate default content
_populate_default_content()
print(" Website Content DocType created with default content.")
def _populate_default_content():
"""Insert default website content for all pages."""
defaults = [
# Homepage
{"page_slug": "homepage", "section_key": "hero_title", "display_order": 1,
"content_ro": "<h1>Combate dezinformarea cu inteligenta artificiala</h1>",
"content_en": "<h1>Fight disinformation with artificial intelligence</h1>"},
{"page_slug": "homepage", "section_key": "hero_subtitle", "display_order": 2,
"content_ro": "<p>DiDi analizeaza automat continut media - text, imagini, audio si video - pentru a detecta dezinformarea si a verifica informatiile in timp real.</p>",
"content_en": "<p>DiDi automatically analyzes media content - text, images, audio and video - to detect disinformation and verify information in real time.</p>"},
{"page_slug": "homepage", "section_key": "hero_cta", "display_order": 3,
"content_ro": "Incearca gratuit",
"content_en": "Try for free",
"extra_data": '{"link": "/pricing", "secondary_text_ro": "Vezi planuri", "secondary_text_en": "View plans", "secondary_link": "/pricing"}'},
{"page_slug": "homepage", "section_key": "features", "display_order": 10,
"content_ro": "<h2>Functionalitati</h2>",
"content_en": "<h2>Features</h2>",
"extra_data": json.dumps([
{"icon": "search", "title_ro": "Analiza Text", "title_en": "Text Analysis", "desc_ro": "Detectie automata a dezinformarii din articole si postari", "desc_en": "Automatic disinformation detection from articles and posts"},
{"icon": "image", "title_ro": "Analiza Imagine/Video", "title_en": "Image/Video Analysis", "desc_ro": "Detectie deepfake si manipulare media", "desc_en": "Deepfake and media manipulation detection"},
{"icon": "check", "title_ro": "Fact-Checking", "title_en": "Fact-Checking", "desc_ro": "Verificare automata a afirmatiilor cu surse credibile", "desc_en": "Automatic claim verification with credible sources"},
{"icon": "users", "title_ro": "Human-in-the-Loop", "title_en": "Human-in-the-Loop", "desc_ro": "Colaborare intre AI si experti validatori", "desc_en": "Collaboration between AI and expert validators"},
])},
{"page_slug": "homepage", "section_key": "how_it_works", "display_order": 20,
"content_ro": "<h2>Cum functioneaza</h2>",
"content_en": "<h2>How it works</h2>",
"extra_data": json.dumps([
{"step": 1, "title_ro": "Incarca continutul", "title_en": "Upload content", "desc_ro": "Text, imagine, audio sau video", "desc_en": "Text, image, audio or video"},
{"step": 2, "title_ro": "Analiza AI automata", "title_en": "Automatic AI analysis", "desc_ro": "Multiple modele AI analizeaza continutul", "desc_en": "Multiple AI models analyze the content"},
{"step": 3, "title_ro": "Rezultate verificate", "title_en": "Verified results", "desc_ro": "Raport detaliat cu scor de incredere", "desc_en": "Detailed report with confidence score"},
])},
# Contact
{"page_slug": "contact", "section_key": "title", "display_order": 1,
"content_ro": "<h1>Contacteaza-ne</h1><p>Ai intrebari? Echipa noastra iti sta la dispozitie.</p>",
"content_en": "<h1>Contact us</h1><p>Have questions? Our team is here to help.</p>"},
{"page_slug": "contact", "section_key": "info", "display_order": 2,
"content_ro": "", "content_en": "",
"extra_data": json.dumps({"email": "office@clossers.com", "phone": "+40721063078", "address": "Str. Targovistei 15, Ploiesti, Romania"})},
# Privacy
{"page_slug": "privacy", "section_key": "content", "display_order": 1,
"content_ro": "<h1>Politica de Confidentialitate</h1><p>Ultima actualizare: 2026-03-30</p><p>TOP CLOSSERS SRL respecta confidentialitatea datelor dumneavoastra personale...</p>",
"content_en": "<h1>Privacy Policy</h1><p>Last updated: 2026-03-30</p><p>TOP CLOSSERS SRL respects the privacy of your personal data...</p>"},
# Terms
{"page_slug": "terms", "section_key": "content", "display_order": 1,
"content_ro": "<h1>Termeni si Conditii</h1><p>Ultima actualizare: 2026-03-30</p><p>Prin utilizarea serviciilor DiDi, acceptati urmatorii termeni...</p>",
"content_en": "<h1>Terms and Conditions</h1><p>Last updated: 2026-03-30</p><p>By using DiDi services, you accept the following terms...</p>"},
]
for item in defaults:
name = f"{item['page_slug']}-{item['section_key']}"
if not frappe.db.exists("Website Content", name):
doc = frappe.get_doc({
"doctype": "Website Content",
"page_slug": item["page_slug"],
"section_key": item["section_key"],
"display_order": item.get("display_order", 0),
"is_active": 1,
"content_ro": item.get("content_ro", ""),
"content_en": item.get("content_en", ""),
"extra_data": item.get("extra_data", ""),
})
doc.insert(ignore_permissions=True)
def add_custom_fields():
"""Add custom fields to existing DocTypes (Lead, Customer)."""
print("[4/4] Adding custom fields to Lead and Customer...")
custom_fields = {
"Lead": [
{"fieldname": "source_form", "label": "Source Form", "fieldtype": "Data",
"insert_after": "source", "description": "Which website form generated this lead"},
{"fieldname": "page_origin", "label": "Page Origin", "fieldtype": "Data",
"insert_after": "source_form", "description": "URL of the page where lead was captured"},
{"fieldname": "utm_source", "label": "UTM Source", "fieldtype": "Data",
"insert_after": "page_origin"},
{"fieldname": "utm_medium", "label": "UTM Medium", "fieldtype": "Data",
"insert_after": "utm_source"},
{"fieldname": "utm_campaign", "label": "UTM Campaign", "fieldtype": "Data",
"insert_after": "utm_medium"},
],
"Customer": [
{"fieldname": "didi_user_id", "label": "DiDi User ID", "fieldtype": "Data",
"insert_after": "customer_name", "unique": 1,
"description": "User ID in DiDi IAM (Keycloak)"},
{"fieldname": "iam_role", "label": "IAM Role", "fieldtype": "Select",
"options": "\nfree_tier\npaid_tier\nenterprise_tier",
"insert_after": "didi_user_id", "description": "Current role in DiDi IAM"},
{"fieldname": "active_plan", "label": "Active Plan", "fieldtype": "Link",
"options": "Subscription Plan", "insert_after": "iam_role"},
{"fieldname": "plan_activation_date", "label": "Plan Activation Date",
"fieldtype": "Date", "insert_after": "active_plan"},
],
}
for dt, fields in custom_fields.items():
for field_data in fields:
field_name = f"{dt}-{field_data['fieldname']}"
if not frappe.db.exists("Custom Field", field_name):
cf = frappe.get_doc({
"doctype": "Custom Field",
"dt": dt,
**field_data
})
cf.insert(ignore_permissions=True)
print(f" Added {dt}.{field_data['fieldname']}")
print(" Custom fields added.")

View file

@ -0,0 +1,126 @@
"""
Cleanup ERPNext: hide unused modules, configure sidebar, check users/permissions.
"""
import frappe
def execute():
cleanup_modules()
cleanup_users()
setup_workspace()
frappe.db.commit()
print("\n=== Cleanup Complete ===\n")
def cleanup_modules():
"""Hide modules that are not needed from the sidebar."""
print("[1/3] Hiding unused modules...")
# Modules to KEEP visible
keep_modules = {
"Accounts", "Selling", "CRM", "Buying",
"Didi Custom", "Setup", "Core",
}
# Modules to HIDE
hide_modules = {
"Stock", "Manufacturing", "Assets", "Projects",
"Quality Management", "Subcontracting", "Support",
"Maintenance", "EDI", "Telephony", "Portal",
"Bulk Transaction", "ERPNext Integrations",
"Social", "Website",
}
# Use Module Profile to restrict what Administrator sees
# Actually in ERPNext v15, we use "Allowed Modules" on User
# But for Administrator we can't restrict. Instead, we hide workspaces.
# Hide workspaces for unused modules
workspaces_to_hide = [
"Stock", "Manufacturing", "Assets", "Projects",
"Quality", "Subcontracting", "Support",
"HR", "Payroll", "Website",
]
for ws_name in workspaces_to_hide:
ws_list = frappe.get_all("Workspace", filters={"name": ["like", f"%{ws_name}%"]})
for ws in ws_list:
doc = frappe.get_doc("Workspace", ws.name)
if doc.public:
doc.public = 0
doc.save(ignore_permissions=True)
print(f" Hidden workspace: {ws.name}")
print(" Module cleanup done.")
def cleanup_users():
"""Verify users and their roles."""
print("[2/3] Checking users...")
users = frappe.get_all("User",
filters={"user_type": "System User", "enabled": 1},
fields=["name", "full_name", "user_type"]
)
for u in users:
roles = frappe.get_all("Has Role",
filters={"parent": u.name},
fields=["role"]
)
role_names = [r.role for r in roles]
print(f" User: {u.name} ({u.full_name})")
print(f" Roles: {', '.join(role_names)}")
# Ensure website_api user has minimal roles
api_user = "website_api@didi.localhost"
if frappe.db.exists("User", api_user):
user_doc = frappe.get_doc("User", api_user)
current_roles = [r.role for r in user_doc.roles]
print(f"\n API User roles: {current_roles}")
# Remove unnecessary roles if any
wanted_roles = {"Website Integration"}
unwanted = set(current_roles) - wanted_roles - {"All", "Guest"}
if unwanted:
for role_name in unwanted:
user_doc.roles = [r for r in user_doc.roles if r.role != role_name]
user_doc.save(ignore_permissions=True)
print(f" Removed unwanted roles: {unwanted}")
print(" Users OK.")
def setup_workspace():
"""Create a clean DiDi workspace with only relevant shortcuts."""
print("[3/3] Setting up DiDi workspace...")
ws_name = "DiDi"
if frappe.db.exists("Workspace", ws_name):
frappe.delete_doc("Workspace", ws_name, force=True)
ws = frappe.get_doc({
"doctype": "Workspace",
"name": ws_name,
"label": "DiDi",
"title": "DiDi",
"module": "Didi Custom",
"icon": "home",
"public": 1,
"sequence_id": 1,
"shortcuts": [
{"type": "DocType", "link_to": "Customer", "label": "Clienti", "color": "#2563eb"},
{"type": "DocType", "link_to": "Lead", "label": "Lead-uri", "color": "#7c3aed"},
{"type": "DocType", "link_to": "Sales Invoice", "label": "Facturi", "color": "#059669"},
{"type": "DocType", "link_to": "Subscription Plan", "label": "Planuri Abonament", "color": "#d97706"},
{"type": "DocType", "link_to": "Subscription", "label": "Subscriptii", "color": "#dc2626"},
{"type": "DocType", "link_to": "Website Content", "label": "Continut Website (CMS)", "color": "#0891b2"},
{"type": "DocType", "link_to": "Service Agreement", "label": "Acorduri Servicii", "color": "#4f46e5"},
{"type": "DocType", "link_to": "Payment Log", "label": "Log Plati", "color": "#be185d"},
{"type": "DocType", "link_to": "Supplier", "label": "Furnizori", "color": "#64748b"},
{"type": "DocType", "link_to": "Email Template", "label": "Template-uri Email", "color": "#475569"},
],
})
ws.insert(ignore_permissions=True)
print(f" Created workspace: DiDi (with shortcuts + number cards)")

View file

@ -0,0 +1,397 @@
"""
Populate ERPNext Website Content with real DiDi platform content.
All text content is managed here - website pulls from ERPNext CMS.
"""
import frappe
import json
def execute():
populate_homepage()
populate_services()
populate_about()
populate_contact()
populate_privacy()
populate_terms()
frappe.db.commit()
print("\n=== Content populated ===\n")
def upsert(page_slug, section_key, content_ro, content_en, display_order=0, extra_data=""):
"""Create or update Website Content entry."""
name = f"{page_slug}-{section_key}"
if frappe.db.exists("Website Content", name):
doc = frappe.get_doc("Website Content", name)
doc.content_ro = content_ro
doc.content_en = content_en
doc.display_order = display_order
doc.extra_data = extra_data
doc.is_active = 1
doc.save(ignore_permissions=True)
else:
doc = frappe.get_doc({
"doctype": "Website Content",
"page_slug": page_slug,
"section_key": section_key,
"content_ro": content_ro,
"content_en": content_en,
"display_order": display_order,
"extra_data": extra_data,
"is_active": 1,
})
doc.insert(ignore_permissions=True)
print(f" {name}")
def populate_homepage():
print("[1/6] Homepage...")
upsert("homepage", "hero_title",
"Combate dezinformarea cu inteligenta artificiala",
"Fight disinformation with artificial intelligence", 1)
upsert("homepage", "hero_subtitle",
"Platforma DiDi de la Clossers analizeaza automat continut media — text, imagini, audio si video — pentru a detecta dezinformarea si a verifica informatiile in timp real.",
"The DiDi platform by Clossers automatically analyzes media content — text, images, audio and video — to detect disinformation and verify information in real time.", 2)
upsert("homepage", "hero_cta",
"Incepe gratuit", "Start free", 3,
json.dumps({"link": "/pricing", "secondary_text_ro": "Descopera serviciile", "secondary_text_en": "Discover services", "secondary_link": "/services"}))
upsert("homepage", "features",
"Servicii de detectie si analiza", "Detection and analysis services", 10,
json.dumps([
{"icon": "text", "title_ro": "Analiza Text & NLP", "title_en": "Text Analysis & NLP",
"desc_ro": "Detectie automata a propagandei, limbajului manipulativ si a informatiilor false din articole si postari",
"desc_en": "Automatic detection of propaganda, manipulative language and false information from articles and posts"},
{"icon": "image", "title_ro": "Detectie Deepfake", "title_en": "Deepfake Detection",
"desc_ro": "Identificare imagini si videoclipuri generate sau manipulate prin AI, inclusiv face-swap si lip-sync",
"desc_en": "Identification of AI-generated or manipulated images and videos, including face-swap and lip-sync"},
{"icon": "check", "title_ro": "Fact-Checking Automat", "title_en": "Automated Fact-Checking",
"desc_ro": "Verificare automata a afirmatiilor cu surse oficiale, baze de date credibile si articole de presa verificate",
"desc_en": "Automatic verification of claims with official sources, credible databases and verified press articles"},
{"icon": "source", "title_ro": "Evaluare Surse", "title_en": "Source Assessment",
"desc_ro": "Scor de credibilitate pentru domenii web, publicatii si conturi social media cu analiza in profunzime",
"desc_en": "Credibility score for web domains, publications and social media accounts with in-depth analysis"},
]))
upsert("homepage", "how_it_works",
"Cum functioneaza DiDi", "How DiDi works", 20,
json.dumps([
{"step": 1, "title_ro": "Incarca continutul", "title_en": "Upload content",
"desc_ro": "Trimite un text, URL, imagine sau video pentru analiza. Suportam toate formatele majore.", "desc_en": "Submit a text, URL, image or video for analysis. We support all major formats."},
{"step": 2, "title_ro": "Analiza AI multi-model", "title_en": "Multi-model AI analysis",
"desc_ro": "6 module AI specializate ruleaza in paralel: analiza text, detectie deepfake, fact-checking, evaluare surse, detectie tehnici de manipulare si monitorizare.", "desc_en": "6 specialized AI modules run in parallel: text analysis, deepfake detection, fact-checking, source assessment, manipulation technique detection and monitoring."},
{"step": 3, "title_ro": "Validare Human-in-the-Loop", "title_en": "Human-in-the-Loop validation",
"desc_ro": "Expertii nostri verifica si valideaza rezultatele AI pentru acuratete maxima. Combinam viteza AI cu expertiza umana.", "desc_en": "Our experts verify and validate AI results for maximum accuracy. We combine AI speed with human expertise."},
{"step": 4, "title_ro": "Raport detaliat", "title_en": "Detailed report",
"desc_ro": "Primesti un raport complet: scor de incredere, dovezi, surse, tehnici identificate si recomandari. Export PDF disponibil.", "desc_en": "Receive a complete report: confidence score, evidence, sources, identified techniques and recommendations. PDF export available."},
]))
upsert("homepage", "stats",
"DiDi in cifre", "DiDi in numbers", 30,
json.dumps([
{"value": "6", "label_ro": "Module AI specializate", "label_en": "Specialized AI modules"},
{"value": "18+", "label_ro": "Tehnici de manipulare detectate", "label_en": "Manipulation techniques detected"},
{"value": "<30s", "label_ro": "Timp mediu analiza", "label_en": "Average analysis time"},
{"value": "99.5%", "label_ro": "Disponibilitate platforma", "label_en": "Platform availability"},
]))
upsert("homepage", "use_cases",
"Cine foloseste DiDi?", "Who uses DiDi?", 40,
json.dumps([
{"title_ro": "Redactii & Jurnalisti", "title_en": "Newsrooms & Journalists",
"desc_ro": "Verificare rapida a informatiilor inainte de publicare. Detectie surse nesigure si continut manipulat.",
"desc_en": "Quick information verification before publishing. Detection of unreliable sources and manipulated content."},
{"title_ro": "Agentii de comunicare", "title_en": "Communication agencies",
"desc_ro": "Monitorizare spatiu mediatic, analiza narrativa si protectie brand impotriva dezinformarii.",
"desc_en": "Media space monitoring, narrative analysis and brand protection against disinformation."},
{"title_ro": "Institutii publice", "title_en": "Public institutions",
"desc_ro": "Detectie campanii de dezinformare care vizeaza institutii, alegeri sau politici publice.",
"desc_en": "Detection of disinformation campaigns targeting institutions, elections or public policies."},
{"title_ro": "Cercetatori", "title_en": "Researchers",
"desc_ro": "API si instrumente de analiza pentru studiul fenomenului dezinformarii in proiecte academice.",
"desc_en": "API and analysis tools for studying the disinformation phenomenon in academic projects."},
{"title_ro": "ONG-uri & Fact-checkeri", "title_en": "NGOs & Fact-checkers",
"desc_ro": "Automatizare si accelerare a procesului de verificare cu instrumente profesionale.",
"desc_en": "Automation and acceleration of the verification process with professional tools."},
{"title_ro": "Companii", "title_en": "Companies",
"desc_ro": "Protectia reputatiei impotriva stirilor false. Monitorizare si alerte automate.",
"desc_en": "Reputation protection against fake news. Automated monitoring and alerts."},
]))
def populate_services():
print("[2/6] Services...")
modules_data = [
{"id": "text-analysis", "order": 1,
"title_ro": "Analiza Text & NLP", "title_en": "Text Analysis & NLP",
"subtitle_ro": "Detectie automata a dezinformarii din articole, postari si documente",
"subtitle_en": "Automatic detection of disinformation from articles, posts and documents",
"desc_ro": "Motorul nostru de procesare a limbajului natural analizeaza textul la nivel semantic, identificand tipare de manipulare, limbaj emotional excesiv, afirmatii neverificabile si inconsistente factuale. Suporta limba romana si engleza cu modele antrenate pe corpus media local.",
"desc_en": "Our natural language processing engine analyzes text at semantic level, identifying manipulation patterns, excessive emotional language, unverifiable claims and factual inconsistencies. Supports Romanian and English with models trained on local media corpus.",
"capabilities_ro": ["Detectie propaganda si limbaj manipulativ", "Analiza sentimentului si a tonului emotional", "Identificare afirmatii factuale vs. opinii", "Detectie clickbait si titluri inselatoare", "Analiza coerenta narativa", "Suport limba romana si engleza"],
"capabilities_en": ["Propaganda and manipulative language detection", "Sentiment and emotional tone analysis", "Factual claims vs opinions identification", "Clickbait and misleading headline detection", "Narrative coherence analysis", "Romanian and English support"]},
{"id": "deepfake", "order": 2,
"title_ro": "Detectie Deepfake & Manipulare Media", "title_en": "Deepfake & Media Manipulation Detection",
"subtitle_ro": "Identificare imagini si videoclipuri manipulate prin AI",
"subtitle_en": "Identification of AI-manipulated images and videos",
"desc_ro": "Modulul de analiza vizuala foloseste retele neuronale convolutionale si transformere vizuale pentru a detecta manipulari la nivel de pixel, artefacte de generare AI, inconsistente in iluminare si geometrie faciala.",
"desc_en": "The visual analysis module uses convolutional neural networks and visual transformers to detect pixel-level manipulations, AI generation artifacts, lighting inconsistencies and facial geometry.",
"capabilities_ro": ["Detectie imagini generate de AI (DALL-E, Midjourney, Stable Diffusion)", "Detectie deepfake video (face-swap, lip-sync)", "Analiza artefacte JPEG si metadata EXIF", "Detectie splice, copy-move, inpainting", "Verificare autenticitate fotografii", "Analiza consistenta iluminare si umbre"],
"capabilities_en": ["AI-generated image detection (DALL-E, Midjourney, Stable Diffusion)", "Deepfake video detection (face-swap, lip-sync)", "JPEG artifacts and EXIF metadata analysis", "Splice, copy-move, inpainting detection", "Photo authenticity verification", "Lighting and shadow consistency analysis"]},
{"id": "fact-checking", "order": 3,
"title_ro": "Fact-Checking Automat", "title_en": "Automated Fact-Checking",
"subtitle_ro": "Verificare automata a afirmatiilor cu surse credibile",
"subtitle_en": "Automatic claim verification with credible sources",
"desc_ro": "Sistemul de fact-checking extrage automat afirmatiile verificabile din text, le compara cu baze de date de fapte verificate, surse oficiale si articole de presa credibile. Foloseste tehnici de retrieval-augmented generation (RAG) si reranking.",
"desc_en": "The fact-checking system automatically extracts verifiable claims from text, compares them with verified fact databases, official sources and credible press articles. Uses retrieval-augmented generation (RAG) and reranking techniques.",
"capabilities_ro": ["Extractie automata a afirmatiilor verificabile", "Cautare in baze de date de fact-checking", "Verificare cu surse oficiale (INS, Eurostat, OMS)", "Scor de credibilitate cu explicatii", "Linkuri directe catre surse", "Istoric verificari"],
"capabilities_en": ["Automatic verifiable claim extraction", "Fact-checking database search", "Official source verification (INS, Eurostat, WHO)", "Credibility score with explanations", "Direct source links", "Verification history"]},
{"id": "source-assessment", "order": 4,
"title_ro": "Evaluare Surse & Domenii", "title_en": "Source & Domain Assessment",
"subtitle_ro": "Analiza credibilitatii surselor de informare",
"subtitle_en": "Information source credibility analysis",
"desc_ro": "Modulul de evaluare a surselor analizeaza domenii web, publicatii si conturi de social media pentru a determina nivelul de credibilitate. Combina indicatori tehnici cu indicatori editoriali.",
"desc_en": "The source assessment module analyzes web domains, publications and social media accounts to determine credibility level. Combines technical and editorial indicators.",
"capabilities_ro": ["Scor credibilitate domeniu (0-100)", "Verificare WHOIS si istoric domeniu", "Analiza retea de link-uri", "Detectie site-uri clone si typosquatting", "Monitorizare reputatie", "Baza de date surse de dezinformare"],
"capabilities_en": ["Domain credibility score (0-100)", "WHOIS and domain history verification", "Link network analysis", "Clone sites and typosquatting detection", "Reputation monitoring", "Disinformation source database"]},
{"id": "techniques", "order": 5,
"title_ro": "Detectie Tehnici de Manipulare", "title_en": "Manipulation Technique Detection",
"subtitle_ro": "Identificarea tehnicilor de propaganda si dezinformare",
"subtitle_en": "Identification of propaganda and disinformation techniques",
"desc_ro": "Bazat pe taxonomia tehnicilor de propaganda dezvoltata in cercetare academica, acest modul identifica si clasifica tehnicile specifice de manipulare folosite intr-un text. De la apel la emotii pana la whataboutism si cherry-picking.",
"desc_en": "Based on the propaganda techniques taxonomy developed in academic research, this module identifies and classifies specific manipulation techniques used in text. From appeal to emotions to whataboutism and cherry-picking.",
"capabilities_ro": ["Clasificare pe 18+ tehnici de propaganda", "Apel la emotii (frica, furie, empatie falsa)", "Apel la autoritate falsa sau anonima", "Generalizari pripite si stereotipuri", "Whataboutism si false echivalente", "Cherry-picking si scoatere din context"],
"capabilities_en": ["Classification of 18+ propaganda techniques", "Appeal to emotions (fear, anger, false empathy)", "Appeal to false or anonymous authority", "Hasty generalizations and stereotypes", "Whataboutism and false equivalences", "Cherry-picking and out-of-context"]},
{"id": "monitoring", "order": 6,
"title_ro": "Monitorizare Media & Alerte", "title_en": "Media Monitoring & Alerts",
"subtitle_ro": "Supraveghere continua a spatiului informational",
"subtitle_en": "Continuous information space surveillance",
"desc_ro": "Sistemul de monitorizare scaneaza in timp real surse media, retele sociale si site-uri de stiri pentru a identifica narrativa dezinformante emergente. Alertele automate notifica echipa cand se detecteaza campanii coordonate.",
"desc_en": "The monitoring system scans media sources, social networks and news sites in real time to identify emerging disinformation narratives. Automatic alerts notify the team when coordinated campaigns are detected.",
"capabilities_ro": ["Monitorizare 24/7 surse media si social media", "Detectie narrativa emergente si trenduri", "Alerte automate pe email si in dashboard", "Identificare campanii coordonate", "Harta de raspandire a dezinformarii", "Rapoarte periodice automate"],
"capabilities_en": ["24/7 media and social media monitoring", "Emerging narrative and trend detection", "Automatic email and dashboard alerts", "Coordinated campaign identification", "Disinformation spread map", "Automatic periodic reports"]},
]
for mod in modules_data:
upsert("services", f"module_{mod['id']}",
f"<h3>{mod['title_ro']}</h3><p class='subtitle'>{mod['subtitle_ro']}</p><p>{mod['desc_ro']}</p>",
f"<h3>{mod['title_en']}</h3><p class='subtitle'>{mod['subtitle_en']}</p><p>{mod['desc_en']}</p>",
mod["order"],
json.dumps({
"id": mod["id"],
"title_ro": mod["title_ro"], "title_en": mod["title_en"],
"subtitle_ro": mod["subtitle_ro"], "subtitle_en": mod["subtitle_en"],
"desc_ro": mod["desc_ro"], "desc_en": mod["desc_en"],
"capabilities_ro": mod["capabilities_ro"],
"capabilities_en": mod["capabilities_en"],
}))
def populate_about():
print("[3/6] About...")
upsert("about", "hero",
"<h1>Despre Clossers si platforma DiDi</h1><p>TOP CLOSSERS SRL dezvolta solutii digitale inovatoare pentru combaterea dezinformarii si protectia spatiului informational romanesc si european.</p>",
"<h1>About Clossers and the DiDi platform</h1><p>TOP CLOSSERS SRL develops innovative digital solutions for combating disinformation and protecting the Romanian and European information space.</p>", 1)
upsert("about", "mission",
"<h2>Misiunea noastra</h2><p>DiDi (Platforma Digitala Inteligenta pentru Prevenirea si Combaterea Dezinformarii) este raspunsul nostru la una dintre cele mai mari provocari ale erei digitale: dezinformarea. Combinam inteligenta artificiala de ultima generatie cu expertiza umana pentru a oferi instrumente profesionale de verificare a informatiilor.</p>",
"<h2>Our mission</h2><p>DiDi (Intelligent Digital Platform for Preventing and Combating Disinformation) is our answer to one of the biggest challenges of the digital era: disinformation. We combine cutting-edge artificial intelligence with human expertise to provide professional information verification tools.</p>", 2)
upsert("about", "project",
"<h2>Proiect PNRR</h2><p>Platforma DiDi este dezvoltata in cadrul proiectului finantat prin Planul National de Redresare si Rezilienta (PNRR), Componenta C9 - Suport pentru sectorul privat, cercetare, dezvoltare si inovare, Investitia I3 - Scheme de ajutor pentru sectorul privat. Contract de finantare nr. 11.1.i3.c9/10.05.2024.</p>",
"<h2>PNRR Project</h2><p>The DiDi platform is developed within the project funded by the National Recovery and Resilience Plan (NRRP), Component C9 - Support for private sector, research, development and innovation, Investment I3 - Aid schemes for private sector. Funding contract no. 11.1.i3.c9/10.05.2024.</p>", 3)
upsert("about", "technology",
"<h2>Tehnologie</h2><p>Platforma DiDi integreaza multiple modele AI/ML intr-o arhitectura de orchestrare multi-model. Fiecare tip de analiza (text, imagine, video, fact-checking) este procesat de module specializate care ruleaza in paralel, oferind rezultate in timp real. Arhitectura este containerizata, scalabila si gazduita in cloud pe servere din Uniunea Europeana.</p>",
"<h2>Technology</h2><p>The DiDi platform integrates multiple AI/ML models in a multi-model orchestration architecture. Each type of analysis (text, image, video, fact-checking) is processed by specialized modules running in parallel, providing real-time results. The architecture is containerized, scalable and hosted in cloud on EU servers.</p>", 4)
upsert("about", "company",
"", "", 5,
json.dumps({
"name": "TOP CLOSSERS SRL",
"cui": "36193026",
"reg_com": "J2022000345035",
"address": "Str. Targovistei 15, Bl. 2, Et. 3, Ap. 22, Ploiesti, Prahova 100299",
"email": "office@clossers.com",
"phone": "+40721063078",
"website": "https://clossers.com",
}))
def populate_contact():
print("[4/6] Contact...")
upsert("contact", "title",
"<h1>Contacteaza-ne</h1><p>Ai intrebari despre platforma DiDi sau vrei sa discutam despre nevoile tale? Echipa noastra iti sta la dispozitie.</p>",
"<h1>Contact us</h1><p>Have questions about the DiDi platform or want to discuss your needs? Our team is here to help.</p>", 1)
upsert("contact", "info", "", "", 2,
json.dumps({
"email": "office@clossers.com",
"phone": "+40721063078",
"address_ro": "Str. Targovistei 15, Bl. 2, Et. 3, Ap. 22\nPloiesti, Prahova 100299\nRomania",
"address_en": "15 Targovistei St., Bl. 2, Fl. 3, Ap. 22\nPloiesti, Prahova 100299\nRomania",
"schedule_ro": "Luni - Vineri, 09:00 - 18:00",
"schedule_en": "Monday - Friday, 09:00 - 18:00",
}))
def populate_privacy():
print("[5/6] Privacy...")
upsert("privacy", "content",
"""<h1>Politica de Confidentialitate</h1>
<p><em>Ultima actualizare: 30 martie 2026</em></p>
<h2>1. Introducere</h2>
<p>TOP CLOSSERS SRL (CUI: 36193026), cu sediul in Ploiesti, Str. Targovistei 15, opereaza platforma DiDi si site-ul clossers.com. Respectam confidentialitatea datelor dumneavoastra personale si ne conformam Regulamentului General privind Protectia Datelor (GDPR - Regulamentul UE 2016/679).</p>
<h2>2. Date colectate</h2>
<p>Colectam urmatoarele categorii de date:</p>
<ul>
<li><strong>Date de identificare:</strong> nume, prenume, adresa email, numar telefon, denumire companie, CUI</li>
<li><strong>Date de autentificare:</strong> gestionate prin Keycloak (Identity and Access Management) - nu stocam parole</li>
<li><strong>Date de utilizare:</strong> continut incarcat pentru analiza, istoricul analizelor, credite consumate</li>
<li><strong>Date de plata:</strong> gestionate de Stripe - nu stocam date de card</li>
<li><strong>Date tehnice:</strong> adresa IP, tip browser, cookie-uri (conform politicii de cookie-uri)</li>
</ul>
<h2>3. Scopul prelucrarii</h2>
<ul>
<li>Furnizarea serviciilor platformei DiDi (analiza continut media)</li>
<li>Gestionarea contului si a abonamentului</li>
<li>Facturare si evidenta contabila</li>
<li>Comunicari tranzactionale (facturi, notificari cont)</li>
<li>Imbunatatirea serviciilor si suport tehnic</li>
</ul>
<h2>4. Temeiul legal</h2>
<ul>
<li>Executarea contractului (furnizarea serviciilor)</li>
<li>Obligatii legale (facturare, contabilitate)</li>
<li>Consimtamant (cookie-uri analitice, comunicari marketing)</li>
<li>Interes legitim (securitate, prevenire frauda)</li>
</ul>
<h2>5. Stocarea datelor</h2>
<p>Toate datele sunt stocate exclusiv pe servere localizate in Uniunea Europeana, conform cerintelor GDPR. Datele sunt criptate in tranzit (TLS 1.2+) si la repaus (AES-256).</p>
<h2>6. Drepturile dumneavoastra</h2>
<p>Conform GDPR, aveti dreptul la:</p>
<ul>
<li><strong>Acces</strong> - puteti vizualiza datele din dashboard-ul contului</li>
<li><strong>Rectificare</strong> - puteti modifica datele din profilul contului</li>
<li><strong>Stergere</strong> - puteti solicita stergerea contului din dashboard (sectiunea Profil)</li>
<li><strong>Portabilitate</strong> - puteti exporta datele in format CSV din dashboard</li>
<li><strong>Opozitie</strong> - puteti retrage consimtamantul pentru cookie-uri analitice</li>
</ul>
<h2>7. Contact DPO</h2>
<p>Pentru exercitarea drepturilor sau intrebari privind protectia datelor: <strong>office@clossers.com</strong></p>""",
"""<h1>Privacy Policy</h1>
<p><em>Last updated: March 30, 2026</em></p>
<h2>1. Introduction</h2>
<p>TOP CLOSSERS SRL (Tax ID: 36193026), headquartered in Ploiesti, 15 Targovistei St., operates the DiDi platform and clossers.com website. We respect the privacy of your personal data and comply with the General Data Protection Regulation (GDPR - EU Regulation 2016/679).</p>
<h2>2. Data collected</h2>
<ul>
<li><strong>Identification data:</strong> name, email, phone, company name, tax ID</li>
<li><strong>Authentication data:</strong> managed by Keycloak (IAM) - we do not store passwords</li>
<li><strong>Usage data:</strong> content uploaded for analysis, analysis history, credits consumed</li>
<li><strong>Payment data:</strong> managed by Stripe - we do not store card data</li>
<li><strong>Technical data:</strong> IP address, browser type, cookies</li>
</ul>
<h2>3. Purpose</h2>
<ul>
<li>Providing DiDi platform services</li>
<li>Account and subscription management</li>
<li>Invoicing and accounting</li>
<li>Transactional communications</li>
<li>Service improvement and support</li>
</ul>
<h2>4. Your rights</h2>
<p>Under GDPR: access, rectification, erasure, portability, objection. Contact: office@clossers.com</p>""", 1)
def populate_terms():
print("[6/6] Terms...")
upsert("terms", "content",
"""<h1>Termeni si Conditii</h1>
<p><em>Ultima actualizare: 30 martie 2026</em></p>
<h2>1. Definitii</h2>
<ul>
<li><strong>Furnizor:</strong> TOP CLOSSERS SRL, CUI 36193026, operator al platformei DiDi</li>
<li><strong>Utilizator:</strong> persoana fizica sau juridica care acceseaza si utilizeaza serviciile DiDi</li>
<li><strong>Platforma:</strong> platforma digitala DiDi accesibila prin intermediul site-ului clossers.com</li>
<li><strong>Servicii:</strong> serviciile de analiza si detectie a dezinformarii oferite prin platforma DiDi</li>
</ul>
<h2>2. Servicii oferite</h2>
<p>Platforma DiDi ofera servicii de analiza automata a continutului media (text, imagini, audio, video) pentru detectarea dezinformarii, prin intermediul unor module AI specializate: analiza text, detectie deepfake, fact-checking automat, evaluare surse, detectie tehnici de manipulare si monitorizare media.</p>
<h2>3. Inregistrare si cont</h2>
<p>Accesul la servicii necesita crearea unui cont. Utilizatorul este responsabil pentru securitatea credentialelor de acces si pentru toate activitatile desfasurate prin contul sau.</p>
<h2>4. Planuri si plata</h2>
<p>Serviciile sunt disponibile in mai multe planuri (Free, Paid, Enterprise). Platile sunt procesate securizat prin Stripe. Facturile sunt generate automat si disponibile in dashboard.</p>
<h2>5. Utilizare acceptabila</h2>
<p>Utilizatorul se obliga sa nu foloseasca platforma pentru:</p>
<ul>
<li>Activitati ilegale sau care incalca drepturile tertilor</li>
<li>Incarcarea de continut ilegal sau care incalca drepturile de autor</li>
<li>Tentative de compromitere a securitatii platformei</li>
<li>Utilizare automata abuziva (scraping, DDoS)</li>
</ul>
<h2>6. Proprietate intelectuala</h2>
<p>Platforma DiDi, algoritmii, modelele AI si documentatia sunt proprietatea TOP CLOSSERS SRL. Utilizatorul pastreaza drepturile asupra continutului incarcat pentru analiza.</p>
<h2>7. Limitarea raspunderii</h2>
<p>Rezultatele analizelor DiDi sunt furnizate ca instrumente de suport decizional si nu constituie adevar absolut. TOP CLOSSERS SRL nu raspunde pentru decizii luate exclusiv pe baza rezultatelor platformei.</p>
<h2>8. Legislatie aplicabila</h2>
<p>Prezentii termeni sunt guvernati de legislatia din Romania. Orice litigiu va fi solutionat de instantele competente din Ploiesti, Romania.</p>
<h2>9. Contact</h2>
<p>office@clossers.com | +40 721 063 078</p>""",
"""<h1>Terms and Conditions</h1>
<p><em>Last updated: March 30, 2026</em></p>
<h2>1. Definitions</h2>
<ul>
<li><strong>Provider:</strong> TOP CLOSSERS SRL, Tax ID 36193026, operator of DiDi platform</li>
<li><strong>User:</strong> person or entity accessing DiDi services</li>
<li><strong>Platform:</strong> DiDi digital platform accessible via clossers.com</li>
</ul>
<h2>2. Services</h2>
<p>DiDi provides automated media content analysis services for disinformation detection through specialized AI modules.</p>
<h2>3. Payment</h2>
<p>Services available in Free, Paid, Enterprise plans. Payments processed via Stripe. Invoices generated automatically.</p>
<h2>4. Acceptable use</h2>
<p>Users must not use the platform for illegal activities, copyright infringement, security attacks, or abusive automation.</p>
<h2>5. Governing law</h2>
<p>Romanian law applies. Disputes resolved by Ploiesti courts.</p>
<h2>6. Contact</h2>
<p>office@clossers.com</p>""", 1)

View file

@ -0,0 +1,244 @@
"""
Set ERPNext to Romanian + add custom translations for DiDi labels.
Run: python setup-translations.py
"""
import requests
import json
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
URL = "http://localhost:8080"
s = requests.Session()
login = s.post(f"{URL}/api/method/login", data={"usr": "Administrator", "pwd": "admin"})
if login.status_code != 200:
print("Login failed"); exit(1)
print("Logged in as Administrator")
# ── 1. Set system language to Romanian ──
print("\n1. Setting system language to Romanian...")
r = s.put(f"{URL}/api/resource/System%20Settings/System%20Settings", json={
"language": "ro",
"date_format": "dd.mm.yyyy",
"time_format": "HH:mm:ss",
"number_format": "#.###,##",
"country": "Romania",
"first_day_of_the_week": "Monday",
})
print(f" System Settings: {r.status_code}")
# ── 2. Set Administrator language to RO ──
print("\n2. Setting Administrator language...")
r = s.put(f"{URL}/api/resource/User/Administrator", json={"language": "ro"})
print(f" Admin language: {r.status_code}")
# Also API user
r = s.put(f"{URL}/api/resource/User/website_api@didi-erp", json={"language": "ro"})
print(f" API user language: {r.status_code}")
# ── 3. Custom translations for DiDi labels ──
print("\n3. Adding custom translations...")
TRANSLATIONS = {
# DocType names
"Sales Invoice": "Factura de Vanzare",
"Customer": "Client",
"Lead": "Lead",
"Payment Log": "Jurnal Plati",
"Payment Entry": "Incasare",
"Service Agreement": "Acord Servicii",
"Website Content": "Continut Website",
"Analysis Report": "Raport Analiza",
"Subscription Plan": "Plan Abonament",
"Item": "Articol / Serviciu",
"Item Group": "Grup Articole",
"Supplier": "Furnizor",
"Company": "Companie",
"Account": "Cont Contabil",
"Purchase Invoice": "Factura de Achizitie",
"Email Template": "Sablon Email",
"Sales Taxes and Charges Template": "Sablon Taxe (TVA)",
"Lead Source": "Sursa Lead",
"Sales Stage": "Etapa Vanzare",
# Common fields
"Customer Name": "Nume Client",
"Customer Type": "Tip Client",
"Customer Group": "Grup Clienti",
"Territory": "Teritoriu",
"Grand Total": "Total General",
"Net Total": "Total Net",
"Outstanding Amount": "Suma Restanta",
"Posting Date": "Data Emiterii",
"Due Date": "Data Scadenta",
"Status": "Status",
"Currency": "Moneda",
"Paid": "Platita",
"Unpaid": "Neplatita",
"Overdue": "Restanta",
"Cancelled": "Anulata",
"Draft": "Ciorna",
"Submitted": "Confirmata",
"Amount": "Suma",
"Rate": "Pret Unitar",
"Quantity": "Cantitate",
"Qty": "Cant.",
"Description": "Descriere",
"Total": "Total",
"Tax": "Taxa",
"Taxes": "Taxe",
"Discount": "Reducere",
"Payment Type": "Tip Plata",
"Receive": "Incasare",
# DiDi custom fields
"Analysis Consumed": "Analiza Consumata",
"Analysis Session ID": "ID Sesiune Analiza",
"DiDi User ID": "ID Utilizator DiDi",
"IAM Role": "Rol IAM",
"Active Plan": "Plan Activ",
"Plan Activation Date": "Data Activare Plan",
"Source Form": "Formular Sursa",
"Page Origin": "Pagina Origine",
# Custom DocType fields
"Acceptance Date": "Data Acceptarii",
"Client IP": "IP Client",
"Terms Version": "Versiune Termeni",
"Agreement HTML": "Continut Acord",
"Stripe Payment Intent ID": "ID Plata Stripe",
"Stripe Session ID": "ID Sesiune Stripe",
"Raw Webhook Data": "Date Brute Webhook",
"Event Type": "Tip Eveniment",
"Page Slug": "Slug Pagina",
"Section Key": "Cheie Sectiune",
"Display Order": "Ordine Afisare",
"Is Active": "Activ",
"Content RO": "Continut RO",
"Content EN": "Continut EN",
"Extra Data": "Date Extra",
"Report Title": "Titlu Raport",
"Generated At": "Generat La",
"Result JSON": "Rezultat JSON",
"PDF File": "Fisier PDF",
"Media Type": "Tip Media",
"Component": "Componenta",
# Workspace & navigation
"Home": "Acasa",
"Settings": "Setari",
"Search": "Cauta",
"Help": "Ajutor",
"Logout": "Deconectare",
"Save": "Salveaza",
"Submit": "Confirma",
"Cancel": "Anuleaza",
"Delete": "Sterge",
"New": "Nou",
"Edit": "Editeaza",
"Print": "Tipareste",
"Download": "Descarca",
"Filter": "Filtreaza",
"Sort By": "Sorteaza dupa",
"Created By": "Creat de",
"Modified By": "Modificat de",
"Owner": "Proprietar",
"Creation": "Data Creare",
"Modified": "Data Modificare",
"Actions": "Actiuni",
"Add Row": "Adauga Rand",
"Select All": "Selecteaza Tot",
# Reports
"Balance Sheet": "Bilant",
"Profit and Loss Statement": "Cont Profit si Pierdere",
"General Ledger": "Registru General",
"Trial Balance": "Balanta de Verificare",
"Accounts Receivable": "Debitori",
"Accounts Payable": "Creditori",
# CRM
"Lead Name": "Nume Lead",
"Email Address": "Adresa Email",
"Phone": "Telefon",
"Source": "Sursa",
"Campaign": "Campanie",
"Converted": "Convertit",
"Do Not Contact": "Nu Contacta",
"Replied": "Raspuns",
"Interested": "Interesat",
"Quotation": "Oferta",
"Opportunity": "Oportunitate",
# Subscription
"Plan Name": "Nume Plan",
"Billing Interval": "Interval Facturare",
"Cost": "Cost",
"Month": "Lunar",
"Year": "Anual",
# Common actions/buttons
"Amend": "Modifica",
"Duplicate": "Duplica",
"Rename": "Redenumeste",
"Reload": "Reincarca",
"Close": "Inchide",
"Yes": "Da",
"No": "Nu",
"Confirm": "Confirma",
"Apply": "Aplica",
"Clear": "Curata",
"Refresh": "Actualizeaza",
"Export": "Exporta",
"Import": "Importa",
# Misc
"Romania": "Romania",
"Commercial": "Comercial",
"Individual": "Persoana Fizica",
"Company": "Companie",
"All Item Groups": "Toate Grupurile",
"DiDi Services": "Servicii DiDi",
"Succeeded": "Reusita",
"Failed": "Esuata",
"Refunded": "Rambursata",
"Pending": "In Asteptare",
"Completed": "Finalizat",
"Accepted": "Acceptat",
"Expired": "Expirat",
}
created = 0
skipped = 0
for source, translated in TRANSLATIONS.items():
# Check if translation exists
check = s.get(f"{URL}/api/resource/Translation", params={
"filters": json.dumps([["language", "=", "ro"], ["source_text", "=", source]]),
"fields": json.dumps(["name"]),
"limit_page_length": 1,
})
existing = check.json().get("data", [])
if existing:
skipped += 1
continue
r = s.post(f"{URL}/api/resource/Translation", json={
"language": "ro",
"source_text": source,
"translated_text": translated,
})
if r.status_code in (200, 201):
created += 1
else:
print(f" Error: {source} -> {r.status_code}")
print(f" Created: {created}, Skipped (existing): {skipped}")
# ── 4. Clear cache to apply translations ──
print("\n4. Clearing cache...")
r = s.post(f"{URL}/api/method/frappe.client.clear_cache")
print(f" Cache cleared: {r.status_code}")
print(f"\nDone! {created} traduceri noi adaugate.")
print("Refresh ERPNext (Ctrl+Shift+R) pentru a vedea traducerile.")

View file

@ -0,0 +1,205 @@
"""Fix sidebar workspaces — use simple names without spaces."""
import requests
import json
ERPNEXT_URL = "http://localhost:8080"
session = requests.Session()
login = session.post(f"{ERPNEXT_URL}/api/method/login", data={"usr": "Administrator", "pwd": "admin"})
if login.status_code != 200:
print(f"Login failed"); exit(1)
print("Logged in")
def api_post(endpoint, data):
return session.post(f"{ERPNEXT_URL}{endpoint}", json=data)
def api_put(endpoint, data):
return session.put(f"{ERPNEXT_URL}{endpoint}", json=data)
def api_get(endpoint):
return session.get(f"{ERPNEXT_URL}{endpoint}")
def api_delete(endpoint):
return session.delete(f"{ERPNEXT_URL}{endpoint}")
# Delete broken workspaces first
print("\n1. Cleaning broken workspaces...")
for name in ["Clienti", "CRM DiDi", "Servicii DiDi", "Plati DiDi", "Contabilitate DiDi", "Website CMS DiDi"]:
r = api_delete(f"/api/resource/Workspace/{requests.utils.quote(name)}")
print(f" Delete {name}: {r.status_code}")
# Workspace definitions — simple slugified names
WORKSPACES = [
{
"label": "Facturi",
"title": "Facturi",
"icon": "file-text",
"indicator_color": "green",
"sequence_id": 10,
"shortcuts": [
{"type": "DocType", "link_to": "Sales Invoice", "label": "Toate Facturile", "color": "Green", "doc_view": "List"},
{"type": "DocType", "link_to": "Payment Entry", "label": "Payment Entry", "color": "Yellow", "doc_view": "List"},
],
"links": [
{"type": "Card Break", "label": "Facturi"},
{"type": "Link", "label": "Sales Invoice", "link_to": "Sales Invoice", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Payment Entry", "link_to": "Payment Entry", "link_type": "DocType"},
{"type": "Link", "label": "Taxe (TVA)", "link_to": "Sales Taxes and Charges Template", "link_type": "DocType"},
],
},
{
"label": "Clienti",
"title": "Clienti",
"icon": "users",
"indicator_color": "blue",
"sequence_id": 20,
"shortcuts": [
{"type": "DocType", "link_to": "Customer", "label": "Toti Clientii", "color": "Blue", "doc_view": "List"},
{"type": "DocType", "link_to": "Service Agreement", "label": "Acorduri Servicii", "color": "Purple", "doc_view": "List"},
],
"links": [
{"type": "Card Break", "label": "Clienti"},
{"type": "Link", "label": "Customer", "link_to": "Customer", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Service Agreement", "link_to": "Service Agreement", "link_type": "DocType"},
{"type": "Link", "label": "Subscription Plan", "link_to": "Subscription Plan", "link_type": "DocType"},
],
},
{
"label": "CRM-DiDi",
"title": "CRM",
"icon": "share",
"indicator_color": "orange",
"sequence_id": 30,
"shortcuts": [
{"type": "DocType", "link_to": "Lead", "label": "Lead-uri", "color": "Orange", "doc_view": "List"},
{"type": "DocType", "link_to": "Sales Stage", "label": "Sales Stage", "color": "Yellow"},
{"type": "DocType", "link_to": "Lead Source", "label": "Lead Source", "color": "Grey"},
],
"links": [
{"type": "Card Break", "label": "CRM"},
{"type": "Link", "label": "Lead", "link_to": "Lead", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Sales Stage", "link_to": "Sales Stage", "link_type": "DocType"},
{"type": "Link", "label": "Lead Source", "link_to": "Lead Source", "link_type": "DocType"},
],
},
{
"label": "Servicii",
"title": "Servicii",
"icon": "box",
"indicator_color": "purple",
"sequence_id": 40,
"shortcuts": [
{"type": "DocType", "link_to": "Item", "label": "Articole", "color": "Purple", "doc_view": "List"},
{"type": "DocType", "link_to": "Subscription Plan", "label": "Planuri Abonament", "color": "Cyan"},
],
"links": [
{"type": "Card Break", "label": "Catalog"},
{"type": "Link", "label": "Item", "link_to": "Item", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Item Group", "link_to": "Item Group", "link_type": "DocType"},
{"type": "Link", "label": "Subscription Plan", "link_to": "Subscription Plan", "link_type": "DocType"},
],
},
{
"label": "Plati",
"title": "Plati",
"icon": "credit-card",
"indicator_color": "yellow",
"sequence_id": 50,
"shortcuts": [
{"type": "DocType", "link_to": "Payment Log", "label": "Payment Log", "color": "Yellow", "doc_view": "List"},
{"type": "DocType", "link_to": "Analysis Report", "label": "Rapoarte Analiza", "color": "Pink", "doc_view": "List"},
],
"links": [
{"type": "Card Break", "label": "Plati"},
{"type": "Link", "label": "Payment Log", "link_to": "Payment Log", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Analysis Report", "link_to": "Analysis Report", "link_type": "DocType"},
],
},
{
"label": "Contabilitate",
"title": "Contabilitate",
"icon": "calculator",
"indicator_color": "",
"sequence_id": 60,
"shortcuts": [
{"type": "DocType", "link_to": "Account", "label": "Plan Conturi", "color": "Grey", "doc_view": "Tree"},
{"type": "DocType", "link_to": "Supplier", "label": "Furnizori", "color": "Blue"},
{"type": "DocType", "link_to": "Company", "label": "Companie", "color": "Green"},
],
"links": [
{"type": "Card Break", "label": "Contabilitate"},
{"type": "Link", "label": "Account", "link_to": "Account", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Company", "link_to": "Company", "link_type": "DocType"},
{"type": "Link", "label": "Supplier", "link_to": "Supplier", "link_type": "DocType"},
{"type": "Link", "label": "Purchase Invoice", "link_to": "Purchase Invoice", "link_type": "DocType"},
],
},
{
"label": "Website-CMS",
"title": "Website CMS",
"icon": "globe",
"indicator_color": "cyan",
"sequence_id": 70,
"shortcuts": [
{"type": "DocType", "link_to": "Website Content", "label": "Continut Website", "color": "Cyan", "doc_view": "List"},
{"type": "DocType", "link_to": "Email Template", "label": "Template Email", "color": "Grey"},
],
"links": [
{"type": "Card Break", "label": "CMS"},
{"type": "Link", "label": "Website Content", "link_to": "Website Content", "link_type": "DocType", "onboard": 1},
{"type": "Link", "label": "Email Template", "link_to": "Email Template", "link_type": "DocType"},
],
},
]
print("\n2. Creating workspaces...")
for ws in WORKSPACES:
label = ws["label"]
# Check if already exists
check = api_get(f"/api/resource/Workspace/{requests.utils.quote(label)}")
payload = {
"label": label,
"title": ws["title"],
"icon": ws["icon"],
"indicator_color": ws["indicator_color"],
"is_hidden": 0,
"public": 1,
"module": "Didi Custom",
"sequence_id": ws["sequence_id"],
"content": json.dumps([
{"id": "h1", "type": "header", "data": {"text": f"<span class=\"h4\"><b>{ws['title']}</b></span>", "col": 12}},
] + [
{"id": f"s{i}", "type": "shortcut", "data": {"shortcut_name": s["label"], "col": 4}}
for i, s in enumerate(ws["shortcuts"])
]),
"shortcuts": ws["shortcuts"],
"links": ws["links"],
}
if check.status_code == 200:
r = api_put(f"/api/resource/Workspace/{requests.utils.quote(label)}", payload)
status = "Updated" if r.status_code == 200 else f"Error {r.status_code}"
else:
r = api_post("/api/resource/Workspace", payload)
status = "Created" if r.status_code in (200, 201) else f"Error {r.status_code}"
if r.status_code not in (200, 201):
print(f" {label}: {status}{r.text[:200]}")
continue
print(f" {label}: {status}")
# Verify
print("\n3. Final sidebar:")
r = session.post(f"{ERPNEXT_URL}/api/method/frappe.client.get_list", json={
"doctype": "Workspace",
"filters": [["public", "=", 1], ["is_hidden", "=", 0]],
"fields": ["name", "title", "icon", "sequence_id"],
"order_by": "sequence_id asc",
"limit_page_length": 20,
})
for ws in r.json().get("message", []):
print(f" [{ws.get('sequence_id', '?'):>3}] {ws.get('icon', '?'):15s} {ws['title']:20s} → /app/{ws['name'].lower().replace(' ', '-')}")
print("\nDone! Ctrl+Shift+R in browser.")

View file

@ -0,0 +1,92 @@
"""Create 3 Script Reports in ERPNext for DiDi."""
import sys, io, json, requests
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
URL = "http://localhost:8080"
s = requests.Session()
s.post(f"{URL}/api/method/login", data={"usr": "Administrator", "pwd": "admin"})
print("Logged in")
REPORTS = [
{
"report_name": "DiDi Dashboard Financiar",
"ref_doctype": "Sales Invoice",
"report_type": "Script Report",
"is_standard": "No",
"module": "Accounts",
"report_script": """result = []
total = frappe.db.sql("SELECT COALESCE(SUM(grand_total),0) FROM `tabSales Invoice` WHERE docstatus=1 AND status!='Cancelled'")[0][0]
result.append({"indicator": "Total Venituri (RON)", "valoare": total})
luna = frappe.db.sql("SELECT COALESCE(SUM(grand_total),0) FROM `tabSales Invoice` WHERE docstatus=1 AND status!='Cancelled' AND MONTH(posting_date)=MONTH(CURDATE()) AND YEAR(posting_date)=YEAR(CURDATE())")[0][0]
result.append({"indicator": "Venituri Luna Curenta (RON)", "valoare": luna})
nr_facturi = frappe.db.sql("SELECT COUNT(*) FROM `tabSales Invoice` WHERE docstatus=1 AND status!='Cancelled'")[0][0]
result.append({"indicator": "Nr. Facturi Emise", "valoare": nr_facturi})
nr_platite = frappe.db.sql("SELECT COUNT(*) FROM `tabSales Invoice` WHERE docstatus=1 AND status='Paid'")[0][0]
result.append({"indicator": "Nr. Facturi Platite", "valoare": nr_platite})
nr_clienti = frappe.db.sql("SELECT COUNT(DISTINCT customer) FROM `tabSales Invoice` WHERE docstatus=1")[0][0]
result.append({"indicator": "Clienti Activi", "valoare": nr_clienti})
columns = [{"fieldname":"indicator","label":"Indicator","fieldtype":"Data","width":300},{"fieldname":"valoare","label":"Valoare","fieldtype":"Currency","width":200}]
data = result""",
},
{
"report_name": "DiDi Raport CRM",
"ref_doctype": "Lead",
"report_type": "Script Report",
"is_standard": "No",
"module": "CRM",
"report_script": """result = []
total = frappe.db.sql("SELECT COUNT(*) FROM `tabLead`")[0][0]
result.append({"metric": "Total Lead-uri", "numar": total})
surse = frappe.db.sql("SELECT IFNULL(source,'Necunoscut') as s, COUNT(*) as c FROM `tabLead` GROUP BY source ORDER BY c DESC")
for row in surse:
result.append({"metric": f"Sursa: {row[0]}", "numar": row[1]})
statusuri = frappe.db.sql("SELECT status, COUNT(*) as c FROM `tabLead` GROUP BY status ORDER BY c DESC")
for row in statusuri:
result.append({"metric": f"Status: {row[0]}", "numar": row[1]})
columns = [{"fieldname":"metric","label":"Metric","fieldtype":"Data","width":350},{"fieldname":"numar","label":"Numar","fieldtype":"Int","width":150}]
data = result""",
},
{
"report_name": "DiDi Clienti per Plan",
"ref_doctype": "Customer",
"report_type": "Script Report",
"is_standard": "No",
"module": "CRM",
"report_script": """result = []
total = frappe.db.sql("SELECT COUNT(*) FROM `tabCustomer`")[0][0]
result.append({"plan": "Total Clienti", "numar": total})
roluri = frappe.db.sql("SELECT IFNULL(NULLIF(iam_role,''),'fara_rol') as r, COUNT(*) as c FROM `tabCustomer` GROUP BY iam_role ORDER BY c DESC")
for row in roluri:
result.append({"plan": f"Rol: {row[0]}", "numar": row[1]})
planuri = frappe.db.sql("SELECT IFNULL(NULLIF(active_plan,''),'fara_plan') as p, COUNT(*) as c FROM `tabCustomer` GROUP BY active_plan ORDER BY c DESC")
for row in planuri:
result.append({"plan": f"Plan: {row[0]}", "numar": row[1]})
columns = [{"fieldname":"plan","label":"Plan / Rol","fieldtype":"Data","width":350},{"fieldname":"numar","label":"Numar","fieldtype":"Int","width":150}]
data = result""",
},
]
for rpt in REPORTS:
name = rpt["report_name"]
check = s.get(f"{URL}/api/resource/Report/{requests.utils.quote(name)}")
if check.status_code == 200:
print(f" Exists: {name}")
continue
r = s.post(f"{URL}/api/resource/Report", json=rpt)
if r.status_code in (200, 201):
print(f" Created: {name}")
else:
print(f" Error {name}: {r.status_code} {r.text[:200]}")
print("Done!")

View file

@ -0,0 +1,175 @@
"""Setup email notifications in ERPNext for DiDi.
Required env vars:
SENDGRID_API_KEY SendGrid SMTP credential (starts with `SG.`)
ERPNEXT_ADMIN_PASSWORD ERPNext Administrator password (default: admin, dev only)
ERPNEXT_URL ERPNext base URL (default: http://localhost:8080)
"""
import sys, io, os, requests
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY")
if not SENDGRID_API_KEY:
print("ERROR: SENDGRID_API_KEY env var not set. Export it before running:")
print(" export SENDGRID_API_KEY='SG.your-real-key-here'")
exit(1)
URL = os.environ.get("ERPNEXT_URL", "http://localhost:8080")
ADMIN_PWD = os.environ.get("ERPNEXT_ADMIN_PASSWORD", "admin")
s = requests.Session()
r = s.post(f"{URL}/api/method/login", data={"usr": "Administrator", "pwd": ADMIN_PWD})
if r.status_code != 200:
print(f"Login failed: {r.status_code} — check ERPNEXT_ADMIN_PASSWORD")
exit(1)
print("Logged in")
def exists(doctype, name):
r = s.get(f"{URL}/api/resource/{requests.utils.quote(doctype)}/{requests.utils.quote(name)}")
return r.status_code == 200
# ── 1. Email Account (SendGrid) ──
print("\n1. Email Account...")
if exists("Email Account", "DiDi Outgoing"):
print(" Already exists")
else:
r = s.post(f"{URL}/api/resource/Email%20Account", json={
"email_account_name": "DiDi Outgoing",
"email_id": "office@clossers.com",
"smtp_server": "smtp.sendgrid.net",
"smtp_port": 587,
"use_tls": 1,
"use_ssl": 0,
"login_id_for_outgoing": 1,
"login_id": "apikey",
"password": SENDGRID_API_KEY,
"default_outgoing": 1,
"enable_outgoing": 1,
"enable_incoming": 0,
"send_notifications": 1,
"always_use_account_email_id_as_sender": 1,
"always_use_account_name_as_sender_name": 1,
"append_to": "",
})
print(f" Created: {r.status_code}")
# ── 2. Notification: Factura Emisa ──
print("\n2. Notification: Factura Emisa...")
if exists("Notification", "DiDi - Factura Emisa"):
print(" Already exists")
else:
r = s.post(f"{URL}/api/resource/Notification", json={
"name": "DiDi - Factura Emisa",
"subject": "Factura {{ doc.name }} - {{ doc.grand_total }} {{ doc.currency }}",
"document_type": "Sales Invoice",
"event": "Submit",
"channel": "Email",
"attach_print": 1,
"print_format": "DiDi Invoice",
"condition": "doc.docstatus == 1",
"recipients": [
{
"receiver_by_document_field": "contact_email",
}
],
"message": """<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #0d9488; padding: 20px; text-align: center;">
<h1 style="color: white; margin: 0;">DiDi - Factura Emisa</h1>
</div>
<div style="padding: 30px; background: #f9fafb;">
<p>Stimate client,</p>
<p>Va informam ca a fost emisa factura <strong>{{ doc.name }}</strong>.</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background: #e5e7eb;"><td style="padding: 8px; font-weight: bold;">Nr. Factura</td><td style="padding: 8px;">{{ doc.name }}</td></tr>
<tr><td style="padding: 8px; font-weight: bold;">Data</td><td style="padding: 8px;">{{ doc.posting_date }}</td></tr>
<tr style="background: #e5e7eb;"><td style="padding: 8px; font-weight: bold;">Total</td><td style="padding: 8px;">{{ doc.grand_total }} {{ doc.currency }}</td></tr>
<tr><td style="padding: 8px; font-weight: bold;">Status</td><td style="padding: 8px;">{{ doc.status }}</td></tr>
</table>
<p>Factura in format PDF este atasata la acest email.</p>
<p>Va multumim,<br><strong>Echipa DiDi / Clossers</strong></p>
</div>
<div style="padding: 15px; text-align: center; font-size: 12px; color: #9ca3af;">
TOP CLOSSERS SRL | CUI: 36193026 | office@clossers.com
</div>
</div>""",
"enabled": 1,
})
print(f" Created: {r.status_code}")
# ── 3. Notification: Plata Confirmata ──
print("\n3. Notification: Plata Confirmata...")
if exists("Notification", "DiDi - Plata Confirmata"):
print(" Already exists")
else:
r = s.post(f"{URL}/api/resource/Notification", json={
"name": "DiDi - Plata Confirmata",
"subject": "Plata confirmata - {{ doc.amount }} {{ doc.currency }}",
"document_type": "Payment Log",
"event": "New",
"channel": "Email",
"condition": "doc.status == 'Succeeded'",
"recipients": [
{
"receiver_by_document_field": "customer",
}
],
"message": """<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #0d9488; padding: 20px; text-align: center;">
<h1 style="color: white; margin: 0;">Plata Confirmata</h1>
</div>
<div style="padding: 30px; background: #f9fafb;">
<p>Stimate client,</p>
<p>Va confirmam ca plata dumneavoastra a fost procesata cu succes.</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background: #e5e7eb;"><td style="padding: 8px; font-weight: bold;">Suma</td><td style="padding: 8px;">{{ doc.amount }} {{ doc.currency }}</td></tr>
<tr><td style="padding: 8px; font-weight: bold;">Status</td><td style="padding: 8px;">{{ doc.status }}</td></tr>
<tr style="background: #e5e7eb;"><td style="padding: 8px; font-weight: bold;">Data</td><td style="padding: 8px;">{{ doc.creation }}</td></tr>
</table>
<p>Factura fiscala va fi disponibila in dashboard-ul dumneavoastra.</p>
<p>Va multumim,<br><strong>Echipa DiDi / Clossers</strong></p>
</div>
<div style="padding: 15px; text-align: center; font-size: 12px; color: #9ca3af;">
TOP CLOSSERS SRL | CUI: 36193026 | office@clossers.com
</div>
</div>""",
"enabled": 1,
})
print(f" Created: {r.status_code}")
# ── 4. Notification: Expirare Abonament (7 zile inainte) ──
print("\n4. Notification: Expirare Abonament...")
if exists("Notification", "DiDi - Expirare Abonament Notificare"):
print(" Already exists")
else:
r = s.post(f"{URL}/api/resource/Notification", json={
"name": "DiDi - Expirare Abonament Notificare",
"subject": "Abonamentul dumneavoastra expira in curand",
"document_type": "Subscription",
"event": "Days Before",
"days_in_advance": 7,
"date_changed": "current_invoice_end",
"channel": "Email",
"condition": "doc.status == 'Active'",
"recipients": [
{
"receiver_by_document_field": "party",
}
],
"message": """<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #f59e0b; padding: 20px; text-align: center;">
<h1 style="color: white; margin: 0;">Abonament - Expirare Apropiata</h1>
</div>
<div style="padding: 30px; background: #f9fafb;">
<p>Stimate client,</p>
<p>Va informam ca abonamentul dumneavoastra expira pe <strong>{{ doc.current_invoice_end }}</strong>.</p>
<p>Pentru a continua sa beneficiati de serviciile DiDi, va rugam sa reinnoti abonamentul din dashboard.</p>
<p>Va multumim,<br><strong>Echipa DiDi / Clossers</strong></p>
</div>
</div>""",
"enabled": 1,
})
print(f" Created: {r.status_code}")
print("\nDone! Email infrastructure ready.")
print("Email Account 'DiDi Outgoing' configured with key from SENDGRID_API_KEY env var.")
print("To rotate the key, run this script again with a new SENDGRID_API_KEY value.")

View file

@ -0,0 +1,288 @@
"""
ERPNext Phase 2 Final Setup: Buying, API User, Reports
"""
import frappe
import json
def execute():
setup_buying()
setup_api_user()
setup_print_format()
frappe.db.commit()
print("\n=== Phase 2 Final Setup Complete ===\n")
def setup_buying():
"""2.6 - Configure suppliers and expense categories."""
print("[1/3] Configuring suppliers and expenses...")
company_abbr = "TC"
# Create expense accounts under existing parent
expenses_parent = frappe.db.get_value("Account",
{"company": "TOP CLOSSERS SRL", "root_type": "Expense", "is_group": 1, "parent_account": ["like", "%Expenses%"]},
"name"
)
if not expenses_parent:
expenses_parent = frappe.db.get_value("Account",
{"company": "TOP CLOSSERS SRL", "root_type": "Expense", "is_group": 1},
"name"
)
if expenses_parent:
expense_accounts = [
{"account_name": "Hosting si Infrastructura", "account_type": "Expense Account"},
{"account_name": "Servicii Software", "account_type": "Expense Account"},
{"account_name": "Marketing si Publicitate", "account_type": "Expense Account"},
{"account_name": "Servicii Plati (Stripe)", "account_type": "Expense Account"},
]
for acc in expense_accounts:
full_name = f"{acc['account_name']} - {company_abbr}"
if not frappe.db.exists("Account", full_name):
try:
a = frappe.get_doc({
"doctype": "Account",
"account_name": acc["account_name"],
"parent_account": expenses_parent,
"account_type": acc["account_type"],
"company": "TOP CLOSSERS SRL",
})
a.insert(ignore_permissions=True)
print(f" Created account: {acc['account_name']}")
except Exception as e:
print(f" Account {acc['account_name']}: {e}")
# Create suppliers
suppliers = [
{"supplier_name": "Hetzner Online GmbH", "supplier_group": "Services", "country": "Germany",
"supplier_type": "Company"},
{"supplier_name": "Stripe Payments Europe", "supplier_group": "Services", "country": "Ireland",
"supplier_type": "Company"},
{"supplier_name": "Twilio SendGrid", "supplier_group": "Services", "country": "United States",
"supplier_type": "Company"},
]
# Ensure supplier group exists
if not frappe.db.exists("Supplier Group", "Services"):
sg = frappe.get_doc({"doctype": "Supplier Group", "supplier_group_name": "Services"})
sg.insert(ignore_permissions=True)
for s in suppliers:
if not frappe.db.exists("Supplier", s["supplier_name"]):
doc = frappe.get_doc({"doctype": "Supplier", **s})
doc.insert(ignore_permissions=True)
print(f" Created supplier: {s['supplier_name']}")
print(" Buying configured.")
def setup_api_user():
"""2.9 - Create dedicated API user for website integration."""
print("[2/3] Creating API user and role...")
# Create custom role
if not frappe.db.exists("Role", "Website Integration"):
role = frappe.get_doc({
"doctype": "Role",
"role_name": "Website Integration",
"desk_access": 0,
"is_custom": 1
})
role.insert(ignore_permissions=True)
print(" Created role: Website Integration")
# Set permissions for the role
doctypes_read_write = [
"Customer", "Lead", "Sales Invoice", "Payment Entry", "Subscription",
"Subscription Plan", "Website Content", "Service Agreement",
"Analysis Report",
"Payment Log", "Item", "Address"
]
doctypes_read_only = [
"Company", "Account", "Sales Taxes and Charges Template",
"Email Template", "Sales Stage"
]
for dt in doctypes_read_write:
try:
# Check if DocType exists
if not frappe.db.exists("DocType", dt):
continue
existing_name = frappe.db.exists("Custom DocPerm", {"parent": dt, "role": "Website Integration"})
values = {
"read": 1,
"write": 1,
"create": 1,
"delete": 0,
"email": 0,
"print": 1,
"export": 1,
"submit": 1 if dt in ("Sales Invoice", "Payment Entry") else 0,
}
if existing_name:
frappe.db.set_value("Custom DocPerm", existing_name, values, update_modified=False)
else:
perm = frappe.get_doc({
"doctype": "Custom DocPerm",
"parent": dt,
"parenttype": "DocType",
"parentfield": "permissions",
"role": "Website Integration",
**values,
})
perm.insert(ignore_permissions=True)
except Exception:
pass
for dt in doctypes_read_only:
if not frappe.db.exists("Custom DocPerm", {"parent": dt, "role": "Website Integration"}):
try:
if frappe.db.exists("DocType", dt):
perm = frappe.get_doc({
"doctype": "Custom DocPerm",
"parent": dt,
"parenttype": "DocType",
"parentfield": "permissions",
"role": "Website Integration",
"read": 1,
"write": 0,
"create": 0,
})
perm.insert(ignore_permissions=True)
except Exception:
pass
# Create API user
api_user_email = "website_api@didi.localhost"
if not frappe.db.exists("User", api_user_email):
user = frappe.get_doc({
"doctype": "User",
"email": api_user_email,
"first_name": "Website",
"last_name": "API",
"enabled": 1,
"user_type": "System User",
"roles": [
{"role": "Website Integration"},
],
"new_password": "didi_api_secure_pwd_2026!",
"send_welcome_email": 0,
})
user.insert(ignore_permissions=True)
print(f" Created API user: {api_user_email}")
# Generate API keys
api_secret = frappe.generate_hash(length=15)
user.reload()
user.api_key = frappe.generate_hash(length=15)
user.api_secret = api_secret
user.save(ignore_permissions=True)
print(f" API Key: {user.api_key}")
print(f" API Secret: {api_secret}")
print(f" >>> Save these credentials in website/.env.local <<<")
else:
user = frappe.get_doc("User", api_user_email)
print(f" API user already exists: {api_user_email}")
print(f" API Key: {user.api_key}")
print(" API user configured.")
def setup_print_format():
"""2.3.2 - Create custom invoice PDF print format."""
print("[3/3] Creating custom invoice print format...")
if frappe.db.exists("Print Format", "DiDi Invoice"):
print(" Already exists, skipping.")
return
html = """
<style>
.didi-invoice { font-family: Arial, sans-serif; font-size: 12px; color: #333; }
.didi-invoice .header { display: flex; justify-content: space-between; margin-bottom: 30px; }
.didi-invoice .company-info { text-align: right; }
.didi-invoice .invoice-title { font-size: 24px; font-weight: bold; color: #2563eb; margin-bottom: 5px; }
.didi-invoice table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.didi-invoice th { background: #2563eb; color: white; padding: 8px 12px; text-align: left; }
.didi-invoice td { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; }
.didi-invoice .totals { text-align: right; margin-top: 20px; }
.didi-invoice .totals td { font-weight: bold; }
.didi-invoice .footer { margin-top: 40px; padding-top: 20px; border-top: 2px solid #2563eb; font-size: 10px; color: #666; }
</style>
<div class="didi-invoice">
<div class="header">
<div>
<div class="invoice-title">FACTURA</div>
<div><strong>{{ doc.name }}</strong></div>
<div>Data: {{ doc.posting_date }}</div>
<div>Scadenta: {{ doc.due_date }}</div>
</div>
<div class="company-info">
<div><strong>{{ doc.company }}</strong></div>
<div>CUI: {{ frappe.db.get_value("Company", doc.company, "tax_id") }}</div>
<div>{{ frappe.db.get_value("Company", doc.company, "address") or "Str. Targovistei 15, Ploiesti" }}</div>
</div>
</div>
<div style="margin-bottom: 20px;">
<strong>Catre:</strong><br>
{{ doc.customer_name }}<br>
{% if doc.tax_id %}CUI: {{ doc.tax_id }}<br>{% endif %}
{{ doc.address_display or "" }}
</div>
<table>
<thead>
<tr>
<th>Nr.</th>
<th>Descriere</th>
<th>Cant.</th>
<th>Pret unitar</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for item in doc.items %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ item.item_name }}<br><small>{{ item.description or "" }}</small></td>
<td>{{ item.qty }}</td>
<td>{{ frappe.format(item.rate, {"fieldtype": "Currency", "currency": doc.currency}) }}</td>
<td>{{ frappe.format(item.amount, {"fieldtype": "Currency", "currency": doc.currency}) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table class="totals" style="width: 300px; margin-left: auto;">
<tr><td>Subtotal:</td><td>{{ frappe.format(doc.net_total, {"fieldtype": "Currency", "currency": doc.currency}) }}</td></tr>
{% for tax in doc.taxes %}
<tr><td>{{ tax.description }}:</td><td>{{ frappe.format(tax.tax_amount, {"fieldtype": "Currency", "currency": doc.currency}) }}</td></tr>
{% endfor %}
<tr style="font-size: 16px;"><td>TOTAL:</td><td>{{ frappe.format(doc.grand_total, {"fieldtype": "Currency", "currency": doc.currency}) }}</td></tr>
</table>
<div class="footer">
<p>Factura generata automat de platforma DiDi. Acest document este valid fara semnatura si stampila conform art. 106 alin. 2 din Legea 227/2015.</p>
<p>TOP CLOSSERS SRL | CUI: 36193026 | J2022000345035 | office@clossers.com</p>
</div>
</div>
"""
pf = frappe.get_doc({
"doctype": "Print Format",
"name": "DiDi Invoice",
"doc_type": "Sales Invoice",
"module": "Didi Custom",
"html": html,
"print_format_type": "Jinja",
"standard": "No",
"custom_format": 1,
"default_print_language": "ro",
})
pf.insert(ignore_permissions=True)
print(" DiDi Invoice print format created.")