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,21 @@
# Root editor config file
root = true
# Common settings
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# python, js indentation settings
[{*.py,*.js,*.vue,*.css,*.scss,*.html}]
indent_style = tab
indent_size = 4
max_line_length = 99
# JSON files - mostly doctype schema files
[{*.json}]
insert_final_newline = false
indent_style = space
indent_size = 2

View file

@ -0,0 +1,124 @@
{
"env": {
"browser": true,
"node": true,
"es2022": true
},
"parserOptions": {
"sourceType": "module"
},
"extends": "eslint:recommended",
"rules": {
"indent": "off",
"brace-style": "off",
"no-mixed-spaces-and-tabs": "off",
"no-useless-escape": "off",
"space-unary-ops": ["error", { "words": true }],
"linebreak-style": "off",
"quotes": ["off"],
"semi": "off",
"camelcase": "off",
"no-unused-vars": "off",
"no-console": ["warn"],
"no-extra-boolean-cast": ["off"],
"no-control-regex": ["off"],
},
"root": true,
"globals": {
"frappe": true,
"Vue": true,
"SetVueGlobals": true,
"__": true,
"repl": true,
"Class": true,
"locals": true,
"cint": true,
"cstr": true,
"cur_frm": true,
"cur_dialog": true,
"cur_page": true,
"cur_list": true,
"cur_tree": true,
"msg_dialog": true,
"is_null": true,
"in_list": true,
"has_common": true,
"posthog": true,
"has_words": true,
"validate_email": true,
"open_web_template_values_editor": true,
"validate_name": true,
"validate_phone": true,
"validate_url": true,
"get_number_format": true,
"format_number": true,
"format_currency": true,
"comment_when": true,
"open_url_post": true,
"toTitle": true,
"lstrip": true,
"rstrip": true,
"strip": true,
"strip_html": true,
"replace_all": true,
"flt": true,
"precision": true,
"CREATE": true,
"AMEND": true,
"CANCEL": true,
"copy_dict": true,
"get_number_format_info": true,
"strip_number_groups": true,
"print_table": true,
"Layout": true,
"web_form_settings": true,
"$c": true,
"$a": true,
"$i": true,
"$bg": true,
"$y": true,
"$c_obj": true,
"refresh_many": true,
"refresh_field": true,
"toggle_field": true,
"get_field_obj": true,
"get_query_params": true,
"unhide_field": true,
"hide_field": true,
"set_field_options": true,
"getCookie": true,
"getCookies": true,
"get_url_arg": true,
"md5": true,
"$": true,
"jQuery": true,
"moment": true,
"hljs": true,
"Awesomplete": true,
"Sortable": true,
"Showdown": true,
"Taggle": true,
"Gantt": true,
"Slick": true,
"Webcam": true,
"PhotoSwipe": true,
"PhotoSwipeUI_Default": true,
"io": true,
"JsBarcode": true,
"L": true,
"Chart": true,
"DataTable": true,
"Cypress": true,
"cy": true,
"it": true,
"describe": true,
"expect": true,
"context": true,
"before": true,
"beforeEach": true,
"after": true,
"qz": true,
"localforage": true,
"extend_cscript": true
}
}

View file

@ -0,0 +1,69 @@
exclude: 'node_modules|.git'
default_stages: [pre-commit]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
files: "didi_custom.*"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
- id: check-merge-conflict
- id: check-ast
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.1
hooks:
- id: ruff
name: "Run ruff import sorter"
args: ["--select=I", "--fix"]
- id: ruff
name: "Run ruff linter"
- id: ruff-format
name: "Run ruff formatter"
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
types_or: [javascript, vue, scss]
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
didi_custom/public/dist/.*|
.*node_modules.*|
.*boilerplate.*|
didi_custom/templates/includes/.*|
didi_custom/public/js/lib/.*
)$
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0
hooks:
- id: eslint
types_or: [javascript]
args: ['--quiet']
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
didi_custom/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
didi_custom/templates/includes/.*|
didi_custom/public/js/lib/.*
)$
ci:
autoupdate_schedule: weekly
skip: []
submodules: false

View file

