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,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.")