livrare website cu erp si crm
This commit is contained in:
parent
28773e3a72
commit
5c7bf7c295
257 changed files with 31929 additions and 0 deletions
21
erp_crm/didi_custom/.editorconfig
Normal file
21
erp_crm/didi_custom/.editorconfig
Normal 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
|
||||
124
erp_crm/didi_custom/.eslintrc
Normal file
124
erp_crm/didi_custom/.eslintrc
Normal 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
|
||||
}
|
||||
}
|
||||
69
erp_crm/didi_custom/.pre-commit-config.yaml
Normal file
69
erp_crm/didi_custom/.pre-commit-config.yaml
Normal 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
|
||||
33
erp_crm/didi_custom/README.md
Normal file
33
erp_crm/didi_custom/README.md
Normal 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
|
||||
1
erp_crm/didi_custom/didi_custom/__init__.py
Normal file
1
erp_crm/didi_custom/didi_custom/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.0.1"
|
||||
4
erp_crm/didi_custom/didi_custom/arhiva_oneoff/README.md
Normal file
4
erp_crm/didi_custom/didi_custom/arhiva_oneoff/README.md
Normal 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/.
|
||||
|
|
@ -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")
|
||||
51
erp_crm/didi_custom/didi_custom/arhiva_oneoff/add_ro.py
Normal file
51
erp_crm/didi_custom/didi_custom/arhiva_oneoff/add_ro.py
Normal 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])
|
||||
|
|
@ -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]}")
|
||||
|
|
@ -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])
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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])
|
||||
|
|
@ -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)
|
||||
59
erp_crm/didi_custom/didi_custom/arhiva_oneoff/final_setup.py
Normal file
59
erp_crm/didi_custom/didi_custom/arhiva_oneoff/final_setup.py
Normal 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}")
|
||||
|
|
@ -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)
|
||||
20
erp_crm/didi_custom/didi_custom/arhiva_oneoff/fix_consume.py
Normal file
20
erp_crm/didi_custom/didi_custom/arhiva_oneoff/fix_consume.py
Normal 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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
55
erp_crm/didi_custom/didi_custom/arhiva_oneoff/fix_round.py
Normal file
55
erp_crm/didi_custom/didi_custom/arhiva_oneoff/fix_round.py
Normal 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}")
|
||||
26
erp_crm/didi_custom/didi_custom/arhiva_oneoff/fix_tva.py
Normal file
26
erp_crm/didi_custom/didi_custom/arhiva_oneoff/fix_tva.py
Normal 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%")
|
||||
|
|
@ -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")
|
||||
26
erp_crm/didi_custom/didi_custom/arhiva_oneoff/fix_tva_v2.py
Normal file
26
erp_crm/didi_custom/didi_custom/arhiva_oneoff/fix_tva_v2.py
Normal 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")
|
||||
|
|
@ -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])
|
||||
16
erp_crm/didi_custom/didi_custom/arhiva_oneoff/inspect_cf.py
Normal file
16
erp_crm/didi_custom/didi_custom/arhiva_oneoff/inspect_cf.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
21
erp_crm/didi_custom/didi_custom/arhiva_oneoff/install_coa.py
Normal file
21
erp_crm/didi_custom/didi_custom/arhiva_oneoff/install_coa.py
Normal 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])
|
||||
|
|
@ -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:])
|
||||
|
|
@ -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}")
|
||||
13
erp_crm/didi_custom/didi_custom/arhiva_oneoff/list_cf.py
Normal file
13
erp_crm/didi_custom/didi_custom/arhiva_oneoff/list_cf.py
Normal 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])
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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])
|
||||
|
|
@ -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])
|
||||
25
erp_crm/didi_custom/didi_custom/arhiva_oneoff/run_wizard.py
Normal file
25
erp_crm/didi_custom/didi_custom/arhiva_oneoff/run_wizard.py
Normal 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"))
|
||||
126
erp_crm/didi_custom/didi_custom/arhiva_oneoff/setup_cleanup.py
Normal file
126
erp_crm/didi_custom/didi_custom/arhiva_oneoff/setup_cleanup.py
Normal 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)")
|
||||
287
erp_crm/didi_custom/didi_custom/arhiva_oneoff/setup_config.py
Normal file
287
erp_crm/didi_custom/didi_custom/arhiva_oneoff/setup_config.py
Normal 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.")
|
||||
0
erp_crm/didi_custom/didi_custom/config/__init__.py
Normal file
0
erp_crm/didi_custom/didi_custom/config/__init__.py
Normal file
0
erp_crm/didi_custom/didi_custom/didi_custom/__init__.py
Normal file
0
erp_crm/didi_custom/didi_custom/didi_custom/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -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", {
|
||||
// });
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Copyright (c) 2026, TOP CLOSSERS SRL and Contributors
|
||||
# See license.txt
|
||||
|
||||
|
||||
def test_placeholder():
|
||||
assert True
|
||||
|
||||
|
|
@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
33
erp_crm/didi_custom/didi_custom/fix_sa.py
Normal file
33
erp_crm/didi_custom/didi_custom/fix_sa.py
Normal 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")
|
||||
292
erp_crm/didi_custom/didi_custom/hooks.py
Normal file
292
erp_crm/didi_custom/didi_custom/hooks.py
Normal 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 = []
|
||||
|
||||
7
erp_crm/didi_custom/didi_custom/inspect_cms_changes.py
Normal file
7
erp_crm/didi_custom/didi_custom/inspect_cms_changes.py
Normal 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))
|
||||
0
erp_crm/didi_custom/didi_custom/integrations/__init__.py
Normal file
0
erp_crm/didi_custom/didi_custom/integrations/__init__.py
Normal file
185
erp_crm/didi_custom/didi_custom/integrations/keycloak.py
Normal file
185
erp_crm/didi_custom/didi_custom/integrations/keycloak.py
Normal 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")
|
||||
|
|
@ -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.")
|
||||
106
erp_crm/didi_custom/didi_custom/integrations/sales_channel.py
Normal file
106
erp_crm/didi_custom/didi_custom/integrations/sales_channel.py
Normal 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,
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
37
erp_crm/didi_custom/didi_custom/make_company_ro3.py
Normal file
37
erp_crm/didi_custom/didi_custom/make_company_ro3.py
Normal 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")
|
||||
42
erp_crm/didi_custom/didi_custom/make_didi_items.py
Normal file
42
erp_crm/didi_custom/didi_custom/make_didi_items.py
Normal 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")
|
||||
1
erp_crm/didi_custom/didi_custom/modules.txt
Normal file
1
erp_crm/didi_custom/didi_custom/modules.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Didi Custom
|
||||
70
erp_crm/didi_custom/didi_custom/notifications.py
Normal file
70
erp_crm/didi_custom/didi_custom/notifications.py
Normal 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}
|
||||
6
erp_crm/didi_custom/didi_custom/patches.txt
Normal file
6
erp_crm/didi_custom/didi_custom/patches.txt
Normal 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
|
||||
0
erp_crm/didi_custom/didi_custom/patches/__init__.py
Normal file
0
erp_crm/didi_custom/didi_custom/patches/__init__.py
Normal file
397
erp_crm/didi_custom/didi_custom/populate_content.py
Normal file
397
erp_crm/didi_custom/didi_custom/populate_content.py
Normal 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)
|
||||
0
erp_crm/didi_custom/didi_custom/public/.gitkeep
Normal file
0
erp_crm/didi_custom/didi_custom/public/.gitkeep
Normal file
262
erp_crm/didi_custom/didi_custom/setup_doctypes.py
Normal file
262
erp_crm/didi_custom/didi_custom/setup_doctypes.py
Normal 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.")
|
||||
46
erp_crm/didi_custom/didi_custom/setup_email.py
Normal file
46
erp_crm/didi_custom/didi_custom/setup_email.py
Normal 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")
|
||||
288
erp_crm/didi_custom/didi_custom/setup_phase2_final.py
Normal file
288
erp_crm/didi_custom/didi_custom/setup_phase2_final.py
Normal 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.")
|
||||
109
erp_crm/didi_custom/didi_custom/setup_ro_coa.py
Normal file
109
erp_crm/didi_custom/didi_custom/setup_ro_coa.py
Normal 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)
|
||||
56
erp_crm/didi_custom/didi_custom/setup_shortcuts.py
Normal file
56
erp_crm/didi_custom/didi_custom/setup_shortcuts.py
Normal 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)})")
|
||||
106
erp_crm/didi_custom/didi_custom/setup_website_integration.py
Normal file
106
erp_crm/didi_custom/didi_custom/setup_website_integration.py
Normal 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])
|
||||
54
erp_crm/didi_custom/didi_custom/setup_workspaces.py
Normal file
54
erp_crm/didi_custom/didi_custom/setup_workspaces.py
Normal 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")
|
||||
0
erp_crm/didi_custom/didi_custom/templates/__init__.py
Normal file
0
erp_crm/didi_custom/didi_custom/templates/__init__.py
Normal file
21
erp_crm/didi_custom/license.txt
Normal file
21
erp_crm/didi_custom/license.txt
Normal 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.
|
||||
56
erp_crm/didi_custom/pyproject.toml
Normal file
56
erp_crm/didi_custom/pyproject.toml
Normal 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
Loading…
Add table
Add a link
Reference in a new issue