@ -0,0 +1,33 @@
### Didi Custom
Custom DocTypes for DiDi Platform
### Installation
You can install this app using the [bench](https://github.com/frappe/bench) CLI:
```bash
cd $PATH_TO_YOUR_BENCH
bench get-app $URL_OF_THIS_REPO --branch develop
bench install-app didi_custom
```
### Contributing
This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository:
```bash
cd apps/didi_custom
pre-commit install
```
Pre-commit is configured to use the following tools for checking and formatting your code:
- ruff
- eslint
- prettier
- pyupgrade
### License
mit

View file

@ -0,0 +1 @@
__version__ = "0.0.1"

View file

@ -0,0 +1,4 @@
# Arhivă scripturi one-off (build & fixuri aplicate deja)
Scripturi folosite O SINGURĂ DATĂ la construcția/repararea instanței, păstrate
pentru trasabilitate. NU se rulează din nou — efectele lor sunt deja în baza de
date. Modulele active ale aplicației sunt în rădăcina didi_custom/ și integrations/.

View file

@ -0,0 +1,34 @@
import frappe
def execute():
custom_fields = [
{"dt": "Customer", "fieldname": "didi_user_id", "label": "DiDi Keycloak User ID", "fieldtype": "Data", "insert_after": "customer_name", "in_standard_filter": 1, "search_index": 1},
{"dt": "Customer", "fieldname": "iam_role", "label": "IAM Role", "fieldtype": "Data", "insert_after": "didi_user_id"},
{"dt": "Customer", "fieldname": "active_plan", "label": "Active Plan", "fieldtype": "Data", "insert_after": "iam_role"},
{"dt": "Customer", "fieldname": "plan_activation_date", "label": "Plan Activation Date", "fieldtype": "Date", "insert_after": "active_plan"},
{"dt": "Lead", "fieldname": "source_form", "label": "Source Form", "fieldtype": "Data", "insert_after": "source"},
{"dt": "Lead", "fieldname": "page_origin", "label": "Page Origin", "fieldtype": "Data", "insert_after": "source_form"},
{"dt": "Sales Invoice", "fieldname": "analysis_consumed", "label": "Analysis Consumed", "fieldtype": "Check", "insert_after": "naming_series"},
{"dt": "Sales Invoice", "fieldname": "analysis_session_id", "label": "Analysis Session ID", "fieldtype": "Data", "insert_after": "analysis_consumed"},
{"dt": "Payment Log", "fieldname": "stripe_session_id", "label": "Stripe Session ID", "fieldtype": "Data", "insert_after": "stripe_payment_intent_id", "in_standard_filter": 1, "search_index": 1},
]
for cf in custom_fields:
name = cf["dt"] + "-" + cf["fieldname"]
try:
if frappe.db.exists("Custom Field", name):
doc = frappe.get_doc("Custom Field", name)
for k, v in cf.items():
if k != "dt":
setattr(doc, k, v)
doc.save(ignore_permissions=True)
print("Updated: " + name)
else:
doc = frappe.get_doc({"doctype": "Custom Field", **cf})
doc.flags.ignore_permissions = True
doc.insert()
print("Created: " + name)
except Exception as e:
print("Failed " + name + ": " + str(e)[:200])
frappe.db.commit()
frappe.clear_cache()
print("DONE")

View file

@ -0,0 +1,51 @@
import frappe
def execute():
company = "TOP CLOSSERS SRL"
def find_parent(like):
r = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND name LIKE %s AND is_group=1 LIMIT 1", (company, like))
return r[0][0] if r else None
bugetul = find_parent("%Bugetul statului%TC")
venituri_root = find_parent("VENITURI - TC")
print("TVA parent:", bugetul)
print("Income root:", venituri_root)
accounts = [
("4426 - TVA deductibila", bugetul, "Asset", "Tax", "4426"),
("4427 - TVA colectata", bugetul, "Liability", "Tax", "4427"),
("4423 - TVA de plata", bugetul, "Liability", "Tax", "4423"),
("4424 - TVA de recuperat", bugetul, "Asset", "Tax", "4424"),
("704 - Venituri din servicii prestate", venituri_root, "Income", "Income Account", "704"),
("707 - Venituri din vanzarea marfurilor", venituri_root, "Income", "Income Account", "707"),
]
for name, parent, rt, at, num in accounts:
if not parent:
print(" SKIP", name)
continue
doc = frappe.get_doc({
"doctype": "Account",
"company": company,
"is_group": 0,
"account_name": name,
"parent_account": parent,
"root_type": rt,
"account_type": at,
"account_number": num,
})
doc.flags.ignore_permissions = True
try:
doc.insert()
print(" OK", doc.name)
except Exception as e:
print(" FAIL", name, str(e)[:100])
frappe.db.commit()
income = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND account_number=%s", (company, "704"))
if income:
co = frappe.get_doc("Company", company)
co.default_income_account = income[0][0]
co.flags.ignore_permissions = True
co.save()
frappe.db.commit()
print("Default income:", income[0][0])

View file

@ -0,0 +1,54 @@
import frappe
def execute():
company = "TOP CLOSSERS SRL"
# Find parent groups
def find_parent(like):
r = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND name LIKE %s AND is_group=1 LIMIT 1", (company, like))
return r[0][0] if r else None
bugetul = find_parent("%Bugetul statului%TC")
venituri_root = find_parent("VENITURI - TC")
print(f"TVA parent: {bugetul}")
print(f"Income root: {venituri_root}")
# Add specific accounts
accounts_to_create = [
{"account_name": "4426 - TVA deductibila", "parent_account": bugetul, "root_type": "Asset", "account_type": "Tax", "account_number": "4426"},
{"account_name": "4427 - TVA colectata", "parent_account": bugetul, "root_type": "Liability", "account_type": "Tax", "account_number": "4427"},
{"account_name": "4423 - TVA de plata", "parent_account": bugetul, "root_type": "Liability", "account_type": "Tax", "account_number": "4423"},
{"account_name": "4424 - TVA de recuperat", "parent_account": bugetul, "root_type": "Asset", "account_type": "Tax", "account_number": "4424"},
{"account_name": "704 - Venituri din servicii prestate", "parent_account": venituri_root, "root_type": "Income", "account_type": "Income Account", "account_number": "704"},
{"account_name": "707 - Venituri din vanzarea marfurilor", "parent_account": venituri_root, "root_type": "Income", "account_type": "Income Account", "account_number": "707"},
]
for acc in accounts_to_create:
if not acc["parent_account"]:
print(f" SKIP {acc[\"account_name\"]} - no parent")
continue
doc = frappe.get_doc({
"doctype": "Account",
"company": company,
"is_group": 0,
**acc,
})
doc.flags.ignore_permissions = True
try:
doc.insert()
print(f" CREATED: {doc.name}")
except Exception as e:
print(f" FAIL {acc[\"account_name\"]}: {str(e)[:100]}")
frappe.db.commit()
# Update Company default income account
income = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND account_number=%s", (company, "704"))
if income:
co = frappe.get_doc("Company", company)
co.default_income_account = income[0][0]
co.flags.ignore_permissions = True
co.save()
frappe.db.commit()
print(f" Default income → {income[0][0]}")

View file

@ -0,0 +1,28 @@
import frappe
def execute():
# Check what get_doctype_module returns for Customer
from frappe.model.utils import get_doctype_module
try:
mod = get_doctype_module('Customer')
print('Module for Customer:', mod)
except Exception as e:
print('get_doctype_module ERROR:', e)
# Try to actually insert a Customer
try:
c = frappe.get_doc({
'doctype': 'Customer',
'customer_name': 'Test User Direct',
'customer_type': 'Individual',
'customer_group': 'Commercial',
'territory': 'Romania',
'didi_user_id': '14142351-ad1e-466b-ac5d-4a7a0ff562bf',
'email_id': 'test@didi.local',
})
c.flags.ignore_permissions = True
c.insert()
frappe.db.commit()
print('Customer created:', c.name)
except Exception as e:
print('Insert ERROR:', str(e)[:300])

View file

@ -0,0 +1,64 @@
import frappe
def execute():
company = "TOP CLOSSERS SRL"
# Find key accounts
def find_account(like):
r = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND name LIKE %s AND is_group=0 LIMIT 1", (company, like))
return r[0][0] if r else None
# Receivable (Clienti)
receivable = find_account("%Clienti%TC") or find_account("%Clienti 1 - TC")
# Cash account (Casa) - or create one
cash = find_account("%Casa%lei%TC") or find_account("%Casa 1%TC") or find_account("%Casa - TC")
# Bank account
bank = find_account("%Conturi la banci in lei%TC") or find_account("%5121%TC")
# Income for services
income = find_account("%Venituri din prestari de servicii%TC") or find_account("%704%TC") or find_account("%Venituri din vanzarea%TC")
# TVA colectata (output VAT)
tva_colectata = find_account("%TVA colectata%TC") or find_account("%4427%TC")
# TVA deductibila (input VAT)
tva_deductibila = find_account("%TVA deductibila%TC") or find_account("%4426%TC")
print(f"Receivable: {receivable}")
print(f"Cash: {cash}")
print(f"Bank: {bank}")
print(f"Income: {income}")
print(f"TVA colectata: {tva_colectata}")
print(f"TVA deductibila: {tva_deductibila}")
# Update Company defaults
co = frappe.get_doc("Company", company)
if receivable: co.default_receivable_account = receivable
if cash: co.default_cash_account = cash
if bank: co.default_bank_account = bank
if income: co.default_income_account = income
co.flags.ignore_permissions = True
co.save()
frappe.db.commit()
print("Company defaults updated")
# Delete old tax templates
frappe.db.sql("DELETE FROM \`tabSales Taxes and Charges Template\` WHERE company=%s", (company,))
frappe.db.sql("DELETE FROM \`tabSales Taxes and Charges\` WHERE parent NOT IN (SELECT name FROM \`tabSales Taxes and Charges Template\`)")
frappe.db.commit()
# Create TVA 19% template
if tva_colectata:
tax = frappe.get_doc({
"doctype": "Sales Taxes and Charges Template",
"title": "TVA 19%",
"company": company,
"is_default": 1,
"taxes": [{
"charge_type": "On Net Total",
"account_head": tva_colectata,
"description": "TVA 19%",
"rate": 19,
}],
})
tax.flags.ignore_permissions = True
tax.insert()
frappe.db.commit()
print(f"Created tax template: {tax.name}{tva_colectata}")

View file

@ -0,0 +1,18 @@
import frappe
def execute():
if frappe.db.exists("Company", "TOP CLOSSERS SRL"):
print("Company already exists")
return
doc = frappe.get_doc({
"doctype": "Company",
"company_name": "TOP CLOSSERS SRL",
"abbr": "TC",
"default_currency": "RON",
"country": "Romania",
"chart_of_accounts": "Standard",
})
doc.insert(ignore_permissions=True, ignore_mandatory=True)
frappe.db.commit()
print("Company created:", doc.name)

View file

@ -0,0 +1,55 @@
import frappe
def execute():
# Create custom Report "DiDi - Raport TVA D300" (Script Report)
report_name = "DiDi - Raport TVA D300"
if frappe.db.exists("Report", report_name):
print("Already exists, deleting first")
frappe.delete_doc("Report", report_name, ignore_permissions=True, force=True)
frappe.db.commit()
report_js = """
frappe.query_reports["DiDi - Raport TVA D300"] = {
filters: [
{ fieldname: "company", label: "Companie", fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), reqd: 1 },
{ fieldname: "from_date", label: "De la data", fieldtype: "Date", default: frappe.datetime.month_start(), reqd: 1 },
{ fieldname: "to_date", label: "Pana la data", fieldtype: "Date", default: frappe.datetime.month_end(), reqd: 1 },
]
};
"""
# Server-side query method (we'll create as a custom script)
report_query = """
SELECT
DATE_FORMAT(posting_date, '%Y-%m') as luna,
CASE
WHEN account LIKE '%4427%' THEN 'TVA colectata (vanzari)'
WHEN account LIKE '%4426%' THEN 'TVA deductibila (cumparari)'
WHEN account LIKE '%4423%' THEN 'TVA de plata'
WHEN account LIKE '%4424%' THEN 'TVA de recuperat'
ELSE account
END as categorie,
ROUND(SUM(debit), 2) as total_debit,
ROUND(SUM(credit), 2) as total_credit,
ROUND(SUM(credit) - SUM(debit), 2) as sold_net
FROM `tabGL Entry`
WHERE company = %(company)s
AND posting_date BETWEEN %(from_date)s AND %(to_date)s
AND (account LIKE '%%4426%%' OR account LIKE '%%4427%%' OR account LIKE '%%4423%%' OR account LIKE '%%4424%%')
GROUP BY luna, categorie
ORDER BY luna, categorie
"""
doc = frappe.get_doc({
"doctype": "Report",
"report_name": report_name,
"ref_doctype": "Sales Invoice",
"report_type": "Query Report",
"is_standard": "No",
"query": report_query,
"module": "Accounts",
})
doc.flags.ignore_permissions = True
doc.insert()
frappe.db.commit()
print("Created:", doc.name)

View file

@ -0,0 +1,12 @@
import frappe
import traceback
def execute():
from erpnext.setup.setup_wizard.operations.install_fixtures import install_company
args = frappe._dict({"country": "Romania", "company_name": "TOP CLOSSERS SRL", "company_abbr": "TC"})
try:
install_company(args)
print("OK")
except Exception as e:
print("ERROR:", str(e)[:500])
print(traceback.format_exc()[:2000])

View file

@ -0,0 +1,26 @@
import frappe
def execute():
company = "TOP CLOSSERS SRL"
if not frappe.db.exists("Company", company):
print("Company not found")
return
# Patch out the country_fixtures call to avoid Romania ImportError
from erpnext.setup.doctype.company import company as cm
cm.install_country_fixtures = lambda *a, **kw: None
# Delete company (cascades through accounts/cost_centers/fiscal_year/series)
try:
frappe.delete_doc("Company", company, ignore_permissions=True, force=True, ignore_missing=True)
frappe.db.commit()
print("Deleted:", company)
except Exception as e:
print("Soft-delete via SQL:", str(e)[:200])
# Fallback: direct SQL
for tbl in ["tabAccount","tabCost Center","tabFiscal Year","tabSeries","tabMode of Payment Account","tabParty Account"]:
try:
frappe.db.sql(f"DELETE FROM \`{tbl}\` WHERE company=%s OR name LIKE %s", (company, "%- TC"))
except: pass
frappe.db.sql("DELETE FROM tabCompany WHERE name=%s", (company,))
frappe.db.commit()
print("Force deleted:", company)

View file

@ -0,0 +1,59 @@
import frappe
def execute():
company = "TOP CLOSSERS SRL"
# Create TVA 19% tax template using 4427 - TVA colectata
tva_colectata = "4427 - 4427 - TVA colectata - TC"
# Delete old templates
for t in frappe.get_all("Sales Taxes and Charges Template", filters={"company": company}):
frappe.delete_doc("Sales Taxes and Charges Template", t.name, ignore_permissions=True, force=True)
tax = frappe.get_doc({
"doctype": "Sales Taxes and Charges Template",
"title": "TVA 19%",
"company": company,
"is_default": 1,
"taxes": [{
"charge_type": "On Net Total",
"account_head": tva_colectata,
"description": "TVA 19%",
"rate": 19,
}],
})
tax.flags.ignore_permissions = True
tax.insert()
print("Tax template:", tax.name)
# Recreate Customer
if frappe.db.exists("Customer", "Test User"):
frappe.delete_doc("Customer", "Test User", ignore_permissions=True, force=True)
cust = frappe.get_doc({
"doctype": "Customer",
"customer_name": "Test User",
"customer_type": "Individual",
"customer_group": "Commercial",
"territory": "Romania",
"didi_user_id": "14142351-ad1e-466b-ac5d-4a7a0ff562bf",
"email_id": "test@didi.local",
"iam_role": "free_tier",
})
cust.flags.ignore_permissions = True
cust.insert()
frappe.db.commit()
print("Customer:", cust.name)
# Verify
print("\n=== Accounts summary ===")
for rt in ["Asset","Liability","Equity","Income","Expense"]:
c = frappe.db.sql("SELECT COUNT(*) FROM tabAccount WHERE company=%s AND root_type=%s", (company, rt))[0][0]
print(f" {rt}: {c}")
print("\n=== Company defaults ===")
co = frappe.get_doc("Company", company)
print(f" default_receivable: {co.default_receivable_account}")
print(f" default_income: {co.default_income_account}")
print(f" default_cash: {co.default_cash_account}")
print(f" default_bank: {co.default_bank_account}")

View file

@ -0,0 +1,8 @@
import frappe
def execute():
notif = frappe.get_doc("Notification", "DiDi - Factura Emisa")
doc = frappe.get_doc("Sales Invoice", "DIDI-INV-2026-00010")
notif.send(doc)
frappe.db.commit()
print("Notification fired for", doc.name)

View file

@ -0,0 +1,20 @@
import frappe
def execute():
# Allow analysis_consumed and analysis_session_id to be set on submitted invoices
fields_to_fix = [
("Sales Invoice", "analysis_consumed"),
("Sales Invoice", "analysis_session_id"),
]
for dt, fn in fields_to_fix:
name = f"{dt}-{fn}"
if frappe.db.exists("Custom Field", name):
doc = frappe.get_doc("Custom Field", name)
doc.allow_on_submit = 1
doc.save(ignore_permissions=True)
print(f" Allow on Submit set: {name}")
else:
print(f" NOT FOUND: {name}")
frappe.db.commit()
frappe.clear_cache()
print("DONE")

View file

@ -0,0 +1,13 @@
import frappe
def execute():
# Find the broken row pointing to Customer Portal Users
rows = frappe.db.sql("SELECT name, fieldname, options FROM tabDocField WHERE parent=%s AND options=%s", ("Customer", "Customer Portal Users"), as_dict=True)
print("Found broken rows:", rows)
for r in rows:
# Delete the broken field
frappe.db.delete("DocField", {"name": r["name"]})
print("Deleted:", r["fieldname"])
frappe.db.commit()
frappe.clear_cache()
print("DONE")

View file

@ -0,0 +1,10 @@
import frappe
def execute():
# Get row 79 of Customer doctype
rows = frappe.db.sql("SELECT name, fieldname, fieldtype, options, idx FROM tabDocField WHERE parent=%s ORDER BY idx", ("Customer",), as_dict=True)
for r in rows[75:85]:
print(f" idx={r[\"idx\"]} fn={r[\"fieldname\"]} ft={r[\"fieldtype\"]} opt={r[\"options\"]}")
# Find any row referencing Customer Portal Users
rows2 = frappe.db.sql("SELECT name, parent, fieldname, options FROM tabDocField WHERE options LIKE %s", ("%Customer Portal%",), as_dict=True)
print("Any with Customer Portal opt:", rows2)

View file

@ -0,0 +1,55 @@
import frappe
def execute():
company = "TOP CLOSSERS SRL"
# Find/create Round Off accounts
# Round Off Account - for rounding (Expense or Income)
# Round Off Cost Center - cost center
venituri = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND name LIKE %s AND is_group=1 LIMIT 1", (company, "VENITURI%TC"))
chelt = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND name LIKE %s AND is_group=1 LIMIT 1", (company, "CHELTUIELI%TC"))
venituri_parent = venituri[0][0] if venituri else None
chelt_parent = chelt[0][0] if chelt else None
# Create Round Off accounts
round_off_name = None
if chelt_parent:
if not frappe.db.exists("Account", {"company": company, "account_name": "Round Off"}):
doc = frappe.get_doc({
"doctype": "Account",
"company": company,
"account_name": "Round Off",
"parent_account": chelt_parent,
"root_type": "Expense",
"account_type": "Round Off",
"is_group": 0,
})
doc.flags.ignore_permissions = True
doc.insert()
round_off_name = doc.name
print("Created Round Off:", round_off_name)
else:
round_off_name = frappe.db.get_value("Account", {"company": company, "account_name": "Round Off"}, "name")
print("Round Off exists:", round_off_name)
# Find a Cost Center
cc = frappe.db.sql("SELECT name FROM `tabCost Center` WHERE company=%s LIMIT 1", (company,))
cost_center = cc[0][0] if cc else None
print("Cost Center:", cost_center)
# Update Company
co = frappe.get_doc("Company", company)
if round_off_name:
co.round_off_account = round_off_name
if cost_center:
co.round_off_cost_center = cost_center
co.cost_center = cost_center # default cost center
co.flags.ignore_permissions = True
co.save()
frappe.db.commit()
print("Updated Company defaults")
print(f" round_off_account: {co.round_off_account}")
print(f" cost_center: {co.cost_center}")

View file

@ -0,0 +1,26 @@
import frappe
def execute():
company = "TOP CLOSSERS SRL"
tva_colectata = "4427 - 4427 - TVA colectata - TC"
# Update existing template OR recreate
for t in frappe.get_all("Sales Taxes and Charges Template", filters={"company": company}):
frappe.delete_doc("Sales Taxes and Charges Template", t.name, ignore_permissions=True, force=True)
tax = frappe.get_doc({
"doctype": "Sales Taxes and Charges Template",
"title": "TVA 21%",
"company": company,
"is_default": 1,
"taxes": [{
"charge_type": "On Net Total",
"account_head": tva_colectata,
"description": "TVA 21% (cota standard RO din 01.08.2025)",
"rate": 21,
}],
})
tax.flags.ignore_permissions = True
tax.insert()
frappe.db.commit()
print("Tax template updated:", tax.name, "rate=21%")

View file

@ -0,0 +1,25 @@
import frappe
def execute():
query = """
SELECT
CASE
WHEN account LIKE "%4427%" THEN "TVA colectata (vanzari)"
WHEN account LIKE "%4426%" THEN "TVA deductibila (cumparari)"
WHEN account LIKE "%4423%" THEN "TVA de plata"
WHEN account LIKE "%4424%" THEN "TVA de recuperat"
ELSE account
END as categorie,
ROUND(SUM(debit), 2) as total_debit,
ROUND(SUM(credit), 2) as total_credit,
ROUND(SUM(credit) - SUM(debit), 2) as sold_net
FROM `tabGL Entry`
WHERE company = %(company)s
AND posting_date BETWEEN %(from_date)s AND %(to_date)s
AND (account LIKE "%4426%" OR account LIKE "%4427%" OR account LIKE "%4423%" OR account LIKE "%4424%")
GROUP BY categorie
ORDER BY categorie
"""
frappe.db.sql("UPDATE tabReport SET query=%s WHERE name=%s", (query, "DiDi - Raport TVA D300"))
frappe.db.commit()
print("Updated query")

View file

@ -0,0 +1,26 @@
import frappe
def execute():
# Escape % as %% for psycopg/pymysql named-param queries
query = """
SELECT
CASE
WHEN account LIKE "%%4427%%" THEN "TVA colectata (vanzari)"
WHEN account LIKE "%%4426%%" THEN "TVA deductibila (cumparari)"
WHEN account LIKE "%%4423%%" THEN "TVA de plata"
WHEN account LIKE "%%4424%%" THEN "TVA de recuperat"
ELSE account
END as categorie,
ROUND(SUM(debit), 2) as total_debit,
ROUND(SUM(credit), 2) as total_credit,
ROUND(SUM(credit) - SUM(debit), 2) as sold_net
FROM `tabGL Entry`
WHERE company = %(company)s
AND posting_date BETWEEN %(from_date)s AND %(to_date)s
AND (account LIKE "%%4426%%" OR account LIKE "%%4427%%" OR account LIKE "%%4423%%" OR account LIKE "%%4424%%")
GROUP BY categorie
ORDER BY categorie
"""
frappe.db.sql("UPDATE tabReport SET query=%s WHERE name=%s", (query, "DiDi - Raport TVA D300"))
frappe.db.commit()
print("Updated")

View file

@ -0,0 +1,21 @@
import frappe
def execute():
try:
c = frappe.get_doc({
'doctype': 'Customer',
'customer_name': 'Test User Direct',
'customer_type': 'Individual',
'customer_group': 'Commercial',
'territory': 'Romania',
'didi_user_id': '14142351-ad1e-466b-ac5d-4a7a0ff562bf',
'email_id': 'test@didi.local',
})
c.flags.ignore_permissions = True
c.insert()
frappe.db.commit()
print('Customer created:', c.name)
except Exception as e:
import traceback
print('Insert ERROR:', str(e)[:300])
print(traceback.format_exc()[:1500])

View file

@ -0,0 +1,16 @@
import frappe
def execute():
# Check Custom Field table
rows = frappe.db.sql("SELECT name, dt, fieldname, fieldtype, options, idx FROM `tabCustom Field` WHERE dt='Customer' ORDER BY idx", as_dict=True)
print("Custom Fields on Customer:", len(rows))
for r in rows:
print(' ', r)
# Check Property Setter
rows3 = frappe.db.sql("SELECT name, doc_type, field_name, property, value FROM `tabProperty Setter` WHERE doc_type='Customer'", as_dict=True)
print("Property Setters on Customer:", len(rows3))
# Search ALL for "Customer Portal Users"
rows4 = frappe.db.sql("SELECT name, dt, fieldname, options FROM `tabCustom Field` WHERE options LIKE '%Customer Portal%'", as_dict=True)
print("Custom Fields with Customer Portal:", rows4)

View file

@ -0,0 +1,13 @@
import frappe
def execute():
rows = frappe.db.sql("SELECT name, fieldname, fieldtype, options, idx FROM tabDocField WHERE parent='Customer' ORDER BY idx", as_dict=True)
print("Total Customer fields:", len(rows))
for r in rows[75:85]:
idx = r['idx']
fn = r['fieldname']
ft = r['fieldtype']
opt = r['options']
print(' idx=', idx, 'fn=', fn, 'ft=', ft, 'opt=', opt)
rows2 = frappe.db.sql("SELECT name, parent, fieldname, options FROM tabDocField WHERE options LIKE '%Customer Portal%'", as_dict=True)
print("Rows with Customer Portal in options:", rows2)

View file

@ -0,0 +1,21 @@
import frappe
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
def execute():
company = "TOP CLOSSERS SRL"
print("Installing Romania COA for", company)
try:
create_charts(company, "Romania - Chart of Accounts", None, "Standard Template")
frappe.db.commit()
count = frappe.db.sql("SELECT COUNT(*) FROM tabAccount WHERE company=%s", (company,))[0][0]
print(f"OK - {count} accounts created")
# Sample
samples = frappe.db.sql("SELECT account_number, account_name FROM tabAccount WHERE company=%s AND (account_number LIKE %s OR account_number LIKE %s OR account_number LIKE %s) LIMIT 10",
(company, "4426%", "707%", "411%"))
for s in samples:
print(f" {s[0]} {s[1]}")
except Exception as e:
import traceback
print("ERROR:", str(e)[:200])
print(traceback.format_exc()[:1500])

View file

@ -0,0 +1,33 @@
import json
import frappe
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
def execute():
company = "TOP CLOSSERS SRL"
# Load and fix JSON
with open("/home/frappe/frappe-bench/apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/unverified/ro_ro_chart_template.json") as f:
chart_data = json.load(f)
tree = chart_data.get("tree", {})
# Assign root_type to root accounts based on Romanian COA conventions
# Class 1=Equity, 2=Asset, 3=Asset, 4=Liab+Asset, 5=Asset, 6=Expense, 7=Income, 8=Off-balance
# The two roots are "CONTURI FINANCIARE" and "CONTURI IN AFARA BILANTULUI"
if "CONTURI FINANCIARE" in tree:
tree["CONTURI FINANCIARE"]["root_type"] = "Asset" # default; children have their own
tree["CONTURI FINANCIARE"]["is_group"] = 1
if "CONTURI IN AFARA BILANTULUI" in tree:
tree["CONTURI IN AFARA BILANTULUI"]["root_type"] = "Asset"
tree["CONTURI IN AFARA BILANTULUI"]["is_group"] = 1
print("Fixed tree, installing...")
try:
create_charts(company, custom_chart=tree)
frappe.db.commit()
count = frappe.db.sql("SELECT COUNT(*) FROM tabAccount WHERE company=%s", (company,))[0][0]
print(f"OK - {count} accounts created")
except Exception as e:
import traceback
print("ERROR:", str(e)[:200])
print(traceback.format_exc()[-800:])

View file

@ -0,0 +1,84 @@
import json
import frappe
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
def execute():
company = "TOP CLOSSERS SRL"
with open("/home/frappe/frappe-bench/apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/unverified/ro_ro_chart_template.json") as f:
chart_data = json.load(f)
tree = chart_data["tree"]
# Reorganize roots properly for ERPNext
new_tree = {}
fin = tree.get("CONTURI FINANCIARE", {})
bilant = fin.get("CONTURI DE BILANT", {})
venituri = fin.get("CONTURI DE VENITURI SI CHELTUIELI", {})
# Balance Sheet -> need to be split into Asset/Liability/Equity
# In Romanian COA: Class 1=Capital(Equity+long Liab), 2=Imobilizari(Asset), 3=Stocuri(Asset), 4=Terti(mixed), 5=Trezorerie(Asset)
# Simplest: keep "Bilant" as Asset wrapper but mark Liab/Equity ones properly
# Class-based split
capitaluri = bilant.get("CONTURI DE CAPITALURI", {})
imobilizari = bilant.get("CONTURI DE IMOBILIZARI", {})
stocuri = bilant.get("CONTURI DE STOCURI SI PRODUCTIE IN CURS DE EXECUTIE", {})
terti = bilant.get("CONTURI DE TERTI", {})
trezorerie = bilant.get("CONTURI DE TREZORERIE", {})
new_tree["ACTIVE"] = {
"root_type": "Asset", "is_group": 1,
"Imobilizari (Clasa 2)": {**imobilizari, "root_type": "Asset", "is_group": 1},
"Stocuri (Clasa 3)": {**stocuri, "root_type": "Asset", "is_group": 1},
"Trezorerie (Clasa 5)": {**trezorerie, "root_type": "Asset", "is_group": 1},
"Clienti si conturi asimilate": {
"root_type": "Asset", "is_group": 1,
**terti.get("CLIENTI SI CONTURI ASIMILATE", {}),
},
"Debitori diversi": {
"root_type": "Asset", "is_group": 1,
**terti.get("DEBITORI SI CREDITORI DIVERSI", {}),
},
}
new_tree["PASIVE (DATORII)"] = {
"root_type": "Liability", "is_group": 1,
"Furnizori si conturi asimilate": {
"root_type": "Liability", "is_group": 1,
**terti.get("FURNIZORI SI CONTURI ASIMILATE", {}),
},
"Personal si conturi asimilate": {
"root_type": "Liability", "is_group": 1,
**terti.get("PERSONAL SI CONTURI ASIMILATE", {}),
},
"Asigurari sociale, protectia sociala": {
"root_type": "Liability", "is_group": 1,
**terti.get("ASIGURARI SOCIALE, PROTECTIA SOCIALA SI CONTURI ASIMILATE", {}),
},
"Bugetul statului (TVA, impozite)": {
"root_type": "Liability", "is_group": 1,
**terti.get("BUGETUL STATULUI, FONDURI SPECIALE SI CONTURI ASIMILATE", {}),
},
}
new_tree["CAPITALURI PROPRII"] = {
"root_type": "Equity", "is_group": 1,
**capitaluri,
}
new_tree["VENITURI"] = {
"root_type": "Income", "is_group": 1,
**venituri.get("CONTURI DE VENITURI", {}),
}
new_tree["CHELTUIELI"] = {
"root_type": "Expense", "is_group": 1,
**venituri.get("CONTURI DE CHELTUIELI", {}),
}
create_charts(company, custom_chart=new_tree)
frappe.db.commit()
count = frappe.db.sql("SELECT COUNT(*) FROM tabAccount WHERE company=%s", (company,))[0][0]
print(f"Total accounts: {count}")
for rt in ["Asset","Liability","Equity","Income","Expense"]:
c = frappe.db.sql("SELECT COUNT(*) FROM tabAccount WHERE company=%s AND root_type=%s", (company, rt))[0][0]
print(f" {rt}: {c}")

View file

@ -0,0 +1,13 @@
import frappe
def execute():
rows = frappe.db.sql("SELECT name, fieldname, fieldtype FROM `tabCustom Field` WHERE dt='Customer'", as_dict=True)
for r in rows:
print(' CF:', r['fieldname'], '-', r['fieldtype'])
# Check if API can now query Customer by didi_user_id
try:
result = frappe.db.sql("SELECT name FROM tabCustomer WHERE didi_user_id='test'", as_dict=True)
print("Query worked! Result:", result)
except Exception as e:
print("Query failed:", str(e)[:200])

View file

@ -0,0 +1,33 @@
import frappe
def execute():
# First clear any partial state
frappe.db.sql("DELETE FROM tabCompany WHERE name LIKE %s", ("TOP CLOSSERS%",))
frappe.db.commit()
# Create directly via SQL bypassing all validation
company = frappe.get_doc({
"doctype": "Company",
"company_name": "TOP CLOSSERS SRL",
"abbr": "TC",
"default_currency": "RON",
"country": "Romania",
"chart_of_accounts": "Standard",
"domain": "Services",
"create_chart_of_accounts_based_on": "Standard Template",
})
company.flags.ignore_permissions = True
company.flags.ignore_mandatory = True
company.flags.ignore_links = True
try:
company.insert()
frappe.db.commit()
print("Company inserted:", company.name)
except Exception as e:
print("Insert failed:", str(e)[:300])
frappe.db.rollback()
# Re-mark setup as complete
frappe.db.set_single_value("System Settings", "setup_complete", 1)
frappe.db.commit()
print("Final state - Companies:", frappe.db.sql("SELECT name FROM tabCompany"))

View file

@ -0,0 +1,34 @@
import frappe
def execute():
# Create with India (which has the most complete fixtures) then change to Romania after
if frappe.db.exists("Company", "TOP CLOSSERS SRL"):
print("Already exists")
return
# Patch the country fixtures function to skip
from erpnext.setup.doctype.company import company as company_module
original = company_module.install_country_fixtures
company_module.install_country_fixtures = lambda *a, **kw: None
try:
doc = frappe.get_doc({
"doctype": "Company",
"company_name": "TOP CLOSSERS SRL",
"abbr": "TC",
"default_currency": "RON",
"country": "Romania",
"chart_of_accounts": "Standard",
})
doc.flags.ignore_permissions = True
doc.insert()
frappe.db.commit()
print("Created:", doc.name)
except Exception as e:
print("Error:", str(e)[:300])
import traceback
print(traceback.format_exc()[:1500])
finally:
company_module.install_country_fixtures = original
print("Final:", frappe.db.sql("SELECT name, country, default_currency FROM tabCompany"))

View file

@ -0,0 +1,39 @@
import frappe
def execute():
if frappe.db.exists("Company", "TOP CLOSSERS SRL"):
print("Already exists")
return
# Patch country fixtures to skip the broken portal_user import in Romania-specific setup
from erpnext.setup.doctype.company import company as cm
cm.install_country_fixtures = lambda *a, **kw: None
doc = frappe.get_doc({
"doctype": "Company",
"company_name": "TOP CLOSSERS SRL",
"abbr": "TC",
"default_currency": "RON",
"country": "Romania",
"chart_of_accounts": "Romania - Chart of Accounts",
"domain": "Services",
"create_chart_of_accounts_based_on": "Standard Template",
})
doc.flags.ignore_permissions = True
doc.insert()
frappe.db.commit()
print("Created:", doc.name)
# Set as default
frappe.db.set_single_value("Global Defaults", "default_company", "TOP CLOSSERS SRL")
frappe.db.set_single_value("Global Defaults", "default_currency", "RON")
frappe.db.commit()
# Check accounts created
count = frappe.db.sql("SELECT COUNT(*) FROM tabAccount WHERE company=%s", ("TOP CLOSSERS SRL",))[0][0]
print(f"Accounts created: {count}")
# Show some Romania-specific accounts
samples = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND (name LIKE %s OR name LIKE %s OR name LIKE %s OR name LIKE %s) LIMIT 10",
("TOP CLOSSERS SRL", "4426%", "4427%", "707%", "411%"), as_dict=False)
print("Sample RO accounts:", samples[:10])

