livrare website cu erp si crm
This commit is contained in:
parent
28773e3a72
commit
5c7bf7c295
257 changed files with 31929 additions and 0 deletions
27
erp_crm/.env.example
Normal file
27
erp_crm/.env.example
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# ERPNext Docker Configuration — TEMPLATE
|
||||
# =======================================
|
||||
# Copy to .env and fill in real values.
|
||||
# NEVER commit .env — it is gitignored.
|
||||
|
||||
# MariaDB — generate strong random passwords (e.g. openssl rand -base64 24)
|
||||
MYSQL_ROOT_PASSWORD=<schimba-ma>
|
||||
DB_PASSWORD=<schimba-ma>
|
||||
|
||||
# ERPNext
|
||||
ERPNEXT_VERSION=v15
|
||||
FRAPPE_VERSION=v15
|
||||
SITE_NAME=didi-erp
|
||||
ADMIN_PASSWORD=<schimba-ma>
|
||||
|
||||
# Redis (internal Docker DNS — don't change unless changing compose)
|
||||
REDIS_CACHE=redis-cache:6379
|
||||
REDIS_QUEUE=redis-queue:6379
|
||||
REDIS_SOCKETIO=redis-socketio:6379
|
||||
|
||||
# Network
|
||||
ERPNEXT_PORT=8080
|
||||
|
||||
# Setup scripts (08-email.py etc) — required when running setup
|
||||
SENDGRID_API_KEY=<your-sendgrid-key-starts-with-SG>
|
||||
ERPNEXT_ADMIN_PASSWORD=<schimba-ma>
|
||||
ERPNEXT_URL=http://localhost:8080
|
||||
5
erp_crm/.gitignore
vendored
Normal file
5
erp_crm/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
102
erp_crm/.gitlab-ci.yml
Normal file
102
erp_crm/.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# DiDi ERP/CRM (ERPNext v15 + custom app didi_custom) — GitLab CI/CD
|
||||
# ===================================================================
|
||||
|
||||
stages:
|
||||
- security
|
||||
- lint
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
- release
|
||||
|
||||
security:gitleaks:
|
||||
stage: security
|
||||
image:
|
||||
name: zricethezav/gitleaks:latest
|
||||
entrypoint: [""]
|
||||
script:
|
||||
- gitleaks detect --source . --no-banner --report-format json --report-path gitleaks-report.json
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths: [gitleaks-report.json]
|
||||
expire_in: 1 month
|
||||
allow_failure: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
|
||||
lint:python:
|
||||
stage: lint
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- pip install --quiet ruff black
|
||||
script:
|
||||
- ruff check didi_custom/ scripts/ || true
|
||||
- black --check didi_custom/ scripts/ || true
|
||||
allow_failure: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
|
||||
lint:shell:
|
||||
stage: lint
|
||||
image: koalaman/shellcheck-alpine:latest
|
||||
script:
|
||||
- shellcheck build-from-zero.sh seed-data.sh bin/*.sh || true
|
||||
allow_failure: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
test:doctype-validation:
|
||||
stage: test
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- pip install --quiet jsonschema
|
||||
script:
|
||||
- python3 -c "
|
||||
import json, glob;
|
||||
for f in glob.glob('didi_custom/**/doctype/*/*.json', recursive=True):
|
||||
d = json.load(open(f));
|
||||
assert 'doctype' in d.get('doctype', d), f'Bad doctype in {f}';
|
||||
print(f'OK: {f}')
|
||||
"
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
changes: [didi_custom/**/*]
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
|
||||
build:image:
|
||||
stage: build
|
||||
script:
|
||||
- docker compose build
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
when: manual
|
||||
tags: [shell, docker]
|
||||
|
||||
deploy:staging:
|
||||
stage: deploy
|
||||
script:
|
||||
- ./build-from-zero.sh
|
||||
- python3 scripts/setup/01-config.py
|
||||
- python3 scripts/setup/02-doctypes.py
|
||||
- python3 scripts/setup/08-email.py
|
||||
environment:
|
||||
name: staging
|
||||
url: http://erp.example.com
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
when: manual
|
||||
tags: [shell, docker]
|
||||
|
||||
release:
|
||||
stage: release
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
script:
|
||||
- echo "Release ${CI_COMMIT_TAG}"
|
||||
release:
|
||||
tag_name: '$CI_COMMIT_TAG'
|
||||
name: 'Release $CI_COMMIT_TAG'
|
||||
description: 'ERP/CRM release $CI_COMMIT_TAG'
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/
|
||||
253
erp_crm/CONFIGURATION.md
Normal file
253
erp_crm/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
# Configurare ERP/CRM DiDi
|
||||
|
||||
Variabile de mediu, credentiale, integrari externe.
|
||||
|
||||
## Fisier `.env`
|
||||
|
||||
Localizat la `~/erp_crm/.env`. NU se commiteaza in git.
|
||||
|
||||
```env
|
||||
# MariaDB
|
||||
MYSQL_ROOT_PASSWORD=<schimba-ma>
|
||||
DB_PASSWORD=<schimba-ma>
|
||||
|
||||
# ERPNext
|
||||
ERPNEXT_VERSION=v15
|
||||
FRAPPE_VERSION=v15
|
||||
SITE_NAME=didi-erp
|
||||
ADMIN_PASSWORD=<schimba-ma>
|
||||
|
||||
# Redis (intern, container DNS)
|
||||
REDIS_CACHE=redis-cache:6379
|
||||
REDIS_QUEUE=redis-queue:6379
|
||||
REDIS_SOCKETIO=redis-queue:6379
|
||||
|
||||
# Network
|
||||
ERPNEXT_PORT=8080
|
||||
```
|
||||
|
||||
### Variabile critice de schimbat la productie
|
||||
|
||||
| Variabila | De ce | Recomandare |
|
||||
|-----------|-------|-------------|
|
||||
| `MYSQL_ROOT_PASSWORD` | Acces total la DB | 32+ caractere random |
|
||||
| `ADMIN_PASSWORD` | Login ERPNext Administrator | Min 12 caractere, schimbare la prima logare |
|
||||
| `DB_PASSWORD` | Conexiune Frappe la DB | Generat random la `bench new-site` |
|
||||
|
||||
Genereaza parole:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 24
|
||||
# sau
|
||||
pwgen -s 32 1
|
||||
```
|
||||
|
||||
## Configurare in DB (post-install)
|
||||
|
||||
Anumite valori sunt setate doar in DB, nu in `.env`.
|
||||
|
||||
### `Global Defaults`
|
||||
|
||||
| Camp | Valoare implicita | Unde se schimba |
|
||||
|------|-------------------|-----------------|
|
||||
| `default_company` | TOP CLOSSERS SRL | UI: Setup > Global Defaults |
|
||||
| `default_currency` | RON | UI: Setup > Global Defaults |
|
||||
| `country` | Romania | UI: Setup > Global Defaults |
|
||||
|
||||
### `System Settings`
|
||||
|
||||
| Camp | Valoare implicita |
|
||||
|------|-------------------|
|
||||
| `time_zone` | Europe/Bucharest |
|
||||
| `language` | ro |
|
||||
| `setup_complete` | 1 |
|
||||
| `host_name` | http://didi-erpnext:8080 |
|
||||
| `country` | Romania |
|
||||
|
||||
### `Website Settings`
|
||||
|
||||
| Camp | Valoare |
|
||||
|------|---------|
|
||||
| `home_page` | login (sau pagina ce vrei) |
|
||||
| `disable_signup` | 1 (sign-up doar prin Stripe checkout) |
|
||||
|
||||
## Company config
|
||||
|
||||
Company-ul `TOP CLOSSERS SRL` are urmatoarele conturi default (din planul de conturi RO):
|
||||
|
||||
| Setting | Cont |
|
||||
|---------|------|
|
||||
| `default_receivable_account` | Clienti - facturi de intocmit - TC |
|
||||
| `default_income_account` | 704 - Venituri din servicii prestate - TC |
|
||||
| `default_cash_account` | Casa in lei - TC |
|
||||
| `default_bank_account` | Conturi la banci in lei - TC |
|
||||
| `round_off_account` | Round Off - TC |
|
||||
| `cost_center` | Main - TC |
|
||||
|
||||
## Tax templates
|
||||
|
||||
| Template | Cota | Cont colectata |
|
||||
|----------|------|----------------|
|
||||
| TVA 21% | 21% | 4427 - 4427 - TVA colectata - TC |
|
||||
|
||||
TVA 21% e cota standard RO de la 01.08.2025. Pentru cote reduse (5%, 9%), se creeaza template-uri suplimentare.
|
||||
|
||||
## Custom Fields
|
||||
|
||||
Adaugate de `scripts/setup/02-doctypes.py`:
|
||||
|
||||
### `Customer`
|
||||
- `didi_user_id` (Data) - UUID Keycloak, link la userul DiDi
|
||||
- `iam_role` (Data) - `free_tier` / `paid_tier` / `enterprise_tier`
|
||||
- `active_plan` (Data) - cheia planului curent (paid-monthly, etc.)
|
||||
- `plan_activation_date` (Date) - data activarii
|
||||
|
||||
### `Lead`
|
||||
- `source_form` (Data) - formularul de origine (contact, demo, etc.)
|
||||
- `page_origin` (Data) - pagina de origine
|
||||
- `utm_source`, `utm_medium`, `utm_campaign` (Data)
|
||||
|
||||
### `Sales Invoice`
|
||||
- `analysis_consumed` (Check) - daca analiza din factura a fost folosita
|
||||
- `analysis_session_id` (Data) - ID-ul sesiunii de analiza in DiDi
|
||||
|
||||
### `Payment Log`
|
||||
- `stripe_session_id` (Data, indexed) - ID Stripe Checkout Session
|
||||
|
||||
## Custom DocTypes (didi_custom)
|
||||
|
||||
| DocType | Scop |
|
||||
|---------|------|
|
||||
| `Analysis Report` | Raport rezultate analiza cu PDF atasat |
|
||||
| `Payment Log` | Log evenimente webhook Stripe |
|
||||
| `Service Agreement` | Acord servicii la activarea unui plan |
|
||||
| `Website Content` | CMS pentru paginile publice (homepage, services, etc.) |
|
||||
|
||||
## Integrari externe
|
||||
|
||||
### SendGrid (email tranzactional)
|
||||
|
||||
| Setting | Valoare |
|
||||
|---------|---------|
|
||||
| Email Account | `DiDi Outgoing` |
|
||||
| SMTP Server | smtp.sendgrid.net:587 |
|
||||
| TLS | DA |
|
||||
| Username (`login_id`) | `apikey` (literal) |
|
||||
| Password | API key SendGrid (`SG.xxxxxxxx...`) |
|
||||
| Default outgoing | DA |
|
||||
| Send notifications | DA |
|
||||
| From | office@clossers.com (Sender Identity verificat) |
|
||||
|
||||
API key se genereaza din: SendGrid > Settings > API Keys > Create
|
||||
|
||||
Sender Identity se verifica din: SendGrid > Settings > Sender Authentication > Verify Single Sender (sau Domain Authentication pentru deliverability mai buna).
|
||||
|
||||
### Stripe (plati)
|
||||
|
||||
ERPNext nu se conecteaza direct la Stripe. Toate platile pleaca via website-ul Next.js care:
|
||||
1. Creeaza Stripe Checkout Session
|
||||
2. Stripe trimite webhook la `/api/webhooks/stripe`
|
||||
3. Website apeleaza ERPNext API pentru a crea Sales Invoice + Payment Entry + Payment Log
|
||||
|
||||
Variabilele Stripe sunt in `~/website/.env.production`, nu aici.
|
||||
|
||||
### Keycloak (autentificare)
|
||||
|
||||
ERPNext NU foloseste Keycloak direct - se logheaza doar admin/Administrator local. Userii finali (clientii) se logheaza prin website via Keycloak, iar website-ul ii referenctiaza in ERPNext prin `Customer.didi_user_id`.
|
||||
|
||||
### API tokens pentru integrare externa (website -> ERPNext)
|
||||
|
||||
User dedicat: `Administrator` (sau cream `website_api@didi.local` cu rol Website Integration).
|
||||
|
||||
Token-ul de API key + secret se genereaza din:
|
||||
ERPNext > Settings > User > [user] > API Access > Generate Keys
|
||||
|
||||
Format header pentru request:
|
||||
```
|
||||
Authorization: token <api_key>:<api_secret>
|
||||
```
|
||||
|
||||
## SECURITATE — token-urile
|
||||
|
||||
**Token-urile NU se mai pun in acest fisier.** Sunt stocate doar in `.env` (gitignored):
|
||||
|
||||
```bash
|
||||
# In ~/erp_crm/.env si ~/website/.env.production
|
||||
ERPNEXT_API_KEY=<api_key_generat>
|
||||
ERPNEXT_API_SECRET=<api_secret_generat>
|
||||
```
|
||||
|
||||
**Recomandare puternica**: NU folosi userul `Administrator` ca service account pentru website. Creeaza un user dedicat:
|
||||
|
||||
```bash
|
||||
# 1. Creeaza userul website_api@didi.local cu rol "Website Integration"
|
||||
docker exec didi-erpnext bench --site didi-erp add-user \
|
||||
website_api@didi.local --first-name "Website" --last-name "API" --send-welcome-email 0
|
||||
|
||||
# 2. Asigneaza rol minim necesar (in UI: Settings > User > website_api@didi.local > Roles)
|
||||
# Roles recomandate: "Website Manager", "Sales User", "Accounts User"
|
||||
# NU asigna "System Manager" (echivalentul de admin)
|
||||
|
||||
# 3. Genereaza key+secret pentru noul user
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench && \
|
||||
bench --site didi-erp execute frappe.core.doctype.user.user.generate_keys \
|
||||
--args "[\"website_api@didi.local\"]"
|
||||
'
|
||||
|
||||
# 4. Output: copiaza api_key:api_secret in .env (NU comita in repo)
|
||||
```
|
||||
|
||||
**Pentru rotatie** (Administrator sau service account):
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench && \
|
||||
bench --site didi-erp execute frappe.core.doctype.user.user.generate_keys \
|
||||
--args "[\"<user_email>\"]"
|
||||
'
|
||||
```
|
||||
|
||||
**ATENTIE**: Generarea unei chei noi INVALIDEAZA cheile vechi. Actualizeaza imediat `.env` in toate locatiile (website, erp_crm, agent-v3 daca aplicabil).
|
||||
|
||||
## Porturi expuse
|
||||
|
||||
| Port host | Container | Scop |
|
||||
|-----------|-----------|------|
|
||||
| 8080 | didi-erpnext:8080 | Web UI + API |
|
||||
| 3307 | didi-mariadb:3306 | DB (pentru debug, nu expune la prod) |
|
||||
|
||||
Pentru productie, expune doar 8080 via reverse proxy (nginx/Caddy) cu HTTPS.
|
||||
|
||||
## Workspace-uri custom
|
||||
|
||||
Configurate de `scripts/setup/06-sidebar.py`. Active (vizibile):
|
||||
|
||||
| Workspace | Continut |
|
||||
|-----------|----------|
|
||||
| Home | Welcome page Frappe |
|
||||
| Facturi | Sales Invoice list + shortcut Create |
|
||||
| Clienti | Customer list |
|
||||
| CRM | Lead, Opportunity, pipeline Kanban |
|
||||
| Servicii | Item list (DIDI-*) |
|
||||
| Plati | Payment Entry + Payment Log |
|
||||
| Contabilitate | Account, Journal Entry, GL Entry |
|
||||
| Financial Reports | Trial Balance, P&L, Balance Sheet, etc. |
|
||||
| Website CMS | Website Content list |
|
||||
|
||||
Ascunse (standard ERPNext nefolosit): Selling, Buying, Stock, Assets, Quality, Projects, Support, Manufacturing.
|
||||
|
||||
## Notificari email
|
||||
|
||||
Configurate de `scripts/setup/08-email.py`:
|
||||
|
||||
| Notification | Trigger | Receiver |
|
||||
|--------------|---------|----------|
|
||||
| DiDi - Factura Emisa | Sales Invoice Submit | customer.email_id (cu PDF atasat) |
|
||||
| DiDi - Plata Confirmata | Payment Log New | customer.email_id |
|
||||
| DiDi - Expirare Abonament | Subscription, 7 zile inainte | customer.email_id |
|
||||
|
||||
## Limba
|
||||
|
||||
Frappe e configurat in `ro` (Romana). Traduceri suplimentare in `scripts/setup/05-translations.py` (~146 termeni custom DiDi).
|
||||
|
||||
Schimbare limba per user: User > Language.
|
||||
300
erp_crm/INSTALL.md
Normal file
300
erp_crm/INSTALL.md
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
# Instalare ERP/CRM DiDi de la 0
|
||||
|
||||
Ghid complet pentru a porni stack-ul ERPNext + DiDi custom de la o masina goala.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
### Software pe host
|
||||
|
||||
```bash
|
||||
docker --version # >= 24.0
|
||||
docker compose version # >= v2
|
||||
```
|
||||
|
||||
Toata logica de setup ruleaza IN containerul ERPNext prin `bench execute` pe
|
||||
functiile din app-ul `didi_custom` — nu e nevoie de Python pe host.
|
||||
|
||||
### Resurse
|
||||
|
||||
- 4 GB RAM disponibili
|
||||
- 10 GB spatiu disc
|
||||
- Port 8080 liber (sau schimba ERPNEXT_PORT)
|
||||
|
||||
### Retea Docker
|
||||
|
||||
ERPNext-ul ruleaza pe reteaua `didi-network` (impartita cu website-ul). Creeaz-o:
|
||||
|
||||
```bash
|
||||
docker network create didi-network 2>/dev/null || true
|
||||
```
|
||||
|
||||
## Build automat (recomandat)
|
||||
|
||||
Ruleaza:
|
||||
|
||||
```bash
|
||||
cd ~/erp_crm
|
||||
bash build-from-zero.sh
|
||||
```
|
||||
|
||||
Scriptul face urmatoarele (~5 min):
|
||||
1. Verifica `docker network` si `.env`
|
||||
2. Porneste containerele (`docker compose up -d`)
|
||||
3. Asteapta MariaDB sa fie healthy
|
||||
4. Configureaza Redis in `common_site_config.json`
|
||||
5. Creeaza site-ul `didi-erp` cu ERPNext instalat
|
||||
6. Instaleaza app-ul `didi_custom`
|
||||
7. Creeaza Company `TOP CLOSSERS SRL` + plan de conturi RO + An Fiscal curent
|
||||
8. Ruleaza setup-ul complet din `didi_custom` prin `bench execute`
|
||||
(doctypes, campuri custom, CMS, items DIDI + user API, print format,
|
||||
email, workspace + scurtaturi)
|
||||
9. Build assets (`bench build`)
|
||||
10. Verifica accesul HTTP
|
||||
|
||||
## Build manual (pas cu pas)
|
||||
|
||||
Daca preferi sa rulezi fiecare pas individual:
|
||||
|
||||
### Pas 1. Containere
|
||||
|
||||
```bash
|
||||
cd ~/erp_crm
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Asteapta ~30 secunde pana MariaDB e healthy:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
# Toate trebuie sa fie Up; mariadb cu (healthy)
|
||||
```
|
||||
|
||||
### Pas 2. Configurare Redis
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench set-config -g redis_cache "redis://redis-cache:6379" &&
|
||||
bench set-config -g redis_queue "redis://redis-queue:6379" &&
|
||||
bench set-config -g redis_socketio "redis://redis-queue:6379"
|
||||
'
|
||||
```
|
||||
|
||||
### Pas 3. Creare site + instalare ERPNext
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench new-site didi-erp \
|
||||
--mariadb-root-password <parola-root-db> \
|
||||
--admin-password <parola-admin> \
|
||||
--db-host mariadb \
|
||||
--install-app erpnext
|
||||
'
|
||||
```
|
||||
|
||||
Dureaza ~3 minute.
|
||||
|
||||
### Pas 4. Fix MariaDB user pentru orice host
|
||||
|
||||
Userul DB creat de Frappe e restrictionat la host-ul curent. Pentru ca containerele cu workers/scheduler sa se conecteze, schimba la wildcard:
|
||||
|
||||
```bash
|
||||
DB_NAME=$(docker exec didi-erpnext python3 -c "
|
||||
import json
|
||||
with open('/home/frappe/frappe-bench/sites/didi-erp/site_config.json') as f:
|
||||
print(json.load(f)['db_name'])
|
||||
")
|
||||
DB_PWD=$(docker exec didi-erpnext python3 -c "
|
||||
import json
|
||||
with open('/home/frappe/frappe-bench/sites/didi-erp/site_config.json') as f:
|
||||
print(json.load(f)['db_password'])
|
||||
")
|
||||
|
||||
docker exec didi-mariadb mariadb -uroot -p<parola-root-db> -e "
|
||||
DROP USER IF EXISTS '$DB_NAME'@'%';
|
||||
CREATE USER '$DB_NAME'@'%' IDENTIFIED BY '$DB_PWD';
|
||||
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_NAME'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
"
|
||||
```
|
||||
|
||||
### Pas 5. Instalare didi_custom
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
printf "frappe\nerpnext\ndidi_custom\n" > sites/apps.txt &&
|
||||
env/bin/pip install -e apps/didi_custom &&
|
||||
bench --site didi-erp install-app didi_custom
|
||||
'
|
||||
```
|
||||
|
||||
### Pas 6. Patch ERPNext (portal_users compatibility)
|
||||
|
||||
Frappe v15 are un bug minor cu campul `portal_users` pe Customer (referinta la modul lipsa). Patch:
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext sed -i \
|
||||
's|for portal_user in self.portal_users:|for portal_user in (getattr(self, "portal_users", None) or []):|' \
|
||||
/home/frappe/frappe-bench/apps/erpnext/erpnext/selling/doctype/customer/customer.py
|
||||
```
|
||||
|
||||
### Pas 7. Companie + plan de conturi Romania + An Fiscal
|
||||
|
||||
⚠️ **NU rula logica frappe cu `python3` pe host sau cu python-ul de sistem din
|
||||
container** — esueaza tacut (frappe e instalat doar in venv-ul bench). Foloseste
|
||||
`bench --site didi-erp execute <modul.functie>`.
|
||||
|
||||
```bash
|
||||
BE='docker exec didi-erpnext bash -c'
|
||||
|
||||
# Warehouse Type "Transit" — cerut la crearea companiei, lipseste in instalarea curata
|
||||
$BE 'cd /home/frappe/frappe-bench && bench --site didi-erp execute frappe.client.insert \
|
||||
--kwargs "{\"doc\":{\"doctype\":\"Warehouse Type\",\"name\":\"Transit\"}}"' || true
|
||||
|
||||
# Company TOP CLOSSERS SRL (cu patch-uri country fixtures incluse in modul)
|
||||
$BE 'cd /home/frappe/frappe-bench && bench --site didi-erp execute didi_custom.make_company_ro3.execute'
|
||||
|
||||
# Plan de conturi RO complet (447 conturi: TVA 4426/4427/4423/4424, venituri 704/707,
|
||||
# default-uri companie, template TVA 21%)
|
||||
$BE 'cd /home/frappe/frappe-bench && bench --site didi-erp execute didi_custom.setup_ro_coa.execute'
|
||||
|
||||
# An Fiscal curent — lipseste complet dupa instalarea curata; facturile pica fara el
|
||||
YEAR=$(date +%Y)
|
||||
$BE "cd /home/frappe/frappe-bench && bench --site didi-erp execute frappe.client.insert \
|
||||
--kwargs '{\"doc\":{\"doctype\":\"Fiscal Year\",\"year\":\"$YEAR\",\"year_start_date\":\"$YEAR-01-01\",\"year_end_date\":\"$YEAR-12-31\"}}'"
|
||||
|
||||
# Price list implicit in RON
|
||||
$BE 'cd /home/frappe/frappe-bench && bench --site didi-erp execute frappe.client.set_value \
|
||||
--kwargs "{\"doctype\":\"Price List\",\"name\":\"Standard Selling\",\"fieldname\":\"currency\",\"value\":\"RON\"}"'
|
||||
```
|
||||
|
||||
### Pas 8. Doctypes custom, campuri si continut CMS
|
||||
|
||||
```bash
|
||||
for fn in didi_custom.setup_doctypes.execute \
|
||||
didi_custom.integrations.migrations.setup_custom_fields.execute \
|
||||
didi_custom.fix_sa.execute \
|
||||
didi_custom.populate_content.execute; do
|
||||
docker exec didi-erpnext bash -c "cd /home/frappe/frappe-bench && bench --site didi-erp execute $fn"
|
||||
done
|
||||
```
|
||||
|
||||
Creeaza: doctype-urile Service Agreement / Payment Log / Website Content, campurile
|
||||
custom pe Customer / Sales Invoice / Lead (didi_user_id, iam_role, active_plan,
|
||||
sales_invoice „Factura asociata" pe Service Agreement etc.) si continutul CMS.
|
||||
|
||||
### Pas 9. Integrare website, print format, email, workspace
|
||||
|
||||
```bash
|
||||
for fn in didi_custom.setup_website_integration.execute \
|
||||
didi_custom.setup_phase2_final.setup_print_format \
|
||||
didi_custom.setup_website_integration.fix_print_format \
|
||||
didi_custom.setup_email.execute \
|
||||
didi_custom.setup_workspaces.execute \
|
||||
didi_custom.setup_shortcuts.execute; do
|
||||
docker exec didi-erpnext bash -c "cd /home/frappe/frappe-bench && bench --site didi-erp execute $fn"
|
||||
done
|
||||
```
|
||||
|
||||
- `setup_website_integration.execute` creeaza items DIDI, rolul „Website Integration"
|
||||
(cu toate permisiunile read necesare validarii facturilor) si userul API
|
||||
`website_api@didi-erp.local` — **noteaza api_key/api_secret afisate**, intra in
|
||||
`.env.production`-ul website-ului.
|
||||
- `fix_print_format` repara print formatul „DiDi Invoice" si seteaza `host_name`
|
||||
(altfel wkhtmltopdf da `HostNotFoundError` la logo).
|
||||
- `setup_email.execute` configureaza contul SMTP „DiDi Outgoing" (Mailpit in test;
|
||||
in productie se schimba doar host/port/credentiale in Email Account).
|
||||
|
||||
### Pas 10. Scripturile legacy (scripts/setup/) — NU se mai folosesc
|
||||
|
||||
Directorul `scripts/setup/` contine prima generatie de scripturi (rulate pe host).
|
||||
Sunt pastrate doar ca referinta istorica — totul a fost mutat in module
|
||||
`didi_custom` rulabile cu `bench execute` (pasii 7-9 de mai sus).
|
||||
|
||||
### Pas 11. Build assets + restart
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench build --force'
|
||||
docker restart didi-erpnext
|
||||
sleep 8
|
||||
curl -s -o /dev/null -w "ERPNext: %{http_code}\n" http://localhost:8080
|
||||
```
|
||||
|
||||
## Verificare instalare
|
||||
|
||||
Daca totul a mers, deschide http://localhost:8080:
|
||||
- Login: `Administrator` / `admin`
|
||||
- Sidebar trebuie sa contina: Home, Facturi, Clienti, CRM, Servicii, Plati, Contabilitate, Financial Reports, Website CMS
|
||||
- Pagina `/app/customer` arata Customer-ul `Test User` (daca ai rulat seed)
|
||||
|
||||
Curl rapid de verificare:
|
||||
|
||||
```bash
|
||||
# Login + verifica company
|
||||
curl -s -c /tmp/c -X POST http://localhost:8080/api/method/login \
|
||||
-d 'usr=Administrator&pwd=admin' && echo
|
||||
|
||||
curl -s -b /tmp/c http://localhost:8080/api/resource/Company | python3 -m json.tool
|
||||
```
|
||||
|
||||
## Date de test (seed)
|
||||
|
||||
Pentru smoke testing dupa instalare:
|
||||
|
||||
```bash
|
||||
bash seed-data.sh
|
||||
```
|
||||
|
||||
Creeaza:
|
||||
- 1 Customer (`Test User`)
|
||||
- 3 Sales Invoices submitted + paid
|
||||
- 1 Payment Log (Stripe simulat)
|
||||
- 1 Service Agreement
|
||||
|
||||
## Probleme frecvente
|
||||
|
||||
### "Module Selling not found"
|
||||
|
||||
`installed_apps` global value nu contine `erpnext`. Fix:
|
||||
|
||||
```bash
|
||||
DB=$(docker exec didi-erpnext cat /home/frappe/frappe-bench/sites/didi-erp/site_config.json | python3 -c "import sys,json; print(json.load(sys.stdin)['db_name'])")
|
||||
docker exec didi-mariadb mariadb -uroot -p<parola-root-db> $DB -e "
|
||||
UPDATE tabDefaultValue SET defvalue='[\"frappe\",\"erpnext\",\"didi_custom\"]' WHERE defkey='installed_apps';
|
||||
"
|
||||
docker exec didi-redis-cache redis-cli FLUSHALL
|
||||
docker restart didi-erpnext
|
||||
```
|
||||
|
||||
### "wkhtmltopdf reported an error: HostNotFoundError"
|
||||
|
||||
`host_name` lipseste in Website Settings. Fix:
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench --site didi-erp set-config -g host_name "http://didi-erpnext:8080"'
|
||||
docker restart didi-erpnext
|
||||
```
|
||||
|
||||
### "Access denied for user '_xxxx'@'host'"
|
||||
|
||||
User DB are host limitat. Vezi Pas 4 mai sus pentru fix.
|
||||
|
||||
### Site ramane la 500 pe `/app/home`
|
||||
|
||||
`setup_complete` e 0 si lipsesc accounts. Treci prin Setup Wizard manual din browser, SAU re-ruleaza Pasul 7 + 8.
|
||||
|
||||
## Reset complet
|
||||
|
||||
Pentru a reseta totul si a porni de la 0:
|
||||
|
||||
```bash
|
||||
cd ~/erp_crm
|
||||
docker compose down -v # -v sterge volumele!
|
||||
docker network rm didi-network 2>/dev/null
|
||||
docker network create didi-network
|
||||
bash build-from-zero.sh
|
||||
```
|
||||
|
||||
ATENTIE: `down -v` sterge IREVERSIBIL toate datele din MariaDB. Faci backup intai daca ai date importante.
|
||||
309
erp_crm/OPERATIONS.md
Normal file
309
erp_crm/OPERATIONS.md
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
# Operare ERP/CRM DiDi
|
||||
|
||||
Ghid pentru administrare zilnica: backup, restart, log-uri, troubleshooting.
|
||||
|
||||
## Comenzi uzuale
|
||||
|
||||
### Status containere
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker stats --no-stream # CPU/RAM realtime
|
||||
```
|
||||
|
||||
### Restart serviciu
|
||||
|
||||
```bash
|
||||
# Doar ERPNext (cel mai des)
|
||||
docker compose restart erpnext
|
||||
|
||||
# Toate
|
||||
docker compose restart
|
||||
|
||||
# Restart cu rebuild image
|
||||
docker compose up -d --build erpnext
|
||||
```
|
||||
|
||||
### Log-uri
|
||||
|
||||
```bash
|
||||
# Container ERPNext (gunicorn + nginx)
|
||||
docker compose logs -f erpnext
|
||||
|
||||
# Worker job-uri background
|
||||
docker compose logs -f queue-short
|
||||
docker compose logs -f queue-long
|
||||
docker compose logs -f scheduler
|
||||
|
||||
# Fisier de log Frappe (in container)
|
||||
docker exec didi-erpnext tail -f /home/frappe/frappe-bench/sites/didi-erp/logs/web.log
|
||||
docker exec didi-erpnext tail -f /home/frappe/logs/database.log
|
||||
```
|
||||
|
||||
### Bench (CLI Frappe)
|
||||
|
||||
```bash
|
||||
# Cache + cache static
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench --site didi-erp clear-cache'
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench --site didi-erp clear-website-cache'
|
||||
|
||||
# Migrate (dupa update Frappe sau didi_custom)
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench --site didi-erp migrate'
|
||||
|
||||
# Rebuild assets (CSS/JS)
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench build --force'
|
||||
|
||||
# Reset Administrator password
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench --site didi-erp set-admin-password admin'
|
||||
|
||||
# Console Python interactiv
|
||||
docker exec -it didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench --site didi-erp console'
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
### Backup automat zilnic (recomandare cron)
|
||||
|
||||
Adauga in crontab (`crontab -e`):
|
||||
|
||||
```cron
|
||||
# Backup zilnic la 02:00, retentie 14 zile
|
||||
0 2 * * * ~/erp_crm/bin/backup.sh
|
||||
```
|
||||
|
||||
Continut `bin/backup.sh` (creeaza-l):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR=/var/backups/didi-erp
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# DB dump
|
||||
docker exec didi-mariadb mariadb-dump -uroot -p<parola-root-db> --all-databases \
|
||||
| gzip > $BACKUP_DIR/db-$DATE.sql.gz
|
||||
|
||||
# Files (uploaded attachments)
|
||||
docker run --rm \
|
||||
-v didi-data-layer_erpnext-sites:/data \
|
||||
-v $BACKUP_DIR:/backup \
|
||||
alpine tar czf /backup/sites-$DATE.tar.gz /data
|
||||
|
||||
# Retention: keep last 14 days
|
||||
find $BACKUP_DIR -mtime +14 -delete
|
||||
echo "Backup OK: $DATE"
|
||||
```
|
||||
|
||||
### Backup manual (Frappe-managed)
|
||||
|
||||
ERPNext are propriu mecanism de backup:
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench --site didi-erp backup --with-files
|
||||
'
|
||||
# Fisierele apar in: sites/didi-erp/private/backups/
|
||||
```
|
||||
|
||||
Copy local:
|
||||
|
||||
```bash
|
||||
docker cp didi-erpnext:/home/frappe/frappe-bench/sites/didi-erp/private/backups/ ./backups-$(date +%Y%m%d)
|
||||
```
|
||||
|
||||
## Restore din backup
|
||||
|
||||
```bash
|
||||
# 1. Copiaza fisierele in container
|
||||
docker cp ./backups-20260525 didi-erpnext:/tmp/restore
|
||||
|
||||
# 2. Restore
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench --site didi-erp restore /tmp/restore/db-XXXXXX.sql.gz \
|
||||
--with-public-files /tmp/restore/files-XXXXXX.tar \
|
||||
--with-private-files /tmp/restore/private-files-XXXXXX.tar \
|
||||
--mariadb-root-password <parola-root-db>
|
||||
'
|
||||
```
|
||||
|
||||
## Update / upgrade
|
||||
|
||||
### Update didi_custom (app-ul nostru)
|
||||
|
||||
Dupa ce modifici cod in `didi_custom/`:
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench --site didi-erp migrate
|
||||
'
|
||||
```
|
||||
|
||||
Daca ai modificat doctype:
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench --site didi-erp execute "frappe.model.sync.sync_for" --kwargs "{\"app\":\"didi_custom\"}"
|
||||
'
|
||||
```
|
||||
|
||||
### Upgrade ERPNext (major version)
|
||||
|
||||
NU recomandat fara o copie test. Procedura:
|
||||
|
||||
1. Backup complet
|
||||
2. Edit `docker-compose.yml`: `ERPNEXT_VERSION=v16` (cand apare)
|
||||
3. `docker compose pull`
|
||||
4. `docker compose up -d`
|
||||
5. `bench --site didi-erp migrate`
|
||||
6. Test extensiv
|
||||
|
||||
## Monitorizare
|
||||
|
||||
### Health checks
|
||||
|
||||
```bash
|
||||
# HTTP
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080
|
||||
|
||||
# DB
|
||||
docker exec didi-mariadb mariadb -uroot -p<parola-root-db> -e "SELECT 1"
|
||||
|
||||
# Redis
|
||||
docker exec didi-redis-cache redis-cli ping
|
||||
docker exec didi-redis-queue redis-cli ping
|
||||
|
||||
# Background workers (verifica heartbeat)
|
||||
docker exec didi-redis-queue redis-cli KEYS 'rq:worker:*'
|
||||
```
|
||||
|
||||
### Email queue
|
||||
|
||||
```bash
|
||||
docker exec didi-mariadb mariadb -u_xxx -pxxx didi_db -e "
|
||||
SELECT status, COUNT(*) FROM \`tabEmail Queue\` GROUP BY status;
|
||||
"
|
||||
|
||||
# Flush manual
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench --site didi-erp execute frappe.email.queue.flush'
|
||||
|
||||
# Trimitere imediata (bypass queue)
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench --site didi-erp send-email --send-now'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Site nu raspunde / 500
|
||||
|
||||
```bash
|
||||
# 1. Log-uri
|
||||
docker compose logs --tail 100 erpnext
|
||||
|
||||
# 2. Restart curat
|
||||
docker exec didi-redis-cache redis-cli FLUSHALL
|
||||
docker exec didi-redis-queue redis-cli FLUSHALL
|
||||
docker compose restart erpnext
|
||||
|
||||
# 3. Daca tot nu merge, verifica DB conexiune
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench && bench --site didi-erp doctor
|
||||
'
|
||||
```
|
||||
|
||||
### Workers nu proceseaza job-uri
|
||||
|
||||
```bash
|
||||
# Status workers
|
||||
docker exec didi-redis-queue redis-cli LRANGE "rq:queue:short" 0 -1 | head
|
||||
docker exec didi-redis-queue redis-cli LRANGE "rq:queue:long" 0 -1 | head
|
||||
|
||||
# Restart workers
|
||||
docker compose restart queue-short queue-long queue-default
|
||||
```
|
||||
|
||||
### "Module Selling not found" sau alte module standard
|
||||
|
||||
`installed_apps` din DB e dezsincronizat. Fix:
|
||||
|
||||
```bash
|
||||
DB=$(docker exec didi-erpnext cat /home/frappe/frappe-bench/sites/didi-erp/site_config.json | python3 -c "import sys,json; print(json.load(sys.stdin)['db_name'])")
|
||||
|
||||
docker exec didi-mariadb mariadb -uroot -p<parola-root-db> $DB -e "
|
||||
UPDATE tabDefaultValue SET defvalue='[\"frappe\",\"erpnext\",\"didi_custom\"]' WHERE defkey='installed_apps';
|
||||
"
|
||||
docker exec didi-redis-cache redis-cli FLUSHALL
|
||||
docker compose restart erpnext
|
||||
```
|
||||
|
||||
### PDF nu se genereaza ("HostNotFoundError")
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench --site didi-erp set-config -g host_name "http://didi-erpnext:8080"
|
||||
'
|
||||
docker compose restart erpnext
|
||||
```
|
||||
|
||||
### Workspace cu titlu romanesc nu se gaseste
|
||||
|
||||
URL-urile Frappe sunt slug-ificate. Diacriticele se pierd. Pune titlul fara diacritice in DB:
|
||||
|
||||
```sql
|
||||
UPDATE tabWorkspace SET title='Plati' WHERE name='Plati';
|
||||
```
|
||||
|
||||
## Scenarii de operare
|
||||
|
||||
### Adaugare user admin nou
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench --site didi-erp add-user nume.user@clossers.com \
|
||||
--first-name Nume \
|
||||
--last-name User \
|
||||
--password parolaTemp123 \
|
||||
--add-system-manager
|
||||
'
|
||||
```
|
||||
|
||||
### Schimbare email From-name (SendGrid)
|
||||
|
||||
```bash
|
||||
# Update Email Account
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench --site didi-erp execute "frappe.db.set_value" --kwargs "{\"doctype\":\"Email Account\", \"docname\":\"DiDi Outgoing\", \"fieldname\":\"email_id\", \"value\":\"office@clossers.com\"}"
|
||||
'
|
||||
docker compose restart erpnext
|
||||
```
|
||||
|
||||
### Sterge factura emisa (anulare)
|
||||
|
||||
ERPNext nu permite delete pe `docstatus=1`. Trebuie cancel + delete:
|
||||
|
||||
```bash
|
||||
docker exec didi-erpnext bash -c '
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench --site didi-erp execute "frappe.client.cancel" --kwargs "{\"doctype\":\"Sales Invoice\", \"name\":\"DIDI-INV-2026-00001\"}"
|
||||
'
|
||||
```
|
||||
|
||||
Apoi din UI sterge documentul anulat.
|
||||
|
||||
## SLA si severitati (conform contract)
|
||||
|
||||
| Severitate | Definitie | Timp raspuns | Timp rezolvare |
|
||||
|------------|-----------|--------------|----------------|
|
||||
| Critica | Sistemul nu functioneaza / plati nefunctionale | 4 ore | 24 ore |
|
||||
| Majora | Functionalitate importanta afectata | 8 ore | 3 zile lucratoare |
|
||||
| Minora | Probleme cosmetice / minore | 24 ore | 10 zile lucratoare |
|
||||
|
||||
Canal tichete: office@clossers.com (sau sistem helpdesk dedicat la productie).
|
||||
|
||||
Program suport: zile lucratoare 09:00 - 18:00.
|
||||
153
erp_crm/README.md
Normal file
153
erp_crm/README.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# DiDi ERP/CRM
|
||||
|
||||
Componenta ERP si CRM a platformei DiDi, construita pe ERPNext v15 (Frappe Framework). Gestioneaza facturarea, contabilitatea, clientii, lead-urile si continutul CMS al website-ului public.
|
||||
|
||||
## Cuprins
|
||||
|
||||
- [Arhitectura](#arhitectura)
|
||||
- [Stack tehnic](#stack-tehnic)
|
||||
- [Structura folder](#structura-folder)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Configurare](#configurare)
|
||||
- [Documentatie detaliata](#documentatie-detaliata)
|
||||
|
||||
---
|
||||
|
||||
## Arhitectura
|
||||
|
||||
```
|
||||
+--------------------+ +-----------+ +----------+
|
||||
| Website Next.js | <--> | ERPNext | ---> | MariaDB |
|
||||
| (clienti, plati) | API | (gunicorn)| | (date) |
|
||||
+--------------------+ +-----+-----+ +----------+
|
||||
|
|
||||
+-------+--------+
|
||||
| Redis cache |
|
||||
| Redis queue |
|
||||
+----------------+
|
||||
|
|
||||
+-------+--------+
|
||||
| 3 workeri |
|
||||
| (short/long/ |
|
||||
| default) |
|
||||
| + scheduler |
|
||||
+----------------+
|
||||
```
|
||||
|
||||
7 containere:
|
||||
- `didi-erpnext` (web API + nginx)
|
||||
- `didi-mariadb` (baza de date)
|
||||
- `didi-redis-cache`, `didi-redis-queue`
|
||||
- `didi-queue-short`, `didi-queue-long`, `didi-queue-default` (background jobs)
|
||||
- `didi-scheduler` (cron jobs)
|
||||
|
||||
## Stack tehnic
|
||||
|
||||
| Componenta | Versiune |
|
||||
|------------|----------|
|
||||
| ERPNext | v15 |
|
||||
| Frappe Framework | v15 |
|
||||
| MariaDB | 10.11 |
|
||||
| Redis | 7-alpine |
|
||||
| Python | 3.11 (in container) |
|
||||
| Domeniu | Services |
|
||||
| Limba | Romana |
|
||||
| Plan conturi | Romania - Chart of Accounts (OMFP 1802/2014) |
|
||||
| TVA | 21% (cota standard RO de la 01.08.2025) |
|
||||
| Moneda | RON |
|
||||
|
||||
## Structura folder
|
||||
|
||||
```
|
||||
erp_crm/
|
||||
├── README.md # Acest fisier
|
||||
├── INSTALL.md # Ghid de instalare de la 0
|
||||
├── OPERATIONS.md # Ghid operare (backup, restart, troubleshooting)
|
||||
├── CONFIGURATION.md # Variabile mediu, credentiale, integrari
|
||||
├── docker-compose.yml # Stack Docker complet
|
||||
├── .env # Configurare credentiale (NU se commiteaza)
|
||||
├── nginx.conf # Reverse proxy intern container
|
||||
├── bin/
|
||||
│ ├── entrypoint.sh # ERPNext startup
|
||||
│ ├── start-bench-service.sh # Worker startup
|
||||
│ └── setup-site.sh # Initial site setup (one-time)
|
||||
├── didi_custom/ # App custom DiDi (Frappe)
|
||||
│ ├── didi_custom/
|
||||
│ │ ├── hooks.py # Frappe hooks
|
||||
│ │ ├── doctype/ # 4 DocType-uri custom:
|
||||
│ │ │ ├── analysis_report/ # - rezultate analize cu PDF
|
||||
│ │ │ ├── payment_log/ # - log evenimente Stripe
|
||||
│ │ │ ├── service_agreement/# - acorduri servicii
|
||||
│ │ │ └── website_content/ # - CMS pagini website
|
||||
│ │ └── ...
|
||||
│ └── pyproject.toml
|
||||
├── mariadb-init/ # Init scripts MariaDB
|
||||
│ └── init.sql
|
||||
├── scripts/
|
||||
│ ├── setup/ # Scripturi setup (numerotate, idempotente)
|
||||
│ │ ├── 01-config.py # Company, taxe, naming series
|
||||
│ │ ├── 02-doctypes.py # Custom fields pe Customer/Lead/Sales Invoice
|
||||
│ │ ├── 03-cleanup.py # Ascunde module standard ERPNext
|
||||
│ │ ├── 04-populate-content.py# Continut CMS implicit (RO + EN)
|
||||
│ │ ├── 05-translations.py # Traduceri suplimentare
|
||||
│ │ ├── 06-sidebar.py # Workspaces custom (Facturi, Clienti, etc.)
|
||||
│ │ ├── 07-reports.py # Rapoarte DiDi (Dashboard Financiar, CRM)
|
||||
│ │ ├── 08-email.py # Email Account SendGrid + notificari
|
||||
│ │ └── 09-phase2-final.py # Finalizari (suppliers, hidden fields)
|
||||
│ └── archive/ # Scripturi vechi/one-off (referinta)
|
||||
└── docs/
|
||||
└── oferta/ # Caietul de sarcini + propunerea tehnica
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
Cere docker si docker compose v2 instalate. Si o retea Docker numita `didi-network`:
|
||||
|
||||
```bash
|
||||
docker network create didi-network 2>/dev/null || true
|
||||
```
|
||||
|
||||
Build complet de la 0 (~5 minute):
|
||||
|
||||
```bash
|
||||
cd ~/erp_crm
|
||||
bash build-from-zero.sh
|
||||
```
|
||||
|
||||
Dupa ce termina, deschide http://localhost:8080 si te loghezi cu `Administrator` / `admin`.
|
||||
|
||||
Pentru detalii, vezi [INSTALL.md](./INSTALL.md).
|
||||
|
||||
## Configurare
|
||||
|
||||
Editezi `.env` pentru a schimba credentiale, parole, porturi:
|
||||
|
||||
```env
|
||||
MYSQL_ROOT_PASSWORD=<parola-root-db>
|
||||
ADMIN_PASSWORD=admin
|
||||
ERPNEXT_PORT=8080
|
||||
SITE_NAME=didi-erp
|
||||
```
|
||||
|
||||
Detalii complete in [CONFIGURATION.md](./CONFIGURATION.md).
|
||||
|
||||
## Documentatie detaliata
|
||||
|
||||
| Document | Scop |
|
||||
|----------|------|
|
||||
| [INSTALL.md](./INSTALL.md) | Build de la 0, dependinte, prima rulare |
|
||||
| [OPERATIONS.md](./OPERATIONS.md) | Backup, restart, log-uri, troubleshooting |
|
||||
| [CONFIGURATION.md](./CONFIGURATION.md) | Env vars, credentiale, integrari externe |
|
||||
| [docs/oferta/](./docs/oferta/) | Caietul de sarcini si propunerea tehnica |
|
||||
|
||||
## Credentiale implicite (DE SCHIMBAT la productie)
|
||||
|
||||
| Cui | Valoare |
|
||||
|-----|---------|
|
||||
| ERPNext Administrator | `Administrator` / `admin` |
|
||||
| MariaDB root | `<parola-root-db>` |
|
||||
| Site name (intern) | `didi-erp` |
|
||||
|
||||
## Suport
|
||||
|
||||
Pentru intrebari tehnice: office@clossers.com
|
||||
30
erp_crm/bin/entrypoint.sh
Executable file
30
erp_crm/bin/entrypoint.sh
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
APP_DIR="/home/frappe/frappe-bench/apps/didi_custom"
|
||||
VENV_PYTHON="/home/frappe/frappe-bench/env/bin/python"
|
||||
|
||||
if [ -d "$APP_DIR" ]; then
|
||||
if ! "$VENV_PYTHON" -c "import didi_custom" >/dev/null 2>&1; then
|
||||
echo "[bootstrap] Installing didi_custom..."
|
||||
/home/frappe/frappe-bench/env/bin/pip install -e "$APP_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start nginx in background for reverse proxy
|
||||
nginx
|
||||
|
||||
# Change to frappe-bench directory (required for site resolution)
|
||||
cd /home/frappe/frappe-bench
|
||||
|
||||
# Start gunicorn (default frappe entrypoint)
|
||||
exec /home/frappe/frappe-bench/env/bin/gunicorn \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--workers 2 \
|
||||
--threads 4 \
|
||||
--worker-class gthread \
|
||||
--worker-tmp-dir /dev/shm \
|
||||
--timeout 120 \
|
||||
--preload \
|
||||
--chdir /home/frappe/frappe-bench/sites \
|
||||
frappe.app:application
|
||||
59
erp_crm/bin/setup-site.sh
Executable file
59
erp_crm/bin/setup-site.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/bin/bash
|
||||
# =============================================================
|
||||
# ERPNext Site Setup Script
|
||||
# Run this ONCE after first docker compose up
|
||||
# Usage: docker exec -it didi-erpnext bash /home/frappe/setup-site.sh
|
||||
# =============================================================
|
||||
|
||||
set -e
|
||||
|
||||
SITE_NAME="${SITE_NAME:-didi.localhost}"
|
||||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin}"
|
||||
DB_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:?seteaza MYSQL_ROOT_PASSWORD in .env}"
|
||||
DB_HOST="mariadb"
|
||||
|
||||
echo "============================================"
|
||||
echo " ERPNext Site Setup - DiDi"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Site: $SITE_NAME"
|
||||
echo "DB Host: $DB_HOST"
|
||||
echo ""
|
||||
|
||||
# Wait for MariaDB to be ready
|
||||
echo "[1/4] Waiting for MariaDB..."
|
||||
for i in $(seq 1 30); do
|
||||
if mysql -h "$DB_HOST" -u root -p"$DB_ROOT_PASSWORD" -e "SELECT 1" &>/dev/null; then
|
||||
echo " MariaDB is ready!"
|
||||
break
|
||||
fi
|
||||
echo " Waiting... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Create new site
|
||||
echo "[2/4] Creating site: $SITE_NAME"
|
||||
bench new-site "$SITE_NAME" \
|
||||
--mariadb-root-password "$DB_ROOT_PASSWORD" \
|
||||
--admin-password "$ADMIN_PASSWORD" \
|
||||
--db-host "$DB_HOST" \
|
||||
--no-mariadb-socket
|
||||
|
||||
# Install ERPNext
|
||||
echo "[3/4] Installing ERPNext app on site..."
|
||||
bench --site "$SITE_NAME" install-app erpnext
|
||||
|
||||
# Set as default site
|
||||
echo "[4/4] Setting as default site..."
|
||||
bench use "$SITE_NAME"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup Complete!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo " URL: http://localhost:8080"
|
||||
echo " User: Administrator"
|
||||
echo " Password: $ADMIN_PASSWORD"
|
||||
echo ""
|
||||
echo "============================================"
|
||||
14
erp_crm/bin/start-bench-service.sh
Executable file
14
erp_crm/bin/start-bench-service.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
APP_DIR="/home/frappe/frappe-bench/apps/didi_custom"
|
||||
VENV_PYTHON="/home/frappe/frappe-bench/env/bin/python"
|
||||
|
||||
if [ -d "$APP_DIR" ]; then
|
||||
if ! "$VENV_PYTHON" -c "import didi_custom" >/dev/null 2>&1; then
|
||||
echo "[bootstrap] Installing didi_custom..."
|
||||
/home/frappe/frappe-bench/env/bin/pip install -e "$APP_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
188
erp_crm/build-from-zero.sh
Executable file
188
erp_crm/build-from-zero.sh
Executable file
|
|
@ -0,0 +1,188 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build de la 0 ERP/CRM DiDi - one-shot installer
|
||||
# Versiune 2.0, iulie 2026
|
||||
#
|
||||
# Cerinte: docker, docker compose v2. Reteaua Docker `didi-network` trebuie sa existe
|
||||
# (o creeaza scriptul daca lipseste).
|
||||
#
|
||||
# NOTA: toata logica de setup ruleaza IN container, prin `bench execute` pe functiile
|
||||
# din app-ul didi_custom. Scripturile din scripts/setup/ sunt LEGACY (prima versiune,
|
||||
# rulate pe host) si NU mai sunt folosite de acest installer.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$DIR"
|
||||
|
||||
log() { printf '\033[1;36m[%(%H:%M:%S)T]\033[0m %s\n' -1 "$*"; }
|
||||
err() { printf '\033[1;31m[%(%H:%M:%S)T] ERROR:\033[0m %s\n' -1 "$*" >&2; exit 1; }
|
||||
|
||||
# ── Verificari ──────────────────────────────────────────────
|
||||
command -v docker >/dev/null || err "docker nu e instalat"
|
||||
docker compose version >/dev/null || err "docker compose v2 nu e instalat"
|
||||
|
||||
[ -f .env ] || err ".env nu exista. Copiaza din .env.example si configureaza."
|
||||
|
||||
source .env
|
||||
SITE_NAME="${SITE_NAME:-didi-erp}"
|
||||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin}"
|
||||
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:?seteaza MYSQL_ROOT_PASSWORD in .env}"
|
||||
ERPNEXT_PORT="${ERPNEXT_PORT:-8080}"
|
||||
|
||||
if ! docker network inspect didi-network >/dev/null 2>&1; then
|
||||
log "Creez reteaua didi-network..."
|
||||
docker network create didi-network
|
||||
fi
|
||||
|
||||
# Helper: ruleaza o functie Python din bench, IN container (cu venv-ul frappe).
|
||||
# NU folosi `python3` pe host sau in container pentru logica frappe — esueaza tacut.
|
||||
bexec() {
|
||||
log " bench execute $1"
|
||||
docker exec didi-erpnext bash -c \
|
||||
"cd /home/frappe/frappe-bench && bench --site $SITE_NAME execute $1" \
|
||||
|| log " ⚠️ $1 a esuat — verifica manual"
|
||||
}
|
||||
# Helper: insert/set-value tolerant la 'exista deja'
|
||||
bexec_kw() {
|
||||
docker exec didi-erpnext bash -c \
|
||||
"cd /home/frappe/frappe-bench && bench --site $SITE_NAME execute $1 --kwargs '$2'" \
|
||||
>/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ── Pas 1. Pornire containere ───────────────────────────────
|
||||
log "Pas 1/10. Pornire containere..."
|
||||
docker compose up -d
|
||||
log " Astept MariaDB sa fie healthy..."
|
||||
for i in $(seq 1 30); do
|
||||
status=$(docker inspect --format='{{.State.Health.Status}}' didi-mariadb 2>/dev/null || echo "starting")
|
||||
[ "$status" = "healthy" ] && break
|
||||
sleep 2
|
||||
done
|
||||
[ "$status" = "healthy" ] || err "MariaDB nu a devenit healthy dupa 60s"
|
||||
log " OK"
|
||||
|
||||
# ── Pas 2. Astepta ERPNext gunicorn ────────────────────────
|
||||
log "Pas 2/10. Astept ERPNext gunicorn..."
|
||||
for i in $(seq 1 30); do
|
||||
if docker exec didi-erpnext curl -sf http://127.0.0.1:8000 >/dev/null 2>&1; then
|
||||
log " OK"
|
||||
break
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
|
||||
# ── Pas 3. Config Redis ─────────────────────────────────────
|
||||
log "Pas 3/10. Configurare Redis in common_site_config..."
|
||||
docker exec didi-erpnext bash -c "
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench set-config -g redis_cache 'redis://redis-cache:6379' &&
|
||||
bench set-config -g redis_queue 'redis://redis-queue:6379' &&
|
||||
bench set-config -g redis_socketio 'redis://redis-queue:6379'
|
||||
" >/dev/null
|
||||
|
||||
# ── Pas 4. Creare site daca nu exista ──────────────────────
|
||||
log "Pas 4/10. Verificare/creare site $SITE_NAME..."
|
||||
if docker exec didi-erpnext test -f "/home/frappe/frappe-bench/sites/${SITE_NAME}/site_config.json"; then
|
||||
log " Site $SITE_NAME exista deja"
|
||||
else
|
||||
log " Creez site $SITE_NAME (dureaza ~3 min)..."
|
||||
docker exec didi-erpnext bash -c "
|
||||
cd /home/frappe/frappe-bench &&
|
||||
bench new-site $SITE_NAME \
|
||||
--mariadb-root-password '$MYSQL_ROOT_PASSWORD' \
|
||||
--admin-password '$ADMIN_PASSWORD' \
|
||||
--db-host mariadb \
|
||||
--install-app erpnext
|
||||
"
|
||||
fi
|
||||
|
||||
# ── Pas 5. Fix MariaDB user @ wildcard ──────────────────────
|
||||
log "Pas 5/10. Fix DB user pentru orice host..."
|
||||
DB_NAME=$(docker exec didi-erpnext python3 -c "
|
||||
import json
|
||||
with open('/home/frappe/frappe-bench/sites/${SITE_NAME}/site_config.json') as f:
|
||||
print(json.load(f)['db_name'])
|
||||
")
|
||||
DB_PWD=$(docker exec didi-erpnext python3 -c "
|
||||
import json
|
||||
with open('/home/frappe/frappe-bench/sites/${SITE_NAME}/site_config.json') as f:
|
||||
print(json.load(f)['db_password'])
|
||||
")
|
||||
docker exec didi-mariadb mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" -e "
|
||||
CREATE USER IF NOT EXISTS '$DB_NAME'@'%' IDENTIFIED BY '$DB_PWD';
|
||||
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_NAME'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
" 2>/dev/null
|
||||
|
||||
# ── Pas 6. Patch portal_user + install didi_custom ─────────
|
||||
log "Pas 6/10. Patch portal_users + install didi_custom..."
|
||||
docker exec didi-erpnext sed -i \
|
||||
's|for portal_user in self.portal_users:|for portal_user in (getattr(self, "portal_users", None) or []):|' \
|
||||
/home/frappe/frappe-bench/apps/erpnext/erpnext/selling/doctype/customer/customer.py 2>/dev/null || true
|
||||
|
||||
docker exec didi-erpnext bash -c "
|
||||
cd /home/frappe/frappe-bench &&
|
||||
printf 'frappe\nerpnext\ndidi_custom\n' > sites/apps.txt &&
|
||||
env/bin/pip install -e apps/didi_custom 2>&1 | tail -2 &&
|
||||
bench --site $SITE_NAME install-app didi_custom 2>&1 | tail -2 || true
|
||||
" >/dev/null
|
||||
|
||||
# Fix installed_apps registry
|
||||
docker exec didi-mariadb mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" "$DB_NAME" -e "
|
||||
UPDATE tabDefaultValue SET defvalue='[\"frappe\",\"erpnext\",\"didi_custom\"]' WHERE defkey='installed_apps';
|
||||
" 2>/dev/null
|
||||
|
||||
# ── Pas 7. Companie + plan de conturi Romania ──────────────
|
||||
log "Pas 7/10. Companie TOP CLOSSERS SRL + plan de conturi RO..."
|
||||
# Warehouse Type "Transit" e cerut la crearea companiei si lipseste in instalarea curata
|
||||
bexec_kw frappe.client.insert '{"doc":{"doctype":"Warehouse Type","name":"Transit"}}'
|
||||
bexec didi_custom.make_company_ro3.execute
|
||||
bexec didi_custom.setup_ro_coa.execute
|
||||
|
||||
# An fiscal curent — lipseste complet dupa instalarea curata; facturile pica fara el
|
||||
YEAR=$(date +%Y)
|
||||
log " An fiscal $YEAR..."
|
||||
bexec_kw frappe.client.insert "{\"doc\":{\"doctype\":\"Fiscal Year\",\"year\":\"$YEAR\",\"year_start_date\":\"$YEAR-01-01\",\"year_end_date\":\"$YEAR-12-31\"}}"
|
||||
|
||||
# Price list implicit in RON
|
||||
bexec_kw frappe.client.set_value '{"doctype":"Price List","name":"Standard Selling","fieldname":"currency","value":"RON"}'
|
||||
|
||||
# ── Pas 8. Doctypes, campuri custom, continut CMS ──────────
|
||||
log "Pas 8/10. Doctypes + campuri custom + CMS..."
|
||||
bexec didi_custom.setup_doctypes.execute
|
||||
bexec didi_custom.integrations.migrations.setup_custom_fields.execute
|
||||
bexec didi_custom.fix_sa.execute
|
||||
bexec didi_custom.populate_content.execute
|
||||
|
||||
# ── Pas 9. Integrare website, print format, email, workspace ─
|
||||
log "Pas 9/10. Items DIDI + user API + print format + email + workspace..."
|
||||
bexec didi_custom.setup_website_integration.execute
|
||||
bexec didi_custom.setup_phase2_final.setup_print_format
|
||||
bexec didi_custom.setup_website_integration.fix_print_format
|
||||
bexec didi_custom.setup_email.execute
|
||||
bexec didi_custom.setup_workspaces.execute
|
||||
bexec didi_custom.setup_shortcuts.execute
|
||||
|
||||
# ── Pas 10. Build + restart ────────────────────────────────
|
||||
log "Pas 10/10. Build assets + restart..."
|
||||
docker exec didi-erpnext bash -c 'cd /home/frappe/frappe-bench && bench build --force' 2>&1 | tail -2
|
||||
docker exec didi-redis-cache redis-cli FLUSHALL >/dev/null
|
||||
docker exec didi-redis-queue redis-cli FLUSHALL >/dev/null
|
||||
docker restart didi-erpnext >/dev/null
|
||||
sleep 8
|
||||
|
||||
# ── Verificare finala ───────────────────────────────────────
|
||||
HTTP=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${ERPNEXT_PORT}")
|
||||
log "============================================="
|
||||
if [ "$HTTP" = "200" ] || [ "$HTTP" = "301" ] || [ "$HTTP" = "302" ]; then
|
||||
log " ERPNext UP: http://localhost:${ERPNEXT_PORT}"
|
||||
log " Login: Administrator / ${ADMIN_PASSWORD}"
|
||||
log " Site: ${SITE_NAME}"
|
||||
else
|
||||
log " HTTP ${HTTP} - verifica log-urile cu: docker compose logs erpnext"
|
||||
fi
|
||||
log "============================================="
|
||||
log ""
|
||||
log "Credentialele API pentru website (user website_api@didi-erp.local) se genereaza"
|
||||
log "la Pas 9 — vezi output-ul lui setup_website_integration.execute."
|
||||
log "Pentru date de test (smoke testing): bash seed-data.sh"
|
||||
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
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