View file

@ -0,0 +1,26 @@
import frappe
import traceback
def execute():
if frappe.db.exists("Company", "TOP CLOSSERS SRL"):
print("Already exists")
return
from erpnext.setup.doctype.company import company as cm
cm.install_country_fixtures = lambda *a, **kw: None
try:
doc = frappe.get_doc({
"doctype": "Company",
"company_name": "TOP CLOSSERS SRL",
"abbr": "TC",
"default_currency": "RON",
"country": "Romania",
"chart_of_accounts": "Romania - Chart of Accounts",
"create_chart_of_accounts_based_on": "Standard Template",
})
doc.flags.ignore_permissions = True
doc.insert()
frappe.db.commit()
print("Created:", doc.name)
except Exception as e:
print("ERROR:", str(e)[:400])
print(traceback.format_exc()[:2000])

View file

@ -0,0 +1,25 @@
import frappe
from erpnext.setup.setup_wizard.setup_wizard import setup_complete
def execute():
args = frappe._dict({
"language": "Romanian",
"country": "Romania",
"timezone": "Europe/Bucharest",
"currency": "RON",
"full_name": "Administrator",
"email": "admin@didi.local",
"company_name": "TOP CLOSSERS SRL",
"company_abbr": "TC",
"company_tagline": "DiDi Platform",
"bank_account": "BCR",
"chart_of_accounts": "Standard",
"fy_start_date": "2026-01-01",
"fy_end_date": "2026-12-31",
"domains": ["Services"],
})
frappe.db.set_single_value("System Settings", "setup_complete", 0)
frappe.db.commit()
setup_complete(args)
frappe.db.commit()
print("DONE - Companies:", frappe.db.sql("SELECT name FROM tabCompany"))

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-erp"
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,287 @@
"""
ERPNext Configuration Script for DiDi Project
Run with: docker exec didi-erpnext bench --site didi-erp 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,6 @@
// Copyright (c) 2026, TOP CLOSSERS SRL and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Analysis Report", {
// });

View file

@ -0,0 +1,173 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "DIDI-AR-.YYYY.-.#####",
"creation": "2026-04-02 14:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
"customer_name",
"column_break_1",
"sales_invoice",
"status",
"section_context",
"analysis_session_id",
"component",
"column_break_2",
"media_type",
"report_title",
"section_output",
"pdf_file",
"generated_at",
"result_json"
],
"fields": [
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer",
"reqd": 1
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"reqd": 0,
"description": "Gol pentru analizele rulate din abonament (pe credite)."
},
{
"default": "Completed",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Draft\nCompleted\nFailed",
"reqd": 1
},
{
"fieldname": "section_context",
"fieldtype": "Section Break",
"label": "Analysis Context"
},
{
"fieldname": "analysis_session_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Analysis Session ID",
"read_only": 1,
"unique": 1
},
{
"fieldname": "component",
"fieldtype": "Data",
"label": "Component",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "media_type",
"fieldtype": "Data",
"label": "Media Type",
"read_only": 1
},
{
"fieldname": "report_title",
"fieldtype": "Data",
"label": "Report Title",
"read_only": 1
},
{
"fieldname": "section_output",
"fieldtype": "Section Break",
"label": "Report Output"
},
{
"fieldname": "pdf_file",
"fieldtype": "Attach",
"label": "PDF File",
"read_only": 1
},
{
"fieldname": "generated_at",
"fieldtype": "Datetime",
"label": "Generated At",
"read_only": 1
},
{
"fieldname": "result_json",
"fieldtype": "Code",
"label": "Result JSON",
"options": "JSON",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-02 14:00:00.000000",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Analysis Report",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"search_fields": "customer,sales_invoice,analysis_session_id,report_title",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "report_title"
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2026, TOP CLOSSERS SRL and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class AnalysisReport(Document):
pass

View file

@ -0,0 +1,7 @@
# Copyright (c) 2026, TOP CLOSSERS SRL and Contributors
# See license.txt
def test_placeholder():
assert True

View file

@ -0,0 +1,8 @@
// Copyright (c) 2026, TOP CLOSSERS SRL and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Payment Log", {
// refresh(frm) {
// },
// });

View file

@ -0,0 +1,160 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "DIDI-PL-.YYYY.-.#####",
"creation": "2026-03-30 12:05:21.168080",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
"customer_name",
"column_break_1",
"event_type",
"status",
"section_stripe",
"stripe_session_id",
"stripe_payment_intent_id",
"stripe_subscription_id",
"column_break_2",
"amount",
"currency",
"section_raw",
"raw_webhook_data"
],
"fields": [
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer"
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "event_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Event Type",
"options": "checkout.session.completed\npayment_intent.succeeded\ninvoice.payment_failed\ncustomer.subscription.updated\ncustomer.subscription.deleted\nrefund.created\nother"
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Succeeded\nFailed\nRefunded\nPending",
"reqd": 1
},
{
"fieldname": "section_stripe",
"fieldtype": "Section Break",
"label": "Stripe Details"
},
{
"fieldname": "stripe_session_id",
"fieldtype": "Data",
"label": "Stripe Session ID"
},
{
"fieldname": "stripe_payment_intent_id",
"fieldtype": "Data",
"label": "Stripe Payment Intent ID"
},
{
"fieldname": "stripe_subscription_id",
"fieldtype": "Data",
"label": "Stripe Subscription ID"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount"
},
{
"default": "RON",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"collapsible": 1,
"fieldname": "section_raw",
"fieldtype": "Section Break",
"label": "Raw Data"
},
{
"fieldname": "raw_webhook_data",
"fieldtype": "Code",
"label": "Raw Webhook Data",
"options": "JSON"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-03-30 12:05:21.168080",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Payment Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"search_fields": "customer,stripe_session_id,stripe_payment_intent_id,status",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "customer_name"
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2026, TOP CLOSSERS SRL and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PaymentLog(Document):
pass

View file

@ -0,0 +1,9 @@
# Copyright (c) 2026, TOP CLOSSERS SRL and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPaymentLog(FrappeTestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2026, TOP CLOSSERS SRL and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Service Agreement", {
// refresh(frm) {
// },
// });

View file

@ -0,0 +1,144 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "DIDI-SA-.YYYY.-.#####",
"creation": "2026-03-30 12:05:20.761021",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
"customer_name",
"column_break_1",
"plan",
"status",
"section_acceptance",
"acceptance_date",
"client_ip",
"column_break_2",
"terms_version",
"subscription",
"section_document",
"agreement_html"
],
"fields": [
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer",
"reqd": 1
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "plan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Subscription Plan",
"options": "Subscription Plan",
"reqd": 1
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Draft\nAccepted\nExpired\nCancelled",
"reqd": 1
},
{
"fieldname": "section_acceptance",
"fieldtype": "Section Break",
"label": "Acceptance Details"
},
{
"fieldname": "acceptance_date",
"fieldtype": "Datetime",
"label": "Acceptance Date"
},
{
"fieldname": "client_ip",
"fieldtype": "Data",
"label": "Client IP",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "terms_version",
"fieldtype": "Data",
"label": "Terms Version"
},
{
"fieldname": "subscription",
"fieldtype": "Link",
"label": "Subscription",
"options": "Subscription"
},
{
"fieldname": "section_document",
"fieldtype": "Section Break",
"label": "Agreement Document"
},
{
"fieldname": "agreement_html",
"fieldtype": "Text Editor",
"label": "Agreement Content"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-03-30 12:05:20.761021",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Service Agreement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"search_fields": "customer,plan,status",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "customer_name"
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2026, TOP CLOSSERS SRL and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ServiceAgreement(Document):
pass

View file

@ -0,0 +1,9 @@
# Copyright (c) 2026, TOP CLOSSERS SRL and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestServiceAgreement(FrappeTestCase):
pass

View file

@ -0,0 +1,9 @@
# Copyright (c) 2026, TOP CLOSSERS SRL and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestWebsiteContent(FrappeTestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2026, TOP CLOSSERS SRL and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Website Content", {
// refresh(frm) {
// },
// });

View file

@ -0,0 +1,138 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:{page_slug}-{section_key}",
"creation": "2026-03-30 12:05:21.259657",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"page_slug",
"section_key",
"display_order",
"is_active",
"section_content_ro",
"content_ro",
"section_content_en",
"content_en",
"section_media",
"image",
"column_break_media",
"extra_data"
],
"fields": [
{
"fieldname": "page_slug",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Page",
"options": "homepage\npricing\ncontact\nabout\nprivacy\nterms\nservices",
"reqd": 1
},
{
"description": "Unique key for this section (e.g. hero_title, feature_1, cta_text)",
"fieldname": "section_key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Section Key",
"reqd": 1
},
{
"default": "0",
"fieldname": "display_order",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Display Order"
},
{
"default": "1",
"fieldname": "is_active",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Active"
},
{
"fieldname": "section_content_ro",
"fieldtype": "Section Break",
"label": "Content (Romanian)"
},
{
"fieldname": "content_ro",
"fieldtype": "Text Editor",
"label": "Content RO"
},
{
"fieldname": "section_content_en",
"fieldtype": "Section Break",
"label": "Content (English)"
},
{
"fieldname": "content_en",
"fieldtype": "Text Editor",
"label": "Content EN"
},
{
"fieldname": "section_media",
"fieldtype": "Section Break",
"label": "Media"
},
{
"fieldname": "image",
"fieldtype": "Attach Image",
"label": "Image"
},
{
"fieldname": "column_break_media",
"fieldtype": "Column Break"
},
{
"description": "Additional structured data (e.g. button links, feature lists)",
"fieldname": "extra_data",
"fieldtype": "Code",
"label": "Extra Data (JSON)",
"options": "JSON"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-03-31 16:40:22.911045",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Website Content",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Website Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"search_fields": "page_slug,section_key",
"sort_field": "display_order",
"sort_order": "ASC",
"states": [],
"title_field": "section_key"
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2026, TOP CLOSSERS SRL and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class WebsiteContent(Document):
pass

View file

@ -0,0 +1,82 @@
{
"charts": [],
"content": "[{\"id\": \"h1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\"><b>Clienti</b></span>\", \"col\": 12}}, {\"id\": \"s0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Toti Clientii\", \"col\": 4}}, {\"id\": \"s1\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Acorduri Servicii\", \"col\": 4}}]",
"creation": "2026-04-03 13:27:54.927572",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "users",
"idx": 0,
"indicator_color": "blue",
"is_hidden": 0,
"label": "Clienti",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Clienti",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Customer",
"link_count": 0,
"link_to": "Customer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Service Agreement",
"link_count": 0,
"link_to": "Service Agreement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Subscription Plan",
"link_count": 0,
"link_to": "Subscription Plan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-04-03 13:27:54.927572",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Clienti",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 20.0,
"shortcuts": [
{
"color": "Blue",
"doc_view": "List",
"label": "Toti Clientii",
"link_to": "Customer",
"type": "DocType"
},
{
"color": "Purple",
"doc_view": "List",
"label": "Acorduri Servicii",
"link_to": "Service Agreement",
"type": "DocType"
}
],
"title": "Clienti"
}

View file

@ -0,0 +1,99 @@
{
"charts": [],
"content": "[{\"id\": \"h1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\"><b>Contabilitate</b></span>\", \"col\": 12}}, {\"id\": \"s0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Plan Conturi\", \"col\": 4}}, {\"id\": \"s1\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Furnizori\", \"col\": 4}}, {\"id\": \"s2\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Companie\", \"col\": 4}}]",
"creation": "2026-04-03 13:27:55.254523",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "calculator",
"idx": 0,
"indicator_color": "",
"is_hidden": 0,
"label": "Contabilitate",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Contabilitate",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Account",
"link_count": 0,
"link_to": "Account",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Company",
"link_count": 0,
"link_to": "Company",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Supplier",
"link_count": 0,
"link_to": "Supplier",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Invoice",
"link_count": 0,
"link_to": "Purchase Invoice",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-04-03 13:27:55.254523",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Contabilitate",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 60.0,
"shortcuts": [
{
"color": "Grey",
"doc_view": "Tree",
"label": "Plan Conturi",
"link_to": "Account",
"type": "DocType"
},
{
"color": "Blue",
"doc_view": "",
"label": "Furnizori",
"link_to": "Supplier",
"type": "DocType"
},
{
"color": "Green",
"doc_view": "",
"label": "Companie",
"link_to": "Company",
"type": "DocType"
}
],
"title": "Contabilitate"
}

View file

@ -0,0 +1,89 @@
{
"charts": [],
"content": "[{\"id\": \"h1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\"><b>CRM</b></span>\", \"col\": 12}}, {\"id\": \"s0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Lead-uri\", \"col\": 4}}, {\"id\": \"s1\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Stage\", \"col\": 4}}, {\"id\": \"s2\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Lead Source\", \"col\": 4}}]",
"creation": "2026-04-03 13:27:55.010265",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "share",
"idx": 0,
"indicator_color": "orange",
"is_hidden": 0,
"label": "CRM",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "CRM",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Lead",
"link_count": 0,
"link_to": "Lead",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales Stage",
"link_count": 0,
"link_to": "Sales Stage",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Lead Source",
"link_count": 0,
"link_to": "Lead Source",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-04-03 13:27:55.010265",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "CRM",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 30.0,
"shortcuts": [
{
"color": "Orange",
"doc_view": "List",
"label": "Lead-uri",
"link_to": "Lead",
"type": "DocType"
},
{
"color": "Yellow",
"doc_view": "",
"label": "Sales Stage",
"link_to": "Sales Stage",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "",
"label": "Lead Source",
"link_to": "Lead Source",
"type": "DocType"
}
],
"title": "CRM"
}

View file

@ -0,0 +1,94 @@
{
"charts": [],
"content": "[]",
"creation": "2026-03-31 20:55:24.921276",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "home",
"idx": 0,
"indicator_color": "green",
"is_hidden": 1,
"label": "DiDi",
"links": [],
"modified": "2026-04-03 13:22:23.399406",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "DiDi",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 0.0,
"shortcuts": [
{
"doc_view": "",
"label": "FACTURI",
"link_to": "Sales Invoice",
"type": "DocType"
},
{
"doc_view": "",
"label": "CLIENTI",
"link_to": "Customer",
"type": "DocType"
},
{
"doc_view": "",
"label": "LEAD-URI CRM",
"link_to": "Lead",
"type": "DocType"
},
{
"doc_view": "",
"label": "LOG PLATI STRIPE",
"link_to": "Payment Log",
"type": "DocType"
},
{
"doc_view": "",
"label": "ACORDURI SERVICII",
"link_to": "Service Agreement",
"type": "DocType"
},
{
"doc_view": "",
"label": "SERVICII / PRODUSE",
"link_to": "Item",
"type": "DocType"
},
{
"doc_view": "",
"label": "PLANURI ABONAMENT",
"link_to": "Subscription Plan",
"type": "DocType"
},
{
"doc_view": "",
"label": "CONTINUT WEBSITE (CMS)",
"link_to": "Website Content",
"type": "DocType"
},
{
"doc_view": "",
"label": "FURNIZORI",
"link_to": "Supplier",
"type": "DocType"
},
{
"doc_view": "",
"label": "TEMPLATE-URI EMAIL",
"link_to": "Email Template",
"type": "DocType"
},
{
"doc_view": "",
"label": "PLAN DE CONTURI",
"link_to": "Account",
"type": "DocType"
}
],
"title": "DiDi"
}

View file

@ -0,0 +1,214 @@
{
"charts": [],
"content": "[ {\"id\":\"h1\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Acces rapid</b></span>\",\"col\":12}}, {\"id\":\"s1\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Facturi\",\"col\":3}}, {\"id\":\"s2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Clienti\",\"col\":3}}, {\"id\":\"s3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead-uri CRM\",\"col\":3}}, {\"id\":\"s4\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plati Stripe\",\"col\":3}}, {\"id\":\"s5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"CMS Content\",\"col\":3}}, {\"id\":\"s6\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Servicii\",\"col\":3}}, {\"id\":\"s7\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Service Agreements\",\"col\":3}}, {\"id\":\"sp1\",\"type\":\"spacer\",\"data\":{\"col\":12}}, {\"id\":\"h2\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Module</b></span>\",\"col\":12}} ]\n",
"creation": "2026-03-31 22:31:39.036298",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
"indicator_color": "green",
"is_hidden": 1,
"label": "DiDi Platform",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Vanzari & Facturare",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Facturi",
"link_count": 0,
"link_to": "Sales Invoice",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Clienti",
"link_count": 0,
"link_to": "Customer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Servicii (Items)",
"link_count": 0,
"link_to": "Item",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Subscription Plan",
"link_count": 0,
"link_to": "Subscription Plan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "CRM",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Lead-uri",
"link_count": 0,
"link_to": "Lead",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Service Agreement",
"link_count": 0,
"link_to": "Service Agreement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Plati & Financiar",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Log",
"link_count": 0,
"link_to": "Payment Log",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Companie",
"link_count": 0,
"link_to": "Company",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Plan de Conturi",
"link_count": 0,
"link_to": "Account",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Website CMS",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Website Content",
"link_count": 0,
"link_to": "Website Content",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
}
],
"modified": "2026-04-03 13:22:23.287762",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "DiDi Platform",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 1.0,
"shortcuts": [
{
"color": "Green",
"doc_view": "",
"label": "Facturi",
"link_to": "Sales Invoice",
"type": "DocType"
},
{
"color": "Blue",
"doc_view": "",
"label": "Clienti",
"link_to": "Customer",
"type": "DocType"
},
{
"color": "Orange",
"doc_view": "",
"label": "Lead-uri CRM",
"link_to": "Lead",
"type": "DocType"
},
{
"color": "Yellow",
"doc_view": "",
"label": "Plati Stripe",
"link_to": "Payment Log",
"type": "DocType"
},
{
"color": "Cyan",
"doc_view": "",
"label": "CMS Content",
"link_to": "Website Content",
"type": "DocType"
},
{
"color": "Purple",
"doc_view": "",
"label": "Servicii",
"link_to": "Item",
"type": "DocType"
},
{
"color": "Pink",
"doc_view": "",
"label": "Service Agreements",
"link_to": "Service Agreement",
"type": "DocType"
}
],
"title": "DiDi Platform"
}

View file

@ -0,0 +1,82 @@
{
"charts": [],
"content": "[{\"id\": \"h1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\"><b>Facturi</b></span>\", \"col\": 12}}, {\"id\": \"s0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Toate Facturile\", \"col\": 4}}, {\"id\": \"s1\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payment Entry\", \"col\": 4}}]",
"creation": "2026-04-03 13:22:24.538065",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "file-text",
"idx": 0,
"indicator_color": "green",
"is_hidden": 0,
"label": "Facturi",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Facturi",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales Invoice",
"link_count": 0,
"link_to": "Sales Invoice",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Entry",
"link_count": 0,
"link_to": "Payment Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Taxe (TVA)",
"link_count": 0,
"link_to": "Sales Taxes and Charges Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-04-03 13:27:54.838261",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Facturi",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 10.0,
"shortcuts": [
{
"color": "Green",
"doc_view": "List",
"label": "Toate Facturile",
"link_to": "Sales Invoice",
"type": "DocType"
},
{
"color": "Yellow",
"doc_view": "List",
"label": "Payment Entry",
"link_to": "Payment Entry",
"type": "DocType"
}
],
"title": "Facturi"
}

View file

@ -0,0 +1,72 @@
{
"charts": [],
"content": "[{\"id\": \"h1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\"><b>Plati</b></span>\", \"col\": 12}}, {\"id\": \"s0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payment Log\", \"col\": 4}}, {\"id\": \"s1\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Rapoarte Analiza\", \"col\": 4}}]",
"creation": "2026-04-03 13:27:55.169179",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "credit-card",
"idx": 0,
"indicator_color": "yellow",
"is_hidden": 0,
"label": "Plati",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Plati",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Log",
"link_count": 0,
"link_to": "Payment Log",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Analysis Report",
"link_count": 0,
"link_to": "Analysis Report",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-04-03 13:27:55.169179",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Plati",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 50.0,
"shortcuts": [
{
"color": "Yellow",
"doc_view": "List",
"label": "Payment Log",
"link_to": "Payment Log",
"type": "DocType"
},
{
"color": "Pink",
"doc_view": "List",
"label": "Rapoarte Analiza",
"link_to": "Analysis Report",
"type": "DocType"
}
],
"title": "Plati"
}

View file

@ -0,0 +1,82 @@
{
"charts": [],
"content": "[{\"id\": \"h1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\"><b>Servicii</b></span>\", \"col\": 12}}, {\"id\": \"s0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Articole\", \"col\": 4}}, {\"id\": \"s1\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Planuri Abonament\", \"col\": 4}}]",
"creation": "2026-04-03 13:27:55.090677",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "box",
"idx": 0,
"indicator_color": "purple",
"is_hidden": 0,
"label": "Servicii",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Catalog",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Item",
"link_count": 0,
"link_to": "Item",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Item Group",
"link_count": 0,
"link_to": "Item Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Subscription Plan",
"link_count": 0,
"link_to": "Subscription Plan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-04-03 13:27:55.090677",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Servicii",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 40.0,
"shortcuts": [
{
"color": "Purple",
"doc_view": "List",
"label": "Articole",
"link_to": "Item",
"type": "DocType"
},
{
"color": "Cyan",
"doc_view": "",
"label": "Planuri Abonament",
"link_to": "Subscription Plan",
"type": "DocType"
}
],
"title": "Servicii"
}

View file

@ -0,0 +1,72 @@
{
"charts": [],
"content": "[{\"id\": \"h1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\"><b>Website CMS</b></span>\", \"col\": 12}}, {\"id\": \"s0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Continut Website\", \"col\": 4}}, {\"id\": \"s1\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Template Email\", \"col\": 4}}]",
"creation": "2026-04-03 13:27:55.336131",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "globe",
"idx": 0,
"indicator_color": "cyan",
"is_hidden": 0,
"label": "Website CMS",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "CMS",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Website Content",
"link_count": 0,
"link_to": "Website Content",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Email Template",
"link_count": 0,
"link_to": "Email Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-04-03 13:27:55.336131",
"modified_by": "Administrator",
"module": "Didi Custom",
"name": "Website CMS",
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 70.0,
"shortcuts": [
{
"color": "Cyan",
"doc_view": "List",
"label": "Continut Website",
"link_to": "Website Content",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "",
"label": "Template Email",
"link_to": "Email Template",
"type": "DocType"
}
],
"title": "Website CMS"
}

View file

@ -0,0 +1,33 @@
import frappe
def execute():
# Use Property Setter to change plan from Link → Data without modifying core doctype
if not frappe.db.exists("Property Setter", {"doc_type": "Service Agreement", "field_name": "plan", "property": "fieldtype"}):
ps = frappe.get_doc({
"doctype": "Property Setter",
"doctype_or_field": "DocField",
"doc_type": "Service Agreement",
"field_name": "plan",
"property": "fieldtype",
"value": "Data",
"property_type": "Select",
})
ps.flags.ignore_permissions = True
ps.insert()
print("Created Property Setter: fieldtype=Data")
if not frappe.db.exists("Property Setter", {"doc_type": "Service Agreement", "field_name": "plan", "property": "options"}):
ps2 = frappe.get_doc({
"doctype": "Property Setter",
"doctype_or_field": "DocField",
"doc_type": "Service Agreement",
"field_name": "plan",
"property": "options",
"value": "",
"property_type": "Text",
})
ps2.flags.ignore_permissions = True
ps2.insert()
print("Created Property Setter: options=")
frappe.db.commit()
frappe.clear_cache()
print("DONE")

View file

@ -0,0 +1,292 @@
app_name = "didi_custom"
app_title = "Didi Custom"
app_publisher = "TOP CLOSSERS SRL"
app_description = "Custom DocTypes for DiDi Platform"
app_email = "office@clossers.com"
app_license = "mit"
# ============================================
# DiDi Business Logic Hooks
# ============================================
# DocType lifecycle events
# ------------------------
# When Payment Log is created (by website /api/webhooks/stripe), trigger IAM
# provisioning to Keycloak (update user attributes + roles).
# When Sales Invoice is saved, tag sales_channel for DESI 11 tracking.
doc_events = {
"Payment Log": {
"after_insert": "didi_custom.integrations.stripe_provisioning.on_payment_log_insert",
},
"Sales Invoice": {
"before_save": "didi_custom.integrations.sales_channel.tag_sales_channel",
},
# Subscription cancellation triggers downgrade to free tier
"Subscription": {
"on_cancel": "didi_custom.integrations.stripe_provisioning.on_subscription_canceled",
},
}
# Scheduler events
# ----------------
# Daily: expire subscriptions past their billing cycle (downgrade to free).
# Hourly: sync Stripe state (lightweight reconciliation between webhooks).
scheduler_events = {
"daily": [
"didi_custom.integrations.subscription_lifecycle.expire_subscriptions",
"didi_custom.integrations.subscription_lifecycle.send_expiry_reminders",
],
"hourly": [
"didi_custom.integrations.subscription_lifecycle.hourly_sync_stripe",
],
}
# Whitelisted REST methods (callable via /api/method/<path>)
# ---------------------------------------------------------
# DESI 11 audit report — read-only metric for compliance dashboard.
override_whitelisted_methods = {
"didi_custom.api.desi11_report": "didi_custom.integrations.sales_channel.desi11_report",
}
# Apps
# ------------------
# required_apps = []
# Each item in the list will be shown as an app in the apps page
# add_to_apps_screen = [
# {
# "name": "didi_custom",
# "logo": "/assets/didi_custom/logo.png",
# "title": "Didi Custom",
# "route": "/didi_custom",
# "has_permission": "didi_custom.api.permission.has_app_permission"
# }
# ]
# Includes in <head>
# ------------------
# include js, css files in header of desk.html
# app_include_css = "/assets/didi_custom/css/didi_custom.css"
# app_include_js = "/assets/didi_custom/js/didi_custom.js"
# include js, css files in header of web template
# web_include_css = "/assets/didi_custom/css/didi_custom.css"
# web_include_js = "/assets/didi_custom/js/didi_custom.js"
# include custom scss in every website theme (without file extension ".scss")
# website_theme_scss = "didi_custom/public/scss/website"
# include js, css files in header of web form
# webform_include_js = {"doctype": "public/js/doctype.js"}
# webform_include_css = {"doctype": "public/css/doctype.css"}
# include js in page
# page_js = {"page" : "public/js/file.js"}
# include js in doctype views
# doctype_js = {"doctype" : "public/js/doctype.js"}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
# Svg Icons
# ------------------
# include app icons in desk
# app_include_icons = "didi_custom/public/icons.svg"
# Home Pages
# ----------
# application home page (will override Website Settings)
# home_page = "login"
# website user home page (by Role)
# role_home_page = {
# "Role": "home_page"
# }
# Generators
# ----------
# automatically create page for each record of this doctype
# website_generators = ["Web Page"]
# Jinja
# ----------
# add methods and filters to jinja environment
# jinja = {
# "methods": "didi_custom.utils.jinja_methods",
# "filters": "didi_custom.utils.jinja_filters"
# }
# Installation
# ------------
# before_install = "didi_custom.install.before_install"
# after_install = "didi_custom.install.after_install"
# Uninstallation
# ------------
# before_uninstall = "didi_custom.uninstall.before_uninstall"
# after_uninstall = "didi_custom.uninstall.after_uninstall"
# Integration Setup
# ------------------
# To set up dependencies/integrations with other apps
# Name of the app being installed is passed as an argument
# before_app_install = "didi_custom.utils.before_app_install"
# after_app_install = "didi_custom.utils.after_app_install"
# Integration Cleanup
# -------------------
# To clean up dependencies/integrations with other apps
# Name of the app being uninstalled is passed as an argument
# before_app_uninstall = "didi_custom.utils.before_app_uninstall"
# after_app_uninstall = "didi_custom.utils.after_app_uninstall"
# Desk Notifications
# ------------------
# See frappe.core.notifications.get_notification_config
# notification_config = "didi_custom.notifications.get_notification_config"
# Permissions
# -----------
# Permissions evaluated in scripted ways
# permission_query_conditions = {
# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
# }
#
# has_permission = {
# "Event": "frappe.desk.doctype.event.event.has_permission",
# }
# DocType Class
# ---------------
# Override standard doctype classes
# override_doctype_class = {
# "ToDo": "custom_app.overrides.CustomToDo"
# }
# Document Events
# ---------------
# Hook on document methods and events
# doc_events = {
# "*": {
# "on_update": "method",
# "on_cancel": "method",
# "on_trash": "method"
# }
# }
# Scheduled Tasks
# ---------------
# scheduler_events = {
# "all": [
# "didi_custom.tasks.all"
# ],
# "daily": [
# "didi_custom.tasks.daily"
# ],
# "hourly": [
# "didi_custom.tasks.hourly"
# ],
# "weekly": [
# "didi_custom.tasks.weekly"
# ],
# "monthly": [
# "didi_custom.tasks.monthly"
# ],
# }
# Testing
# -------
# before_tests = "didi_custom.install.before_tests"
# Overriding Methods
# ------------------------------
#
# override_whitelisted_methods = {
# "frappe.desk.doctype.event.event.get_events": "didi_custom.event.get_events"
# }
#
# each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard,
# along with any modifications made in other Frappe apps
# override_doctype_dashboards = {
# "Task": "didi_custom.task.get_dashboard_data"
# }
# exempt linked doctypes from being automatically cancelled
#
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
# Ignore links to specified DocTypes when deleting documents
# -----------------------------------------------------------
# ignore_links_on_delete = ["Communication", "ToDo"]
# Request Events
# ----------------
# before_request = ["didi_custom.utils.before_request"]
# after_request = ["didi_custom.utils.after_request"]
# Job Events
# ----------
# before_job = ["didi_custom.utils.before_job"]
# after_job = ["didi_custom.utils.after_job"]
# User Data Protection
# --------------------
# user_data_fields = [
# {
# "doctype": "{doctype_1}",
# "filter_by": "{filter_by}",
# "redact_fields": ["{field_1}", "{field_2}"],
# "partial": 1,
# },
# {
# "doctype": "{doctype_2}",
# "filter_by": "{filter_by}",
# "partial": 1,
# },
# {
# "doctype": "{doctype_3}",
# "strict": False,
# },
# {
# "doctype": "{doctype_4}"
# }
# ]
# Authentication and authorization
# --------------------------------
# auth_hooks = [
# "didi_custom.auth.validate"
# ]
# Automatically update python controller files with type annotations for this app.
# export_python_type_annotations = True
# default_log_clearing_doctypes = {
# "Logging DocType Name": 30 # days to retain logs
# }
# Translation
# ------------
# List of apps whose translatable strings should be excluded from this app's translations.
# ignore_translatable_strings_from = []

View file

@ -0,0 +1,7 @@
import frappe, re
def execute():
for n in ["contact-title", "contact-info", "about-hero", "homepage-hero_title"]:
d = frappe.get_doc("Website Content", n)
txt = re.sub(r"<[^>]+>", " ", d.content_ro or "").strip()[:90]
print("ROW:", n, "=>", repr(txt))

View file

@ -0,0 +1,185 @@
"""
Keycloak Admin API wrapper for DiDi user provisioning.
Used by Stripe webhook flow to update user attributes after successful payment:
- active_plan (free | paid | enterprise)
- plan_activation_date
- iam_role (free_tier | paid_tier | enterprise_tier)
Configuration via Frappe site_config (NOT in code):
bench --site didi-erp set-config keycloak_url 'http://didi-keycloak:8080'
bench --site didi-erp set-config keycloak_realm 'didi-clients'
bench --site didi-erp set-config keycloak_admin_user 'admin'
bench --site didi-erp set-config keycloak_admin_password 'admin123'
"""
import frappe
import requests
from typing import Optional, Dict, Any
def _get_config() -> Dict[str, str]:
"""Read Keycloak config from Frappe site_config."""
return {
"url": frappe.conf.get("keycloak_url", "http://didi-keycloak:8080"),
"realm": frappe.conf.get("keycloak_realm", "didi-clients"),
"admin_user": frappe.conf.get("keycloak_admin_user", "admin"),
"admin_pwd": frappe.conf.get("keycloak_admin_password"),
}
def _get_admin_token() -> Optional[str]:
"""Login as admin against master realm — returns access token."""
cfg = _get_config()
if not cfg["admin_pwd"]:
frappe.log_error(
"Keycloak admin password not configured in site_config",
"Keycloak provisioning",
)
return None
url = f"{cfg['url']}/realms/master/protocol/openid-connect/token"
try:
r = requests.post(
url,
data={
"grant_type": "password",
"client_id": "admin-cli",
"username": cfg["admin_user"],
"password": cfg["admin_pwd"],
},
timeout=10,
)
r.raise_for_status()
return r.json().get("access_token")
except Exception as e:
frappe.log_error(f"Keycloak admin login failed: {e}", "Keycloak provisioning")
return None
def _find_user_by_email(token: str, email: str) -> Optional[Dict[str, Any]]:
"""Lookup Keycloak user by email. Returns user dict or None."""
cfg = _get_config()
url = f"{cfg['url']}/admin/realms/{cfg['realm']}/users"
try:
r = requests.get(
url,
params={"email": email, "exact": "true"},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
r.raise_for_status()
users = r.json()
return users[0] if users else None
except Exception as e:
frappe.log_error(
f"Keycloak user lookup failed for {email}: {e}",
"Keycloak provisioning",
)
return None
def update_user_attributes(
email: str,
attributes: Dict[str, str],
roles_add: Optional[list] = None,
roles_remove: Optional[list] = None,
) -> bool:
"""
Update Keycloak user with new attributes + optional role changes.
Used by stripe_provisioning to set:
- active_plan: "free" | "paid" | "enterprise"
- plan_activation_date: ISO timestamp
- subscription_id: Stripe sub_xxx
Roles add/remove use realm-level role names (free_tier, paid_tier, enterprise_tier).
Returns True on success, False on failure (logged to Error Log doctype).
"""
token = _get_admin_token()
if not token:
return False
user = _find_user_by_email(token, email)
if not user:
frappe.log_error(
f"Keycloak user not found for email {email}",
"Keycloak provisioning",
)
return False
cfg = _get_config()
user_id = user["id"]
# Merge attributes (Keycloak stores attributes as list of strings)
existing_attrs = user.get("attributes", {}) or {}
for key, value in attributes.items():
existing_attrs[key] = [str(value)]
# PUT user with merged attributes
try:
r = requests.put(
f"{cfg['url']}/admin/realms/{cfg['realm']}/users/{user_id}",
json={**user, "attributes": existing_attrs},
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
timeout=10,
)
r.raise_for_status()
except Exception as e:
frappe.log_error(
f"Keycloak user update failed for {email}: {e}",
"Keycloak provisioning",
)
return False
# Handle role changes (separate Keycloak endpoints)
if roles_add or roles_remove:
_update_user_roles(token, cfg, user_id, roles_add, roles_remove)
return True
def _update_user_roles(
token: str,
cfg: Dict[str, str],
user_id: str,
roles_add: Optional[list],
roles_remove: Optional[list],
):
"""Add/remove realm-level roles on user."""
role_url = f"{cfg['url']}/admin/realms/{cfg['realm']}/users/{user_id}/role-mappings/realm"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
# Resolve role names → role objects (Keycloak needs full role representation)
def _resolve_roles(names: list) -> list:
out = []
for name in names:
try:
r = requests.get(
f"{cfg['url']}/admin/realms/{cfg['realm']}/roles/{name}",
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
if r.ok:
out.append(r.json())
except Exception as e:
frappe.log_error(f"Role resolve failed {name}: {e}", "Keycloak provisioning")
return out
try:
if roles_add:
resolved = _resolve_roles(roles_add)
if resolved:
requests.post(role_url, json=resolved, headers=headers, timeout=10).raise_for_status()
if roles_remove:
resolved = _resolve_roles(roles_remove)
if resolved:
requests.delete(role_url, json=resolved, headers=headers, timeout=10).raise_for_status()
except Exception as e:
frappe.log_error(f"Role update failed: {e}", "Keycloak provisioning")

View file

@ -0,0 +1,216 @@
"""
ERPNext Custom Fields migration for DiDi business logic.
Adds fields required by hookurile din stripe_provisioning + sales_channel +
subscription_lifecycle. Idempotent safe to run multiple times.
Run via:
docker exec didi-erpnext bench --site didi-erp execute \
didi_custom.integrations.migrations.setup_custom_fields.execute
Or via bench console:
docker exec -it didi-erpnext bench --site didi-erp console
>>> from didi_custom.integrations.migrations import setup_custom_fields
>>> setup_custom_fields.execute()
"""
import frappe
CUSTOM_FIELDS = {
"Customer": [
{
"fieldname": "didi_section",
"fieldtype": "Section Break",
"label": "DiDi Platform",
"insert_after": "tax_id",
"collapsible": 1,
},
{
"fieldname": "didi_user_id",
"fieldtype": "Data",
"label": "DiDi User ID (Keycloak sub)",
"insert_after": "didi_section",
"read_only": 1,
"unique": 1,
"description": "Keycloak UUID — linked to Authentication subject",
},
{
"fieldname": "active_plan",
"fieldtype": "Select",
"label": "Active Plan",
"options": "\nfree\npaid\nenterprise",
"insert_after": "didi_user_id",
"default": "free",
"in_list_view": 1,
},
{
"fieldname": "iam_role",
"fieldtype": "Select",
"label": "IAM Role",
"options": "\nfree_tier\npaid_tier\nenterprise_tier",
"insert_after": "active_plan",
"default": "free_tier",
"read_only": 1,
"description": "Auto-synced cu Keycloak realm role",
},
{
"fieldname": "plan_activation_date",
"fieldtype": "Date",
"label": "Plan Activation Date",
"insert_after": "iam_role",
"read_only": 1,
},
{
"fieldname": "stripe_customer_id",
"fieldtype": "Data",
"label": "Stripe Customer ID",
"insert_after": "plan_activation_date",
"read_only": 1,
"unique": 1,
},
{
"fieldname": "stripe_subscription_id",
"fieldtype": "Data",
"label": "Stripe Subscription ID",
"insert_after": "stripe_customer_id",
"read_only": 1,
},
],
"Sales Invoice": [
{
"fieldname": "didi_section",
"fieldtype": "Section Break",
"label": "DiDi Sales Tracking",
"insert_after": "total_taxes_and_charges",
"collapsible": 1,
},
{
"fieldname": "sales_channel",
"fieldtype": "Select",
"label": "Sales Channel",
"options": "\nonline\napi\noffline",
"insert_after": "didi_section",
"default": "offline",
"in_list_view": 1,
"in_standard_filter": 1,
"description": "Auto-tagged: online=Stripe Checkout, api=DiDi platform direct, offline=manual",
},
{
"fieldname": "stripe_invoice_id",
"fieldtype": "Data",
"label": "Stripe Invoice ID",
"insert_after": "sales_channel",
"read_only": 1,
"unique": 1,
"allow_on_submit": 1,
},
{
"fieldname": "stripe_payment_intent_id",
"fieldtype": "Data",
"label": "Stripe Payment Intent",
"insert_after": "stripe_invoice_id",
"read_only": 1,
"allow_on_submit": 1,
},
{
"fieldname": "analysis_session_id",
"fieldtype": "Data",
"label": "DiDi Analysis Session ID",
"insert_after": "stripe_payment_intent_id",
"read_only": 1,
"allow_on_submit": 1,
"description": "Set când factura provine din consum API DiDi (per-analyze billing)",
},
{
"fieldname": "analysis_consumed",
"fieldtype": "Int",
"label": "Analyses Consumed",
"insert_after": "analysis_session_id",
"default": 0,
"allow_on_submit": 1,
"description": "Numar analize procesate pentru aceasta factura",
},
],
"Service Agreement": [
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"label": "Factura asociata",
"options": "Sales Invoice",
"insert_after": "plan",
"read_only": 1,
},
],
"Lead": [
{
"fieldname": "didi_section",
"fieldtype": "Section Break",
"label": "DiDi Marketing",
"insert_after": "source",
"collapsible": 1,
},
{
"fieldname": "source_form",
"fieldtype": "Data",
"label": "Source Form (page)",
"insert_after": "didi_section",
},
{
"fieldname": "page_origin",
"fieldtype": "Data",
"label": "Origin URL",
"insert_after": "source_form",
},
{
"fieldname": "utm_source",
"fieldtype": "Data",
"label": "UTM Source",
"insert_after": "page_origin",
},
{
"fieldname": "utm_medium",
"fieldtype": "Data",
"label": "UTM Medium",
"insert_after": "utm_source",
},
{
"fieldname": "utm_campaign",
"fieldtype": "Data",
"label": "UTM Campaign",
"insert_after": "utm_medium",
},
],
}
def execute():
"""Add all custom fields idempotently."""
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
print("Setting up DiDi custom fields...")
create_custom_fields(CUSTOM_FIELDS, ignore_validate=True)
frappe.db.commit()
print("✓ Custom fields created/updated")
# Reload doctype meta so changes are immediately visible
for doctype in CUSTOM_FIELDS.keys():
frappe.clear_cache(doctype=doctype)
print(f"✓ Cache cleared for {doctype}")
print("\nDone! Custom fields ready for use by:")
print(" - stripe_provisioning.py (Customer.iam_role, active_plan, ...)")
print(" - sales_channel.py (Sales Invoice.sales_channel)")
print(" - subscription_lifecycle.py (Customer.stripe_subscription_id)")
def rollback():
"""Remove all DiDi custom fields. USE WITH CAUTION."""
print("Removing DiDi custom fields...")
for doctype, fields in CUSTOM_FIELDS.items():
for field in fields:
name = f"{doctype}-{field['fieldname']}"
if frappe.db.exists("Custom Field", name):
frappe.delete_doc("Custom Field", name, ignore_permissions=True)
print(f" ✗ Removed {name}")
frappe.db.commit()
print("Done.")

View file

@ -0,0 +1,106 @@
"""
Sales Invoice channel tagging for DESI 11 (e-commerce online vs offline tracking).
DESI 11 cere "minim 1% vanzari online" pentru audit PNRR avem nevoie sa distingem
clar facturile vandute prin website Stripe (online) de cele facute manual in ERPNext
(office sales, offline).
Trigger: Sales Invoice before_save (Frappe hook).
Logic: daca factura are stripe_invoice_id sau analysis_session_id setat online.
Altfel offline (default).
Adauga in Sales Invoice un custom field "sales_channel" cu valori:
- online: facturat prin website Stripe checkout
- offline: facturat manual din ERPNext UI
- api: facturat prin integration directa (extension browser, API key, etc)
"""
import frappe
def tag_sales_channel(doc, method=None):
"""
Frappe hook: before_save on Sales Invoice.
Sets sales_channel field based on invoice metadata.
"""
# Skip if already tagged (preserve manual override)
if getattr(doc, "sales_channel", None):
return
# Heuristic 1: Stripe-originated invoices have stripe_invoice_id
if getattr(doc, "stripe_invoice_id", None):
doc.sales_channel = "online"
return
# Heuristic 2: API-originated (extension browser, agent-v3, etc.)
if getattr(doc, "analysis_session_id", None):
doc.sales_channel = "api"
return
# Heuristic 3: Items linked to consumable analyze services (per-analysis pricing)
didi_service_items = ["DIDI-TECHNIQUES", "DIDI-AI_DETECTION", "DIDI-CLAIMS", "DIDI-SOURCE"]
for item_row in (doc.items or []):
item_code = getattr(item_row, "item_code", "")
if any(item_code.startswith(prefix) for prefix in didi_service_items):
doc.sales_channel = "api"
return
# Default: offline (manual entry)
doc.sales_channel = "offline"
def desi11_report():
"""
Generate DESI 11 metric: % of revenue from online sales (vs total).
Returns dict with:
- total_revenue
- online_revenue
- online_percentage
- threshold_met: True if online_percentage >= 1.0%
Used by audit script (e.g., audit/desi_report.py) to produce annual report.
"""
from frappe.utils import get_fiscal_year, getdate, today
fy = get_fiscal_year(today())
fy_start, fy_end = fy[1], fy[2]
# Sum all submitted invoices in current fiscal year
invoices = frappe.get_all(
"Sales Invoice",
filters={
"docstatus": 1,
"posting_date": ["between", [fy_start, fy_end]],
},
fields=["name", "grand_total", "sales_channel"],
)
totals = {"total_revenue": 0, "online_revenue": 0, "api_revenue": 0, "offline_revenue": 0}
for inv in invoices:
amt = inv.grand_total or 0
totals["total_revenue"] += amt
channel = inv.sales_channel or "offline"
if channel == "online":
totals["online_revenue"] += amt
elif channel == "api":
totals["api_revenue"] += amt
else:
totals["offline_revenue"] += amt
total = totals["total_revenue"]
online_pct = (totals["online_revenue"] / total * 100) if total > 0 else 0
api_pct = (totals["api_revenue"] / total * 100) if total > 0 else 0
# DESI 11 = online OR api (both are "non-traditional" channels)
desi11_pct = ((totals["online_revenue"] + totals["api_revenue"]) / total * 100) if total > 0 else 0
return {
"fiscal_year": fy[0],
"period": {"start": str(fy_start), "end": str(fy_end)},
"total_revenue": totals["total_revenue"],
"online_revenue": totals["online_revenue"],
"api_revenue": totals["api_revenue"],
"offline_revenue": totals["offline_revenue"],
"online_percentage": round(online_pct, 2),
"api_percentage": round(api_pct, 2),
"desi11_percentage": round(desi11_pct, 2),
"threshold_met": desi11_pct >= 1.0,
}

View file

@ -0,0 +1,194 @@
"""
Stripe Payment Log IAM provisioning hook.
Triggered on Payment Log insert (via hooks.py doc_events):
- When Stripe webhook arrives and creates Payment Log this hook runs
- Maps Stripe event_type to active_plan + iam_role
- Calls Keycloak Admin API to update user attributes + roles
- Also updates Customer doctype iam_role + active_plan fields
The Payment Log is created by the website's /api/webhooks/stripe handler.
"""
import frappe
from datetime import datetime
from typing import Optional
from .keycloak import update_user_attributes
# Map Stripe event type → desired IAM state
PLAN_FROM_PRICE_ID = {
# Paid monthly/yearly
frappe.conf.get("stripe_price_paid_monthly", "price_1TazV2COBVRKKFDgXAZy0PCa"): ("paid", "paid_tier"),
frappe.conf.get("stripe_price_paid_yearly", "price_1TazV2COBVRKKFDgvar35wzy"): ("paid", "paid_tier"),
# Enterprise monthly/yearly
frappe.conf.get("stripe_price_enterprise_monthly", "price_1TazV3COBVRKKFDgNbaz7PTO"): ("enterprise", "enterprise_tier"),
frappe.conf.get("stripe_price_enterprise_yearly", "price_1TazV4COBVRKKFDgHRaklc5l"): ("enterprise", "enterprise_tier"),
}
def on_payment_log_insert(doc, method=None):
"""
Frappe hook called when a Payment Log document is inserted.
Configured in hooks.py:
doc_events = {
"Payment Log": {
"after_insert": "didi_custom.integrations.stripe_provisioning.on_payment_log_insert"
}
}
"""
# Only process successful subscription events
event_type = (doc.event_type or "").lower()
if event_type not in (
"checkout.session.completed",
"customer.subscription.created",
"customer.subscription.updated",
"invoice.payment_succeeded",
):
return
if doc.status != "succeeded" and event_type != "customer.subscription.updated":
return
# Find related customer
customer_email = _extract_customer_email(doc)
if not customer_email:
frappe.log_error(
f"Payment Log {doc.name}: cannot determine customer email — skipping IAM",
"Stripe provisioning",
)
return
# Determine target plan + role from Stripe data
plan, role = _resolve_plan_role(doc)
if not plan:
frappe.log_error(
f"Payment Log {doc.name}: cannot determine plan from event — defaulting to free",
"Stripe provisioning",
)
plan, role = ("free", "free_tier")
# Update Customer in ERPNext
_update_customer(customer_email, plan, role, doc.stripe_subscription_id)
# Update Keycloak (best-effort, fail-open)
activation_iso = datetime.utcnow().isoformat() + "Z"
update_user_attributes(
email=customer_email,
attributes={
"active_plan": plan,
"plan_activation_date": activation_iso,
"stripe_subscription_id": doc.stripe_subscription_id or "",
},
roles_add=[role],
roles_remove=[r for r in ("free_tier", "paid_tier", "enterprise_tier") if r != role],
)
def on_subscription_canceled(doc, method=None):
"""
Triggered on subscription cancellation / expiration.
Downgrades user to free_tier.
"""
customer_email = _extract_customer_email(doc)
if not customer_email:
return
_update_customer(customer_email, "free", "free_tier", None)
update_user_attributes(
email=customer_email,
attributes={
"active_plan": "free",
"plan_activation_date": datetime.utcnow().isoformat() + "Z",
"stripe_subscription_id": "",
},
roles_add=["free_tier"],
roles_remove=["paid_tier", "enterprise_tier"],
)
def _extract_customer_email(doc) -> Optional[str]:
"""Try multiple sources to find customer email."""
# 1. From raw_webhook_data JSON
if doc.raw_webhook_data:
try:
import json
data = json.loads(doc.raw_webhook_data) if isinstance(doc.raw_webhook_data, str) else doc.raw_webhook_data
obj = data.get("data", {}).get("object", {})
for key in ("customer_email", "receipt_email"):
if obj.get(key):
return obj[key]
# Look up via customer ID
if obj.get("customer"):
# Stripe customer ID — would need to query Stripe API to get email
# For now, look up linked Customer in ERP by stripe_customer_id
pass
except Exception:
pass
# 2. From linked Customer (via stripe_customer_id field)
if doc.stripe_payment_intent_id:
customers = frappe.get_all(
"Customer",
filters={"stripe_customer_id": doc.stripe_payment_intent_id},
fields=["email_id"],
limit=1,
)
if customers and customers[0].email_id:
return customers[0].email_id
return None
def _resolve_plan_role(doc) -> tuple:
"""Map Stripe price_id from webhook → (plan, role) tuple."""
if doc.raw_webhook_data:
try:
import json
data = json.loads(doc.raw_webhook_data) if isinstance(doc.raw_webhook_data, str) else doc.raw_webhook_data
# Try to extract price_id from various Stripe object shapes
obj = data.get("data", {}).get("object", {})
# Checkout session shape
for line in obj.get("line_items", {}).get("data", []) or []:
price_id = line.get("price", {}).get("id")
if price_id in PLAN_FROM_PRICE_ID:
return PLAN_FROM_PRICE_ID[price_id]
# Subscription shape
for item in obj.get("items", {}).get("data", []) or []:
price_id = item.get("price", {}).get("id")
if price_id in PLAN_FROM_PRICE_ID:
return PLAN_FROM_PRICE_ID[price_id]
# Invoice line items
for line in obj.get("lines", {}).get("data", []) or []:
price_id = line.get("price", {}).get("id")
if price_id in PLAN_FROM_PRICE_ID:
return PLAN_FROM_PRICE_ID[price_id]
except Exception as e:
frappe.log_error(f"Plan resolution failed: {e}", "Stripe provisioning")
return (None, None)
def _update_customer(email: str, plan: str, role: str, subscription_id: Optional[str]):
"""Update Customer doctype with plan + role."""
customers = frappe.get_all(
"Customer",
filters={"email_id": email},
fields=["name"],
limit=1,
)
if not customers:
return
try:
cust = frappe.get_doc("Customer", customers[0].name)
if hasattr(cust, "active_plan"):
cust.active_plan = plan
if hasattr(cust, "iam_role"):
cust.iam_role = role
if hasattr(cust, "plan_activation_date"):
cust.plan_activation_date = datetime.utcnow().date()
if subscription_id and hasattr(cust, "stripe_subscription_id"):
cust.stripe_subscription_id = subscription_id
cust.save(ignore_permissions=True)
frappe.db.commit()
except Exception as e:
frappe.log_error(f"Customer update failed for {email}: {e}", "Stripe provisioning")

View file

@ -0,0 +1,190 @@
"""
Subscription lifecycle scheduler daily check for expired subscriptions.
Triggered by Frappe scheduler (hooks.py):
scheduler_events = {
"daily": [
"didi_custom.integrations.subscription_lifecycle.expire_subscriptions",
]
}
Workflow:
1. Find all Customer records with `active_plan IN ('paid', 'enterprise')`
2. For each, check Stripe Subscription `current_period_end < now`
3. If expired AND no successful renewal:
- downgrade Customer.active_plan to 'free'
- update Keycloak attributes + roles
- send notification email (via existing "DiDi - Expirare Abonament" Notification)
"""
import frappe
from datetime import datetime
from .stripe_provisioning import on_subscription_canceled
def expire_subscriptions():
"""
Daily scheduler: detect expired subscriptions and downgrade users.
Idempotent safe to run multiple times.
"""
# Find candidates: customers with active paid plans
candidates = frappe.get_all(
"Customer",
filters={"active_plan": ["in", ["paid", "enterprise"]]},
fields=["name", "email_id", "stripe_subscription_id", "active_plan", "plan_activation_date"],
)
if not candidates:
return
now = datetime.utcnow().date()
downgraded_count = 0
for cust in candidates:
if not cust.email_id:
continue
# Check expiry — for now, use a simple lifetime check:
# If activation_date older than 30 days for monthly OR 365 days for yearly + no recent successful payment, downgrade.
# In production, query Stripe API directly with stripe_subscription_id for authoritative status.
if _should_downgrade(cust, now):
try:
_downgrade_customer(cust)
downgraded_count += 1
except Exception as e:
frappe.log_error(
f"Downgrade failed for {cust.email_id}: {e}",
"Subscription lifecycle",
)
if downgraded_count:
frappe.log(f"Subscription lifecycle: downgraded {downgraded_count} expired subscriptions")
def _should_downgrade(cust: dict, now) -> bool:
"""
Check if customer's subscription should be downgraded.
Implementation: check for recent successful Payment Log entries (last 35 days for monthly).
If none found AND plan_activation_date older than 35 days, downgrade.
Production-grade would call Stripe API: GET /v1/subscriptions/{id} to get current_period_end + status.
"""
if not cust.stripe_subscription_id:
return False # No Stripe sub linked — manually managed, skip
if not cust.plan_activation_date:
return False
days_since_activation = (now - cust.plan_activation_date).days
if days_since_activation < 35:
return False # Within first monthly cycle — never downgrade
# Check for recent successful payments
recent_success = frappe.get_all(
"Payment Log",
filters={
"stripe_subscription_id": cust.stripe_subscription_id,
"status": "succeeded",
"creation": [">", frappe.utils.add_days(frappe.utils.now(), -35)],
},
limit=1,
)
return not bool(recent_success)
def _downgrade_customer(cust: dict):
"""Apply downgrade — Customer + Keycloak + send notification."""
# Use stripe_provisioning's helper for consistency
fake_doc = frappe._dict({
"name": f"manual-downgrade-{cust.name}",
"raw_webhook_data": "",
"stripe_payment_intent_id": cust.stripe_subscription_id,
})
# Manual update since we don't have a webhook doc
customer = frappe.get_doc("Customer", cust.name)
customer.active_plan = "free"
if hasattr(customer, "iam_role"):
customer.iam_role = "free_tier"
customer.save(ignore_permissions=True)
frappe.db.commit()
# Update Keycloak attributes + roles
from .keycloak import update_user_attributes
update_user_attributes(
email=cust.email_id,
attributes={
"active_plan": "free",
"plan_activation_date": datetime.utcnow().isoformat() + "Z",
},
roles_add=["free_tier"],
roles_remove=["paid_tier", "enterprise_tier"],
)
# Send notification (uses existing "DiDi - Expirare Abonament" Notification)
try:
frappe.sendmail(
recipients=[cust.email_id],
subject="Abonament DiDi expirat",
message=(
f"<p>Buna ziua,</p>"
f"<p>Abonamentul dumneavoastra DiDi ({cust.active_plan}) a expirat. "
f"Contul a fost retrogradat la planul Free.</p>"
f"<p>Pentru reactivare, va rugam vizitati <a href='https://didi365.eu/pricing'>didi365.eu/pricing</a>.</p>"
f"<p>Echipa DiDi / Clossers</p>"
),
now=True,
)
except Exception as e:
frappe.log_error(f"Downgrade email failed for {cust.email_id}: {e}", "Subscription lifecycle")
def send_expiry_reminders(days_before: int = 7, cycle_days: int = 30):
"""
Daily scheduler (ref. oferta B.1.2): remind customers whose subscription
expires in `days_before` days. Uses the same date heuristic as
expire_subscriptions (activation + cycle_days) until Stripe is authoritative.
Idempotent per day: at most one reminder, on the single matching day.
"""
candidates = frappe.get_all(
"Customer",
filters={"active_plan": ["in", ["paid", "enterprise"]]},
fields=["name", "email_id", "active_plan", "plan_activation_date"],
)
today = datetime.utcnow().date()
sent = 0
for cust in candidates:
if not cust.email_id or not cust.plan_activation_date:
continue
days_left = cycle_days - (today - cust.plan_activation_date).days
if days_left != days_before:
continue
try:
frappe.sendmail(
recipients=[cust.email_id],
subject=f"Abonamentul DiDi expiră în {days_left} zile",
message=(
f"<p>Bună ziua,</p>"
f"<p>Abonamentul dumneavoastră DiDi ({cust.active_plan}) expiră în "
f"<strong>{days_left} zile</strong>.</p>"
f"<p>Pentru a evita întreruperea serviciilor, verificați metoda de plată "
f"sau reînnoiți din <a href='https://didi365.eu/pricing'>pagina de planuri</a>.</p>"
f"<p>Echipa DiDi / Clossers</p>"
),
now=True,
)
sent += 1
except Exception as e:
frappe.log_error(f"Expiry reminder failed for {cust.email_id}: {e}", "Subscription lifecycle")
print(f"expiry reminders sent: {sent}")
return sent
def hourly_sync_stripe():
"""
Hourly scheduler: sync Stripe Subscription status to local Customer record.
Lighter version of expire_subscriptions only flags inconsistencies, doesn't downgrade.
Useful for keeping ERP in sync with Stripe between webhook events.
"""
# Implementation TODO: query Stripe API for all subscriptions, update Customer state
pass

View file

@ -0,0 +1,37 @@
import frappe
def execute():
if frappe.db.exists("Company", "TOP CLOSSERS SRL"):
print("Already exists")
return
# Patch ALL the country-specific setup that crashes
from erpnext.setup.doctype.company import company as cm
cm.install_country_fixtures = lambda *a, **kw: None
# Skip default tax template creation too (we will create our own RO TVA 19% template)
original_method = cm.Company.create_default_tax_template
cm.Company.create_default_tax_template = lambda self: None
try:
doc = frappe.get_doc({
"doctype": "Company",
"company_name": "TOP CLOSSERS SRL",
"abbr": "TC",
"default_currency": "RON",
"country": "Romania",
"chart_of_accounts": "Romania - Chart of Accounts",
"create_chart_of_accounts_based_on": "Standard Template",
})
doc.flags.ignore_permissions = True
doc.insert()
frappe.db.commit()
print("Created:", doc.name)
finally:
cm.Company.create_default_tax_template = original_method
count = frappe.db.sql("SELECT COUNT(*) FROM tabAccount WHERE company=%s", ("TOP CLOSSERS SRL",))[0][0]
print(f"Accounts: {count}")
# Set defaults
frappe.db.set_single_value("Global Defaults", "default_company", "TOP CLOSSERS SRL")
frappe.db.set_single_value("Global Defaults", "default_currency", "RON")
frappe.db.commit()
print("Defaults set")

View file

@ -0,0 +1,42 @@
import frappe
ITEMS = [
("DIDI-TECHNIQUES-TEXT", "DIDI Techniques - Text", 50),
("DIDI-TECHNIQUES-IMAGE", "DIDI Techniques - Image", 75),
("DIDI-TECHNIQUES-AUDIO", "DIDI Techniques - Audio", 100),
("DIDI-TECHNIQUES-VIDEO", "DIDI Techniques - Video", 200),
("DIDI-AI_DETECTION-TEXT", "DIDI AI Detection - Text", 50),
("DIDI-AI_DETECTION-IMAGE", "DIDI AI Detection - Image", 75),
("DIDI-AI_DETECTION-AUDIO", "DIDI AI Detection - Audio", 100),
("DIDI-AI_DETECTION-VIDEO", "DIDI AI Detection - Video", 200),
("DIDI-CLAIMS-TEXT", "DIDI Claims - Text", 50),
("DIDI-CLAIMS-IMAGE", "DIDI Claims - Image", 75),
("DIDI-CLAIMS-AUDIO", "DIDI Claims - Audio", 75),
("DIDI-CLAIMS-VIDEO", "DIDI Claims - Video", 150),
("DIDI-SOURCE-TEXT", "DIDI Source Assessment - Text", 50),
("DIDI-SOURCE-URL", "DIDI Source Assessment - URL", 50),
]
def execute():
created = 0
for code, name, price in ITEMS:
if frappe.db.exists("Item", code):
print(f" {code}: exists")
continue
doc = frappe.get_doc({
"doctype": "Item",
"item_code": code,
"item_name": name,
"item_group": "Services",
"stock_uom": "Nos",
"is_stock_item": 0,
"is_service_item": 1,
"description": name,
"standard_rate": price,
})
doc.flags.ignore_permissions = True
doc.insert()
created += 1
print(f" {code}: created (RON {price})")
frappe.db.commit()
print(f"Created {created} items")

View file

@ -0,0 +1 @@
Didi Custom

View file

@ -0,0 +1,70 @@
"""
Emailuri tranzactionale DiDi (ref. oferta B.1.2/B.1.3): confirmare plata cu
factura PDF atasata, trimisa clientului la finalizarea fulfillment-ului.
Apelata de website dupa plata confirmata:
POST /api/method/didi_custom.notifications.send_invoice_email
body: {"invoice_name": "DIDI-INV-...."}
SMTP-ul vine din Email Account "DiDi Outgoing" (Mailpit in test, SendGrid in
productie vezi setup_email.py).
"""
import frappe
@frappe.whitelist()
def send_payment_failed_email(email: str, amount=None, currency: str = "RON", reactivation_url: str = ""):
"""Notificare eșec plată cu link de reactivare (ref. oferta B.1.2)."""
if not email:
return {"sent": False, "reason": "missing email"}
link = reactivation_url or "https://didi365.eu/pricing"
amount_line = (
f"<p>Suma: <strong>{float(amount):.2f} {currency}</strong>.</p>" if amount else ""
)
frappe.sendmail(
recipients=[email],
subject="DiDi — Plata nu a putut fi procesată",
message=(
f"<p>Bună ziua,</p>"
f"<p>Încercarea de plată pentru abonamentul DiDi a eșuat.</p>"
f"{amount_line}"
f"<p>Vă rugăm să verificați metoda de plată și să "
f"<a href='{link}'>reactivați abonamentul</a>. Vom reîncerca automat plata "
f"în următoarele zile.</p>"
f"<p>Echipa DiDi</p>"
),
delayed=False,
)
frappe.db.commit()
return {"sent": True, "to": email}
@frappe.whitelist()
def send_invoice_email(invoice_name: str):
doc = frappe.get_doc("Sales Invoice", invoice_name)
customer_email = frappe.db.get_value("Customer", doc.customer, "email_id")
if not customer_email:
return {"sent": False, "reason": f"customer {doc.customer} has no email_id"}
pdf = frappe.attach_print(
"Sales Invoice", invoice_name, print_format="DiDi Invoice",
file_name=f"{invoice_name}.pdf",
)
frappe.sendmail(
recipients=[customer_email],
subject=f"DiDi — Confirmare plată și factura {invoice_name}",
message=(
f"<p>Bună ziua, {doc.customer_name},</p>"
f"<p>Vă mulțumim pentru plată. Găsiți atașată factura "
f"<strong>{invoice_name}</strong> în valoare de "
f"<strong>{doc.grand_total:.2f} {doc.currency}</strong>.</p>"
f"<p>Serviciul achiziționat este activ în contul dumneavoastră DiDi.</p>"
f"<p>Echipa DiDi</p>"
),
attachments=[pdf],
reference_doctype="Sales Invoice",
reference_name=invoice_name,
delayed=False,
)
frappe.db.commit()
return {"sent": True, "to": customer_email, "invoice": invoice_name}

View file

@ -0,0 +1,6 @@
[pre_model_sync]
# Patches added in this section will be executed before doctypes are migrated
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
[post_model_sync]
# Patches added in this section will be executed after doctypes are migrated

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,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,46 @@
"""
Outgoing email pentru DiDi cont SMTP implicit (Mailpit in test, SendGrid in
productie: schimbi smtp_server/port/credentiale, restul ramane).
Rulare:
docker exec didi-erpnext bench --site didi-erp execute didi_custom.setup_email.execute
Test trimitere:
docker exec didi-erpnext bench --site didi-erp execute didi_custom.setup_email.send_test
"""
import frappe
ACCOUNT_NAME = "DiDi Outgoing"
def execute():
if frappe.db.exists("Email Account", ACCOUNT_NAME):
acc = frappe.get_doc("Email Account", ACCOUNT_NAME)
else:
acc = frappe.new_doc("Email Account")
acc.email_account_name = ACCOUNT_NAME
acc.update({
"email_id": "noreply@didi.clossers.com",
"enable_outgoing": 1,
"default_outgoing": 1,
"smtp_server": frappe.conf.get("didi_smtp_server") or "didi-mailpit",
"smtp_port": int(frappe.conf.get("didi_smtp_port") or 1025),
"use_tls": 0,
"use_ssl": 0,
"no_smtp_authentication": 1,
"enable_incoming": 0,
})
acc.flags.ignore_permissions = True
acc.save()
frappe.db.commit()
print(f"OK: Email Account '{acc.name}' -> {acc.smtp_server}:{acc.smtp_port} (default outgoing)")
def send_test():
frappe.sendmail(
recipients=["test@didi.local"],
subject="Test SMTP DiDi",
message="<p>Email de proba prin Mailpit.</p>",
delayed=False,
)
frappe.db.commit()
print("OK: email de test trimis")

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

View file

@ -0,0 +1,109 @@
"""Construieste planul de conturi RO minimal + conturi TVA/venituri + default-uri companie
+ template TVA 21%, pentru ca facturarea din website sa functioneze.
Rulare: bench --site didi-erp console -> import didi_custom.setup_ro_coa as c; c.execute()
"""
import json
import frappe
COMPANY = "TOP CLOSSERS SRL"
def execute():
company = COMPANY
if frappe.db.sql("SELECT count(*) FROM tabAccount WHERE company=%s", (company,))[0][0] == 0:
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
with open('/home/frappe/frappe-bench/apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/unverified/ro_ro_chart_template.json') as f:
chart_data = json.load(f)
tree = chart_data["tree"]
fin = tree.get("CONTURI FINANCIARE", {})
bilant = fin.get("CONTURI DE BILANT", {})
venituri = fin.get("CONTURI DE VENITURI SI CHELTUIELI", {})
terti = bilant.get("CONTURI DE TERTI", {})
new_tree = {
"ACTIVE": {
"root_type": "Asset", "is_group": 1,
"Imobilizari": {**bilant.get("CONTURI DE IMOBILIZARI", {}), "root_type": "Asset", "is_group": 1},
"Stocuri": {**bilant.get("CONTURI DE STOCURI SI PRODUCTIE IN CURS DE EXECUTIE", {}), "root_type": "Asset", "is_group": 1},
"Trezorerie": {**bilant.get("CONTURI DE TREZORERIE", {}), "root_type": "Asset", "is_group": 1},
"Clienti si conturi asimilate": {"root_type": "Asset", "is_group": 1, **terti.get("CLIENTI SI CONTURI ASIMILATE", {})},
"Debitori diversi": {"root_type": "Asset", "is_group": 1, **terti.get("DEBITORI SI CREDITORI DIVERSI", {})},
},
"PASIVE (DATORII)": {
"root_type": "Liability", "is_group": 1,
"Furnizori si conturi asimilate": {"root_type": "Liability", "is_group": 1, **terti.get("FURNIZORI SI CONTURI ASIMILATE", {})},
"Personal si conturi asimilate": {"root_type": "Liability", "is_group": 1, **terti.get("PERSONAL SI CONTURI ASIMILATE", {})},
"Asigurari sociale, protectia sociala": {"root_type": "Liability", "is_group": 1, **terti.get("ASIGURARI SOCIALE, PROTECTIA SOCIALA SI CONTURI ASIMILATE", {})},
"Bugetul statului (TVA, impozite)": {"root_type": "Liability", "is_group": 1, **terti.get("BUGETUL STATULUI, FONDURI SPECIALE SI CONTURI ASIMILATE", {})},
},
"CAPITALURI PROPRII": {"root_type": "Equity", "is_group": 1, **bilant.get("CONTURI DE CAPITALURI", {})},
"VENITURI": {"root_type": "Income", "is_group": 1, **venituri.get("CONTURI DE VENITURI", {})},
"CHELTUIELI": {"root_type": "Expense", "is_group": 1, **venituri.get("CONTURI DE CHELTUIELI", {})},
}
create_charts(company, custom_chart=new_tree)
frappe.db.commit()
def find_parent(like):
r = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND name LIKE %s AND is_group=1 LIMIT 1", (company, like))
return r[0][0] if r else None
bugetul = find_parent("%Bugetul statului%TC")
venituri_root = find_parent("VENITURI - TC")
accounts = [
("4426 - TVA deductibila", bugetul, "Asset", "Tax", "4426"),
("4427 - TVA colectata", bugetul, "Liability", "Tax", "4427"),
("4423 - TVA de plata", bugetul, "Liability", "Tax", "4423"),
("4424 - TVA de recuperat", bugetul, "Asset", "Tax", "4424"),
("704 - Venituri din servicii prestate", venituri_root, "Income", "Income Account", "704"),
("707 - Venituri din vanzarea marfurilor", venituri_root, "Income", "Income Account", "707"),
]
for name, parent, rt, at, num in accounts:
if not parent:
continue
if frappe.db.exists("Account", {"company": company, "account_number": num}):
continue
try:
frappe.get_doc({"doctype": "Account", "company": company, "is_group": 0,
"account_name": name, "parent_account": parent,
"root_type": rt, "account_type": at, "account_number": num}).insert(ignore_permissions=True)
except Exception as e:
print(" skip", name, str(e)[:80])
frappe.db.commit()
def find_acc(like):
r = frappe.db.sql("SELECT name FROM tabAccount WHERE company=%s AND name LIKE %s AND is_group=0 LIMIT 1", (company, like))
return r[0][0] if r else None
co = frappe.get_doc("Company", company)
co.default_receivable_account = find_acc("%Clienti%TC")
co.default_income_account = find_acc("%704%TC")
co.default_cash_account = find_acc("%Casa in lei%TC") or find_acc("%Casa%TC")
co.default_bank_account = find_acc("%Conturi la banci in lei%TC") or find_acc("%banci%TC")
if not frappe.db.exists("Account", {"company": company, "account_name": "Round Off"}):
chelt_parent = find_parent("CHELTUIELI - TC")
if chelt_parent:
frappe.get_doc({"doctype": "Account", "company": company, "is_group": 0,
"account_name": "Round Off", "parent_account": chelt_parent,
"root_type": "Expense", "account_type": "Round Off"}).insert(ignore_permissions=True)
co.round_off_account = find_acc("Round Off%TC")
cc = frappe.db.sql("SELECT name FROM `tabCost Center` WHERE company=%s AND is_group=0 LIMIT 1", (company,))
if cc:
co.cost_center = cc[0][0]
co.round_off_cost_center = cc[0][0]
co.save(ignore_permissions=True)
frappe.db.commit()
for t in frappe.get_all("Sales Taxes and Charges Template", filters={"company": company}):
frappe.delete_doc("Sales Taxes and Charges Template", t.name, ignore_permissions=True, force=True)
tva_acc = frappe.db.get_value("Account", {"company": company, "account_number": "4427"}, "name")
if tva_acc:
frappe.get_doc({"doctype": "Sales Taxes and Charges Template", "title": "TVA 21%",
"company": company, "is_default": 1,
"taxes": [{"charge_type": "On Net Total", "account_head": tva_acc, "description": "TVA 21%", "rate": 21}]}).insert(ignore_permissions=True)
frappe.db.set_single_value("Website Settings", "host_name", "http://didi-erpnext:8080")
frappe.db.commit()
print("RESULT_ACCOUNTS", frappe.db.count("Account", {"company": company}))
print("RESULT_TVA_ACC", tva_acc)
print("RESULT_RECEIVABLE", co.default_receivable_account)
print("RESULT_INCOME", co.default_income_account)
print("RESULT_COSTCENTER", co.cost_center)

View file

@ -0,0 +1,56 @@
"""
Scurtaturi 1-click in workspace-ul DiDi: rapoartele si listele folosite zilnic,
ca sa nu mai fie nevoie de cautare sau URL-uri manuale.
Rulare:
docker exec didi-erpnext bench --site didi-erp execute didi_custom.setup_shortcuts.execute
"""
import frappe
SHORTCUTS = [
# (label, type, link_to)
("Facturi de vânzare", "DocType", "Sales Invoice"),
("Încasări", "DocType", "Payment Entry"),
("Clienți", "DocType", "Customer"),
("Lead-uri (CRM)", "DocType", "Lead"),
("Acorduri de servicii", "DocType", "Service Agreement"),
("Jurnal plăți Stripe", "DocType", "Payment Log"),
("Conținut website (CMS)", "DocType", "Website Content"),
("Email SMTP (DiDi Outgoing)", "DocType", "Email Account"),
("Sales Register (centralizator vânzări)", "Report", "Sales Register"),
("General Ledger (jurnal contabil)", "Report", "General Ledger"),
("Trial Balance (balanța)", "Report", "Trial Balance"),
("Accounts Receivable (creanțe)", "Report", "Accounts Receivable"),
]
def execute():
name = "DiDi Platform" if frappe.db.exists("Workspace", "DiDi Platform") else "DiDi"
if not frappe.db.exists("Workspace", name):
print("Nu exista workspace DiDi — abandonez")
return
w = frappe.get_doc("Workspace", name)
existing = {s.link_to for s in w.shortcuts}
added = 0
for label, typ, link in SHORTCUTS:
if link in existing:
continue
row = {"label": label, "type": typ, "link_to": link}
if typ == "Report":
row["report_ref_doctype"] = frappe.db.get_value("Report", link, "ref_doctype")
w.append("shortcuts", row)
added += 1
# workspace-ul afiseaza doar blocurile din campul content — adaugam si acolo
import json, random, string
content = json.loads(w.content or "[]")
shown = {b.get("data", {}).get("shortcut_name") for b in content if b.get("type") == "shortcut"}
rid = lambda: "".join(random.choices(string.ascii_letters + string.digits, k=10))
for s_row in w.shortcuts:
if s_row.label not in shown:
content.append({"id": rid(), "type": "shortcut", "data": {"shortcut_name": s_row.label, "col": 4}})
w.content = json.dumps(content)
w.flags.ignore_permissions = True
w.save()
frappe.db.commit()
frappe.clear_cache()
print(f"OK: workspace '{name}'{added} scurtaturi adaugate (total {len(w.shortcuts)})")

View file

@ -0,0 +1,106 @@
"""One-shot setup pentru integrarea website: item groups/UOM, itemele DIDI, user API.
Rulare: bench --site didi-erp execute didi_custom.setup_website_integration.execute
"""
import frappe
from didi_custom.make_didi_items import ITEMS
def _ensure_prereqs():
# UOM Nos
if not frappe.db.exists("UOM", "Nos"):
frappe.get_doc({"doctype": "UOM", "uom_name": "Nos"}).insert(ignore_permissions=True)
# Item Group "Services" sub root "All Item Groups"
if not frappe.db.exists("Item Group", "Services"):
parent = frappe.db.get_value("Item Group", {"is_group": 1, "parent_item_group": ""}, "name") \
or frappe.db.get_value("Item Group", {"name": "All Item Groups"}, "name")
frappe.get_doc({
"doctype": "Item Group", "item_group_name": "Services",
"parent_item_group": parent, "is_group": 0,
}).insert(ignore_permissions=True)
def _ensure_items():
for code, name, price in ITEMS:
if frappe.db.exists("Item", code):
continue
frappe.get_doc({
"doctype": "Item", "item_code": code, "item_name": name,
"item_group": "Services", "stock_uom": "Nos",
"is_stock_item": 0, "is_sales_item": 1, "include_item_in_manufacturing": 0,
"standard_rate": price,
}).insert(ignore_permissions=True)
def _ensure_role():
if not frappe.db.exists("Role", "Website Integration"):
frappe.get_doc({
"doctype": "Role", "role_name": "Website Integration",
"desk_access": 0, "is_custom": 1,
}).insert(ignore_permissions=True)
rw = ["Customer", "Lead", "Sales Invoice", "Payment Entry", "Subscription",
"Subscription Plan", "Website Content", "Service Agreement",
"Analysis Report", "Payment Log", "Item", "Address"]
for dt in rw:
if not frappe.db.exists("DocType", dt):
continue
vals = {"read": 1, "write": 1, "create": 1, "delete": 0, "print": 1,
"export": 1, "submit": 1 if dt in ("Sales Invoice", "Payment Entry") else 0}
existing = frappe.db.exists("Custom DocPerm", {"parent": dt, "role": "Website Integration"})
if existing:
frappe.db.set_value("Custom DocPerm", existing, vals, update_modified=False)
else:
frappe.get_doc({"doctype": "Custom DocPerm", "parent": dt, "parenttype": "DocType",
"parentfield": "permissions", "role": "Website Integration", **vals}).insert(ignore_permissions=True)
frappe.db.commit()
def _ensure_api_user():
email = "website_api@didi-erp.local"
if not frappe.db.exists("User", email):
u = frappe.get_doc({
"doctype": "User", "email": 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,
})
u.insert(ignore_permissions=True)
else:
u = frappe.get_doc("User", email)
if not any(r.role == "Website Integration" for r in u.roles):
u.append("roles", {"role": "Website Integration"})
u.save(ignore_permissions=True)
secret = u.get_password("api_secret", raise_exception=False)
if not u.api_key or not secret:
u.api_key = frappe.generate_hash(length=15)
secret = frappe.generate_hash(length=15)
u.api_secret = secret
u.save(ignore_permissions=True)
return email, u.api_key, secret
def execute():
_ensure_prereqs()
_ensure_items()
_ensure_role()
email, key, secret = _ensure_api_user()
frappe.db.commit()
print("RESULT_ITEMS", frappe.db.count("Item", {"item_code": ["like", "DIDI-%"]}))
print("RESULT_APIUSER", email)
print("RESULT_APIKEY", key)
print("RESULT_APISECRET", secret)
def fix_print_format():
import frappe
pf = frappe.get_doc("Print Format", "DiDi Invoice")
bad = 'frappe.db.get_value("Company", doc.company, "address")'
if bad in (pf.html or ""):
pf.html = pf.html.replace(bad, '"Str. Targovistei 15, Ploiesti, jud. Prahova"')
pf.save(ignore_permissions=True)
frappe.db.commit()
print("PF_PATCHED")
try:
pdf = frappe.get_print("Sales Invoice", "DIDI-INV-2026-00001", print_format="DiDi Invoice", as_pdf=True)
print("PF_PDF_OK", len(pdf))
except Exception as e:
print("PF_PDF_ERR", repr(e)[:160])

View file

@ -0,0 +1,54 @@
"""
Sidebar ERP curat pentru proiectul DiDi: raman vizibile doar workspace-urile
relevante (contabilitate, facturare, CRM, clienti, CMS, DiDi); restul modulelor
standard ERPNext (Manufacturing, Stock, Quality etc.) se ascund (public=0).
Idempotent safe de rerulat; workspace-urile noi necunoscute NU sunt atinse.
Rulare:
docker exec didi-erpnext bench --site didi-erp execute didi_custom.setup_workspaces.execute
Revenire (arata tot):
docker exec didi-erpnext bench --site didi-erp execute didi_custom.setup_workspaces.show_all
"""
import frappe
KEEP = {
"Home",
# hub-uri DiDi custom
"DiDi", "DiDi Platform",
# contabilitate & facturare
"Accounting", "Receivables", "Payables", "Financial Reports",
"Contabilitate", "Facturi", "Plati",
# vanzari & clienti & CRM
"Selling", "CRM", "CRM-DiDi", "Clienti", "Servicii",
# continut website
"Website CMS",
}
HIDE = {
"Assets", "Build", "Buying", "ERPNext Integrations", "ERPNext Settings",
"Integrations", "Manufacturing", "Projects", "Quality", "Stock",
"Support", "Tools", "Users", "Welcome Workspace",
"Website", # modulul standard ERPNext (portal) — CMS-ul custom ramane
"Website-CMS", # duplicat vechi al "Website CMS"
}
def execute():
for name in HIDE:
if frappe.db.exists("Workspace", name):
frappe.db.set_value("Workspace", name, "public", 0)
print(f" ascuns: {name}")
for name in KEEP:
if frappe.db.exists("Workspace", name):
frappe.db.set_value("Workspace", name, "public", 1)
frappe.db.commit()
frappe.clear_cache()
print("OK: sidebar curatat (raman doar workspace-urile proiectului)")
def show_all():
for w in frappe.get_all("Workspace"):
frappe.db.set_value("Workspace", w.name, "public", 1)
frappe.db.commit()
frappe.clear_cache()
print("OK: toate workspace-urile vizibile din nou")

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,56 @@
[project]
name = "didi_custom"
authors = [
{ name = "TOP CLOSSERS SRL", email = "office@clossers.com"}
]
description = "Custom DocTypes for DiDi Platform"
requires-python = ">=3.10"
readme = "README.md"
dynamic = ["version"]
dependencies = [
# "frappe~=15.0.0" # Installed and managed by bench.
]
[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
# These dependencies are only installed when developer mode is enabled
[tool.bench.dev-dependencies]
# package_name = "~=1.1.0"
[tool.ruff]
line-length = 110
target-version = "py310"
[tool.ruff.lint]
select = [
"F",
"E",
"W",
"I",
"UP",
"B",
"RUF",
]
ignore = [
"B017", # assertRaises(Exception) - should be more specific
"B018", # useless expression, not assigned to anything
"B023", # function doesn't bind loop variable - will have last iteration's value
"B904", # raise inside except without from
"E101", # indentation contains mixed spaces and tabs
"E402", # module level import not at top of file
"E501", # line too long
"E741", # ambiguous variable name
"F401", # "unused" imports
"F403", # can't detect undefined names from * import
"F405", # can't detect undefined names from * import
"F722", # syntax error in forward type annotation
"W191", # indentation contains tabs
]
typing-modules = ["frappe.types.DF"]
[tool.ruff.format]
quote-style = "double"
indent-style = "tab"
docstring-code-format = true

Some files were not shown because too many files have changed in this diff Show more