Compare commits
2 Commits
c1d8357956
...
feat/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c534a972a9 | ||
|
|
6fc2f34ba9 |
224
CLAUDE.md
224
CLAUDE.md
@@ -1,214 +1,60 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## REGULI OBLIGATORII
|
||||
|
||||
**Pentru task-uri paralele foloseste INTOTDEAUNA TeamCreate + TaskCreate, NU Agent tool cu subagenti paraleli.**
|
||||
Skill-ul `superpowers:dispatching-parallel-agents` NU se aplica in acest proiect. In loc de dispatch cu Agent tool, creeaza o echipa cu TeamCreate, defineste task-uri cu TaskCreate, si spawneaza teammates cu Agent tool + `team_name`.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**System:** Import Comenzi Web GoMag → Sistem ROA Oracle
|
||||
Stack: FastAPI + Jinja2 + Bootstrap 5.3 + Oracle PL/SQL + SQLite
|
||||
|
||||
Importa automat comenzi din GoMag in sistemul ERP ROA Oracle. Stack complet Python/FastAPI.
|
||||
Documentatie completa: [README.md](README.md)
|
||||
|
||||
### Tech Stack
|
||||
- **API + Admin:** FastAPI + Jinja2 + Bootstrap 5.3
|
||||
- **GoMag Integration:** Python (`gomag_client.py` — API download with pagination)
|
||||
- **Sync Orchestrator:** Python (`sync_service.py` — download → parse → validate → import)
|
||||
- **Database:** Oracle PL/SQL packages (IMPORT_PARTENERI, IMPORT_COMENZI) + SQLite (tracking)
|
||||
## Implementare cu TeamCreate
|
||||
|
||||
## GStack Workflow (pentru features noi)
|
||||
**OBLIGATORIU:** Folosim TeamCreate + TaskCreate, NU Agent tool cu subagenti paraleli. Skill-ul `superpowers:dispatching-parallel-agents` NU se aplica in acest proiect.
|
||||
|
||||
1. `/plan-ceo-review` — planning interactiv (alegi modul: expansion / hold scope / reducere)
|
||||
2. Implementare cu TeamCreate (ca de obicei)
|
||||
3. `/review` — code review pe diff înainte de ship
|
||||
4. `/ship` — push + unit tests + crează PR automat
|
||||
5. `/qa` — testează aplicația live în browser real (după ce rulează)
|
||||
- Team lead citeste TOATE fisierele implicate, creeaza planul
|
||||
- **ASTEAPTA aprobare explicita** de la user inainte de implementare
|
||||
- Task-uri pe fisiere non-overlapping (evita conflicte)
|
||||
- Cache-bust static assets (`?v=N`) la fiecare schimbare UI
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Run FastAPI server — INTOTDEAUNA via start.sh (seteaza Oracle env vars)
|
||||
# INTOTDEAUNA via start.sh (seteaza Oracle env vars)
|
||||
./start.sh
|
||||
# NU folosi uvicorn direct — lipsesc LD_LIBRARY_PATH si TNS_ADMIN pentru Oracle
|
||||
# NU folosi uvicorn direct — lipsesc LD_LIBRARY_PATH si TNS_ADMIN
|
||||
|
||||
# Tests
|
||||
python api/test_app_basic.py # Test A - fara Oracle
|
||||
python api/test_integration.py # Test C - cu Oracle
|
||||
python api/test_app_basic.py # fara Oracle
|
||||
python api/test_integration.py # cu Oracle
|
||||
```
|
||||
|
||||
## UI Development Workflow: Preview → Implement → Verify
|
||||
## Reguli critice (nu le incalca)
|
||||
|
||||
**OBLIGATORIU**: Respecta ordinea exacta. NU treci la pasul urmator fara aprobare explicita.
|
||||
### Flux import comenzi
|
||||
1. Download GoMag API → JSON → parse → validate SKU-uri → import Oracle
|
||||
2. Ordinea: **parteneri** (cauta/creeaza) → **adrese** → **comanda** → **factura cache**
|
||||
3. SKU lookup: ARTICOLE_TERTI (mapped) are prioritate fata de NOM_ARTICOLE (direct)
|
||||
4. Complex sets: un SKU → multiple CODMAT-uri cu `procent_pret` (trebuie sa fie sum=100%)
|
||||
5. Comenzi anulate (GoMag statusId=7): verifica daca au factura inainte de stergere din Oracle
|
||||
|
||||
### 1. Plan & Preview — ASTEAPTA APROBARE
|
||||
1. Citeste TOATE fisierele implicate
|
||||
2. Scrie planul de implementare cu decizii de design
|
||||
3. Genereaza **mockup-uri Markdown** care descriu rezultatul asteptat (tabele, liste, cod pseudo-CSS) — NU HTML static
|
||||
4. **Prezinta mockup-urile userului si ASTEAPTA aprobare explicita**
|
||||
5. Rafineaza planul daca userul cere modificari
|
||||
6. **NU trece la implementare pana userul nu spune explicit "ok", "aprob", "executa" sau similar**
|
||||
### Statusuri comenzi
|
||||
`IMPORTED` / `ALREADY_IMPORTED` / `SKIPPED` / `ERROR` / `CANCELLED` / `DELETED_IN_ROA`
|
||||
- Upsert: `IMPORTED` existent NU se suprascrie cu `ALREADY_IMPORTED`
|
||||
- Recovery: la fiecare sync, comenzile ERROR sunt reverificate in Oracle
|
||||
|
||||
### 2. Implementation cu TeamCreate (Agent Teams)
|
||||
### Parteneri
|
||||
- Prioritate: **companie** (PJ, cod_fiscal + registru) daca exista in GoMag, altfel persoana fizica cu **shipping name**
|
||||
- Adresa livrare: intotdeauna GoMag shipping
|
||||
- Adresa facturare: daca shipping ≠ billing person → shipping pt ambele; altfel → billing din GoMag
|
||||
|
||||
Folosim **TeamCreate** (team agents), NU superpowers subagents. Diferenta:
|
||||
- **TeamCreate**: agenti independenti cu task list partajat, comunicare directa intre ei, context propriu
|
||||
- **Subagents (Agent tool)**: agenti care raporteaza doar la main — NU se folosesc
|
||||
### Preturi
|
||||
- Dual policy: articolele sunt rutate la `id_pol_vanzare` sau `id_pol_productie` pe baza contului contabil (341/345 = productie)
|
||||
- Daca pretul lipseste, se insereaza automat pret=0
|
||||
|
||||
#### Workflow TeamCreate:
|
||||
### Invoice cache
|
||||
- Coloanele `factura_*` pe `orders` (SQLite), populate lazy din Oracle (`vanzari WHERE sters=0`)
|
||||
- Refresh complet: verifica facturi noi + facturi sterse + comenzi sterse din ROA
|
||||
|
||||
1. **Main agent** (team lead) citeste TOATE fisierele implicate, creeaza planul
|
||||
2. **TeamCreate** creeaza echipa (ex: `ui-polish`)
|
||||
3. **TaskCreate** creeaza task-uri independente, pe fisiere non-overlapping:
|
||||
- Task 1: Templates + CSS (HTML templates, style.css, cache-bust)
|
||||
- Task 2: JavaScript (shared.js, dashboard.js, logs.js, mappings.js)
|
||||
- Task 3: Verificare Playwright (depinde de Task 1 + Task 2)
|
||||
4. **Agent tool** cu `team_name` spawneaza teammates folosind agentii predefiniti din `.claude/agents/`:
|
||||
- `subagent_type: ui-templates` → pentru Task 1 (templates + CSS)
|
||||
- `subagent_type: ui-js` → pentru Task 2 (JavaScript)
|
||||
- `subagent_type: ui-verify` → pentru Task 3 (Playwright verification)
|
||||
- `subagent_type: backend-api` → pentru modificari backend/API (routers, services, Oracle/SQLite)
|
||||
- `subagent_type: qa-tester` → pentru teste de integrare
|
||||
5. Teammates lucreaza in paralel, comunica intre ei, marcheaza task-uri completate
|
||||
6. Cand Task 1 + Task 2 sunt complete, teammate-ul de verificare preia Task 3
|
||||
## Deploy Windows
|
||||
|
||||
#### Teammate-ul de verificare (Task 3):
|
||||
1. Navigheaza la fiecare pagina cu Playwright MCP la 375x812 (mobile) si 1440x900 (desktop)
|
||||
2. **Foloseste browser_snapshot** (NU screenshot-uri) pentru a inspecta structura DOM
|
||||
3. Verifica ca implementarea respecta fiecare punct din preview-ul aprobat (structura coloane, bold, dots, filtre etc.)
|
||||
4. Raporteaza discrepante concrete la team lead (ce e diferit fata de preview)
|
||||
5. NU salveaza screenshot-uri after/
|
||||
|
||||
#### Bucla de corectie (responsabilitatea team lead-ului):
|
||||
1. Dupa ce verify-agent raporteaza, **team lead-ul analizeaza discrepantele**
|
||||
2. Pentru fiecare discrepanta, creeaza un nou task de fix si spawneaza un agent sa-l rezolve
|
||||
3. Dupa fix, spawneaza din nou verify-agent pentru re-verificare
|
||||
4. **Repeta bucla** pana cand toate verificarile trec (implementare ≈ preview)
|
||||
5. Abia atunci declara task-ul complet
|
||||
|
||||
```
|
||||
screenshots/
|
||||
└── preview/ # Mockup-uri Markdown aprobate de user (referinta pentru verificare)
|
||||
```
|
||||
|
||||
### Principii
|
||||
- Team lead citeste TOATE fisierele inainte sa creeze task-uri
|
||||
- Task-uri pe fisiere non-overlapping (evita conflicte)
|
||||
- Fiecare task contine prompt detaliat, self-contained
|
||||
- Desktop-ul nu trebuie sa se schimbe cand se adauga imbunatatiri mobile
|
||||
- Cache-bust static assets (increment `?v=N`) la fiecare schimbare UI
|
||||
- Teammates comunica intre ei cu SendMessage, nu doar cu team lead-ul
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
[GoMag API] → [Python Sync Service] → [Oracle PL/SQL] → [FastAPI Admin]
|
||||
↓ ↓ ↑ ↑
|
||||
JSON Orders Download/Parse/Import Store/Update Dashboard + Config
|
||||
↓
|
||||
[SQLite — tracking DB]
|
||||
orders, sync_runs, missing_skus,
|
||||
order_items, web_products,
|
||||
invoice cache, app_settings
|
||||
```
|
||||
|
||||
### FastAPI App Structure
|
||||
- **Routers:** health, dashboard, mappings, articles, validation, sync
|
||||
- **Services:** gomag_client, sync, order_reader, import, mapping, article, validation, invoice, sqlite, scheduler
|
||||
- **Templates:** Jinja2 (dashboard, mappings, missing_skus, logs)
|
||||
- **Static:** CSS (`style.css`), JS (`shared.js`, `dashboard.js`, `logs.js`, `mappings.js`)
|
||||
- **Databases:** Oracle (ERP data) + SQLite (order tracking, sync runs)
|
||||
|
||||
## API Endpoints — Sync & Comenzi
|
||||
|
||||
### Sync
|
||||
| Method | Path | Descriere |
|
||||
|--------|------|-----------|
|
||||
| POST | `/api/sync/start` | Porneste sync in background |
|
||||
| POST | `/api/sync/stop` | Trimite semnal de stop |
|
||||
| GET | `/api/sync/status` | Status curent + last_run |
|
||||
| GET | `/api/sync/history` | Istoric run-uri (paginat) |
|
||||
| GET | `/api/sync/run/{run_id}` | Detalii run |
|
||||
| GET | `/api/sync/run/{run_id}/log` | Log per comanda (JSON) |
|
||||
| GET | `/api/sync/run/{run_id}/text-log` | Log text (live sau din SQLite) |
|
||||
| GET | `/api/sync/run/{run_id}/orders` | Comenzi run filtrate/paginate |
|
||||
| GET | `/api/sync/order/{order_number}` | Detaliu comanda + items + factura |
|
||||
|
||||
### Dashboard Comenzi
|
||||
| Method | Path | Descriere |
|
||||
|--------|------|-----------|
|
||||
| GET | `/api/dashboard/orders` | Comenzi cu date factura (cache SQLite → Oracle fallback) |
|
||||
| POST | `/api/dashboard/refresh-invoices` | Force-refresh stare facturi + comenzi sterse din ROA |
|
||||
|
||||
**Parametri `/api/dashboard/orders`:**
|
||||
- `period_days`: 3/7/30/90 sau 0 (all / custom range)
|
||||
- `period_start`, `period_end`: interval custom (cand `period_days=0`)
|
||||
- `status`: `all` / `IMPORTED` / `SKIPPED` / `ERROR` / `UNINVOICED` / `INVOICED`
|
||||
- `search`, `sort_by`, `sort_dir`, `page`, `per_page`
|
||||
|
||||
**`POST /api/dashboard/refresh-invoices` face:**
|
||||
1. Necacturate → verifica Oracle daca au primit factura
|
||||
2. Cacturate → verifica Oracle daca factura a fost stearsa
|
||||
3. Toate importate → verifica Oracle daca comanda a fost stearsa (→ `DELETED_IN_ROA`)
|
||||
|
||||
### Scheduler
|
||||
| Method | Path | Descriere |
|
||||
|--------|------|-----------|
|
||||
| PUT | `/api/sync/schedule` | Configureaza scheduler (enabled, interval_minutes) |
|
||||
| GET | `/api/sync/schedule` | Status curent scheduler |
|
||||
|
||||
### Settings
|
||||
| Method | Path | Descriere |
|
||||
|--------|------|-----------|
|
||||
| GET | `/api/settings` | Citeste setari aplicatie |
|
||||
| PUT | `/api/settings` | Salveaza setari |
|
||||
| GET | `/api/settings/sectii` | Lista sectii Oracle (dropdown) |
|
||||
| GET | `/api/settings/politici` | Lista politici preturi Oracle (dropdown) |
|
||||
|
||||
## Invoice Cache (SQLite)
|
||||
|
||||
Facturile sunt cacate in coloana `factura_*` pe tabelul `orders`:
|
||||
- `factura_serie`, `factura_numar`, `factura_data`
|
||||
- `factura_total_fara_tva`, `factura_total_tva`, `factura_total_cu_tva`
|
||||
|
||||
**Sursa Oracle:** `SELECT ... FROM vanzari WHERE id_comanda IN (...) AND sters=0`
|
||||
|
||||
**Populare cache:**
|
||||
- La fiecare cerere `/api/dashboard/orders` — comenzile fara cache sunt verificate live si cacate
|
||||
- La deschidere detaliu comanda `/api/sync/order/{order_number}` — verifica live daca nu e caat
|
||||
- La `POST /api/dashboard/refresh-invoices` — refresh complet pentru toate comenzile
|
||||
|
||||
## Business Rules
|
||||
|
||||
### Partners
|
||||
- Search priority: cod_fiscal → denumire → create new
|
||||
- Individuals (CUI 13 digits): separate nume/prenume
|
||||
- Default address: Bucuresti Sectorul 1
|
||||
- All new partners: ID_UTIL = -3
|
||||
|
||||
### Articles & Mappings
|
||||
- Simple SKUs: found directly in nom_articole (not stored in ARTICOLE_TERTI)
|
||||
- Repackaging: SKU → CODMAT with different quantities
|
||||
- Complex sets: One SKU → multiple CODMATs with percentage pricing (must sum to 100%)
|
||||
- Inactive articles: activ=0 (soft delete)
|
||||
|
||||
### Orders
|
||||
- Default: ID_GESTIUNE=1, ID_SECTIE=1, ID_POL=0
|
||||
- Delivery date = order date + 1 day
|
||||
- All orders: INTERNA=0 (external)
|
||||
- **Statuses:** `IMPORTED` / `ALREADY_IMPORTED` / `SKIPPED` / `ERROR` / `DELETED_IN_ROA`
|
||||
- Upsert rule: daca status=`IMPORTED` exista, nu se suprascrie cu `ALREADY_IMPORTED`
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
# .env
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
ORACLE_PASSWORD=********
|
||||
ORACLE_DSN=ROA_ROMFAST
|
||||
TNS_ADMIN=/app
|
||||
```
|
||||
|
||||
## Deploy & Depanare Windows
|
||||
|
||||
Vezi [README.md](README.md#deploy-windows) pentru instructiuni complete de deploy si depanare pe Windows Server.
|
||||
Vezi [README.md](README.md#deploy-windows)
|
||||
|
||||
62
README.md
62
README.md
@@ -101,11 +101,11 @@ gomag-vending/
|
||||
│ │ ├── database.py # Oracle pool + SQLite schema + migrari
|
||||
│ │ ├── routers/ # Endpoint-uri HTTP
|
||||
│ │ │ ├── health.py # GET /health
|
||||
│ │ │ ├── dashboard.py # GET / (HTML)
|
||||
│ │ │ ├── dashboard.py # GET / (HTML) + /settings (HTML)
|
||||
│ │ │ ├── mappings.py # /mappings, /api/mappings
|
||||
│ │ │ ├── articles.py # /api/articles/search
|
||||
│ │ │ ├── validation.py # /api/validate/*
|
||||
│ │ │ └── sync.py # /api/sync/* + /api/dashboard/orders
|
||||
│ │ │ └── sync.py # /api/sync/* + /api/dashboard/* + /api/settings
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── gomag_client.py # Download comenzi GoMag API
|
||||
│ │ │ ├── sync_service.py # Orchestrare: download→validate→import
|
||||
@@ -117,8 +117,8 @@ gomag-vending/
|
||||
│ │ │ ├── article_service.py
|
||||
│ │ │ ├── invoice_service.py # Verificare facturi ROA
|
||||
│ │ │ └── scheduler_service.py # APScheduler timer
|
||||
│ │ ├── templates/ # Jinja2 HTML
|
||||
│ │ └── static/ # CSS + JS
|
||||
│ │ ├── templates/ # Jinja2 (dashboard, mappings, missing_skus, logs, settings)
|
||||
│ │ └── static/ # CSS (style.css) + JS (dashboard, logs, mappings, settings, shared)
|
||||
│ ├── database-scripts/ # Oracle SQL (ARTICOLE_TERTI, packages)
|
||||
│ ├── data/ # SQLite DB (import.db) + JSON orders
|
||||
│ ├── .env # Configurare locala (nu in git)
|
||||
@@ -165,12 +165,16 @@ gomag-vending/
|
||||
## Fluxul de Import
|
||||
|
||||
```
|
||||
1. gomag_client.py descarca comenzi GoMag API → JSON files
|
||||
2. order_reader.py parseaza JSON-urile
|
||||
3. validation_service.py valideaza SKU-uri contra ARTICOLE_TERTI + NOM_ARTICOLE
|
||||
4. import_service.py creeaza/cauta partener in Oracle (shipping person = facturare)
|
||||
5. PACK_IMPORT_COMENZI.importa_comanda_web() insereaza comanda in ROA
|
||||
6. Rezultate salvate in SQLite (orders, sync_run_orders, order_items)
|
||||
1. gomag_client.py descarca comenzi GoMag API → JSON files (paginat)
|
||||
2. order_reader.py parseaza JSON-urile, sorteaza cronologic (cele mai vechi primele)
|
||||
3. Comenzi anulate (GoMag statusId=7) → separate, sterse din Oracle daca nu au factura
|
||||
4. validation_service.py valideaza SKU-uri: ARTICOLE_TERTI (mapped) → NOM_ARTICOLE (direct) → missing
|
||||
5. Verificare existenta in Oracle (COMENZI by date range) → deja importate se sar
|
||||
6. Stale error recovery: comenzi ERROR reverificate in Oracle (crash recovery)
|
||||
7. Validare preturi + dual policy: articole rutate la id_pol_vanzare sau id_pol_productie
|
||||
8. import_service.py: cauta/creeaza partener → adrese → importa comanda in Oracle
|
||||
9. Invoice cache: verifica facturi + comenzi sterse din ROA
|
||||
10. Rezultate salvate in SQLite (orders, sync_run_orders, order_items)
|
||||
```
|
||||
|
||||
### Statuses Comenzi
|
||||
@@ -180,17 +184,30 @@ gomag-vending/
|
||||
| `IMPORTED` | Importata nou in ROA in acest run |
|
||||
| `ALREADY_IMPORTED` | Existenta deja in Oracle, contorizata |
|
||||
| `SKIPPED` | SKU-uri lipsa → neimportata |
|
||||
| `ERROR` | Eroare la import |
|
||||
| `ERROR` | Eroare la import (reverificate automat la urmatorul sync) |
|
||||
| `CANCELLED` | Comanda anulata in GoMag (statusId=7) |
|
||||
| `DELETED_IN_ROA` | A fost importata dar comanda a fost stearsa din ROA |
|
||||
|
||||
**Regula upsert:** daca statusul existent este `IMPORTED`, nu se suprascrie cu `ALREADY_IMPORTED`.
|
||||
|
||||
### Reguli Business
|
||||
- **Persoana**: shipping name = persoana pe eticheta = beneficiarul facturii
|
||||
- **Adresa**: cand billing ≠ shipping → adresa shipping pentru ambele (facturare + livrare)
|
||||
- **SKU simplu**: gasit direct in NOM_ARTICOLE → nu se stocheaza in ARTICOLE_TERTI
|
||||
- **SKU cu repackaging**: un SKU → CODMAT cu cantitate diferita
|
||||
- **SKU set complex**: un SKU → multiple CODMAT-uri cu procente de pret
|
||||
|
||||
**Parteneri & Adrese:**
|
||||
- Prioritate partener: daca exista **companie** in GoMag (billing.company_name) → firma (PJ, cod_fiscal + registru). Altfel → persoana fizica, cu **shipping name** ca nume partener
|
||||
- Adresa livrare: intotdeauna din GoMag shipping
|
||||
- Adresa facturare: daca shipping name ≠ billing name → adresa shipping pt ambele; daca aceeasi persoana → adresa billing din GoMag
|
||||
- Cautare partener in Oracle: cod_fiscal → denumire → create new (ID_UTIL = -3)
|
||||
|
||||
**Articole & Mapari:**
|
||||
- SKU lookup: ARTICOLE_TERTI (mapped, activ=1) are prioritate fata de NOM_ARTICOLE (direct)
|
||||
- SKU simplu: gasit direct in NOM_ARTICOLE → nu se stocheaza in ARTICOLE_TERTI
|
||||
- SKU cu repackaging: un SKU → CODMAT cu cantitate diferita (`cantitate_roa`)
|
||||
- SKU set complex: un SKU → multiple CODMAT-uri cu `procent_pret` (trebuie sum = 100%)
|
||||
|
||||
**Preturi & Discounturi:**
|
||||
- Dual policy: articolele sunt rutate la `id_pol_vanzare` sau `id_pol_productie` pe baza contului contabil (341/345 = productie)
|
||||
- Daca pretul lipseste in politica, se insereaza automat pret=0
|
||||
- Discount VAT splitting: daca `split_discount_vat=1`, discountul se repartizeaza proportional pe cotele TVA din comanda
|
||||
|
||||
---
|
||||
|
||||
@@ -271,18 +288,7 @@ Configuratia este persistata in SQLite (`scheduler_config`).
|
||||
| GET | `/api/settings/sectii` | Lista sectii Oracle |
|
||||
| GET | `/api/settings/politici` | Lista politici preturi Oracle |
|
||||
|
||||
**Setari disponibile:** `transport_codmat`, `transport_vat`, `discount_codmat`, `discount_vat`, `transport_id_pol`, `discount_id_pol`, `id_pol`, `id_sectie`, `gomag_api_key`, `gomag_api_shop`, `gomag_order_days_back`, `gomag_limit`
|
||||
|
||||
---
|
||||
|
||||
## Status Implementare
|
||||
|
||||
| Faza | Status | Descriere |
|
||||
|------|--------|-----------|
|
||||
| Phase 1: Database Foundation | Complet | ARTICOLE_TERTI, PACK_IMPORT_PARTENERI, PACK_IMPORT_COMENZI |
|
||||
| Phase 2: Python Integration | Complet | gomag_client.py, sync_service.py |
|
||||
| Phase 3-4: FastAPI Dashboard | Complet | UI responsive, smart polling, filter bar, paginare |
|
||||
| Phase 5: Production | In Progress | Logging done, Auth + SMTP pending |
|
||||
**Setari disponibile:** `transport_codmat`, `transport_vat`, `discount_codmat`, `discount_vat`, `transport_id_pol`, `discount_id_pol`, `id_pol`, `id_pol_productie`, `id_sectie`, `split_discount_vat`, `gomag_api_key`, `gomag_api_shop`, `gomag_order_days_back`, `gomag_limit`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ def build_articles_json(items, order=None, settings=None) -> str:
|
||||
return json.dumps(articles)
|
||||
|
||||
|
||||
def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None, id_gestiune: int = None) -> dict:
|
||||
def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None, id_gestiuni: list[int] = None) -> dict:
|
||||
"""Import a single order into Oracle ROA.
|
||||
|
||||
Returns dict with:
|
||||
@@ -339,6 +339,9 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
|
||||
id_comanda = cur.var(oracledb.DB_TYPE_NUMBER)
|
||||
|
||||
# Convert list[int] to CSV string for Oracle VARCHAR2 param
|
||||
id_gestiune_csv = ",".join(str(g) for g in id_gestiuni) if id_gestiuni else None
|
||||
|
||||
cur.callproc("PACK_IMPORT_COMENZI.importa_comanda", [
|
||||
order_number, # p_nr_comanda_ext
|
||||
order_date, # p_data_comanda
|
||||
@@ -348,7 +351,7 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
addr_fact_id, # p_id_adresa_facturare
|
||||
id_pol, # p_id_pol
|
||||
id_sectie, # p_id_sectie
|
||||
id_gestiune, # p_id_gestiune
|
||||
id_gestiune_csv, # p_id_gestiune (CSV string)
|
||||
id_comanda # v_id_comanda (OUT)
|
||||
])
|
||||
|
||||
|
||||
@@ -346,13 +346,18 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
app_settings = await sqlite_service.get_app_settings()
|
||||
id_pol = id_pol or int(app_settings.get("id_pol") or 0) or settings.ID_POL
|
||||
id_sectie = id_sectie or int(app_settings.get("id_sectie") or 0) or settings.ID_SECTIE
|
||||
id_gestiune = int(app_settings.get("id_gestiune") or 0) or None # None = orice gestiune
|
||||
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNE={id_gestiune}")
|
||||
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNE={id_gestiune}")
|
||||
# Parse multi-gestiune CSV: "1,3" → [1, 3], "" → None
|
||||
id_gestiune_raw = (app_settings.get("id_gestiune") or "").strip()
|
||||
if id_gestiune_raw and id_gestiune_raw != "0":
|
||||
id_gestiuni = [int(g) for g in id_gestiune_raw.split(",") if g.strip()]
|
||||
else:
|
||||
id_gestiuni = None # None = orice gestiune
|
||||
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNI={id_gestiuni}")
|
||||
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNI={id_gestiuni}")
|
||||
|
||||
# Step 2b: Validate SKUs (reuse same connection)
|
||||
all_skus = order_reader.get_all_skus(orders)
|
||||
validation = await asyncio.to_thread(validation_service.validate_skus, all_skus, conn, id_gestiune)
|
||||
validation = await asyncio.to_thread(validation_service.validate_skus, all_skus, conn, id_gestiuni)
|
||||
importable, skipped = validation_service.classify_orders(orders, validation)
|
||||
|
||||
# ── Split importable into truly_importable vs already_in_roa ──
|
||||
@@ -580,7 +585,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
result = await asyncio.to_thread(
|
||||
import_service.import_single_order,
|
||||
order, id_pol=id_pol, id_sectie=id_sectie,
|
||||
app_settings=app_settings, id_gestiune=id_gestiune
|
||||
app_settings=app_settings, id_gestiuni=id_gestiuni
|
||||
)
|
||||
|
||||
# Build order items data for storage (R9)
|
||||
|
||||
@@ -28,9 +28,10 @@ def check_orders_in_roa(min_date, conn) -> dict:
|
||||
return existing
|
||||
|
||||
|
||||
def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> dict[str, dict]:
|
||||
def resolve_codmat_ids(codmats: set[str], id_gestiuni: list[int] = None, conn=None) -> dict[str, dict]:
|
||||
"""Resolve CODMATs to best id_articol + cont: prefers article with stock, then MAX(id_articol).
|
||||
Filters: sters=0 AND inactiv=0.
|
||||
id_gestiuni: list of warehouse IDs to check stock in, or None for all.
|
||||
Returns: {codmat: {"id_articol": int, "cont": str|None}}
|
||||
"""
|
||||
if not codmats:
|
||||
@@ -40,8 +41,9 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) ->
|
||||
codmat_list = list(codmats)
|
||||
|
||||
# Build stoc subquery dynamically for index optimization
|
||||
if id_gestiune is not None:
|
||||
stoc_filter = "AND s.id_gestiune = :id_gestiune"
|
||||
if id_gestiuni:
|
||||
gest_placeholders = ",".join([f":g{k}" for k in range(len(id_gestiuni))])
|
||||
stoc_filter = f"AND s.id_gestiune IN ({gest_placeholders})"
|
||||
else:
|
||||
stoc_filter = ""
|
||||
|
||||
@@ -54,8 +56,9 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) ->
|
||||
batch = codmat_list[i:i+500]
|
||||
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
|
||||
params = {f"c{j}": cm for j, cm in enumerate(batch)}
|
||||
if id_gestiune is not None:
|
||||
params["id_gestiune"] = id_gestiune
|
||||
if id_gestiuni:
|
||||
for k, gid in enumerate(id_gestiuni):
|
||||
params[f"g{k}"] = gid
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT codmat, id_articol, cont FROM (
|
||||
@@ -84,11 +87,11 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) ->
|
||||
if own_conn:
|
||||
database.pool.release(conn)
|
||||
|
||||
logger.info(f"resolve_codmat_ids: {len(result)}/{len(codmats)} resolved (gestiune={id_gestiune})")
|
||||
logger.info(f"resolve_codmat_ids: {len(result)}/{len(codmats)} resolved (gestiuni={id_gestiuni})")
|
||||
return result
|
||||
|
||||
|
||||
def validate_skus(skus: set[str], conn=None, id_gestiune: int = None) -> dict:
|
||||
def validate_skus(skus: set[str], conn=None, id_gestiuni: list[int] = None) -> dict:
|
||||
"""Validate a set of SKUs against Oracle.
|
||||
Returns: {mapped: set, direct: set, missing: set, direct_id_map: {codmat: {"id_articol": int, "cont": str|None}}}
|
||||
- mapped: found in ARTICOLE_TERTI (active)
|
||||
@@ -124,7 +127,7 @@ def validate_skus(skus: set[str], conn=None, id_gestiune: int = None) -> dict:
|
||||
# Resolve remaining SKUs via resolve_codmat_ids (consistent id_articol selection)
|
||||
all_remaining = [s for s in sku_list if s not in mapped]
|
||||
if all_remaining:
|
||||
direct_id_map = resolve_codmat_ids(set(all_remaining), id_gestiune, conn)
|
||||
direct_id_map = resolve_codmat_ids(set(all_remaining), id_gestiuni, conn)
|
||||
direct = set(direct_id_map.keys())
|
||||
else:
|
||||
direct_id_map = {}
|
||||
|
||||
@@ -18,12 +18,13 @@ async function loadDropdowns() {
|
||||
const politici = await politiciRes.json();
|
||||
const gestiuni = await gestiuniRes.json();
|
||||
|
||||
const gestiuneEl = document.getElementById('settIdGestiune');
|
||||
if (gestiuneEl) {
|
||||
gestiuneEl.innerHTML = '<option value="">— orice gestiune —</option>';
|
||||
const gestContainer = document.getElementById('settGestiuniContainer');
|
||||
if (gestContainer) {
|
||||
gestContainer.innerHTML = '';
|
||||
gestiuni.forEach(g => {
|
||||
gestiuneEl.innerHTML += `<option value="${escHtml(g.id)}">${escHtml(g.label)}</option>`;
|
||||
gestContainer.innerHTML += `<div class="form-check mb-0"><input class="form-check-input" type="checkbox" value="${escHtml(g.id)}" id="gestChk_${escHtml(g.id)}"><label class="form-check-label" for="gestChk_${escHtml(g.id)}">${escHtml(g.label)}</label></div>`;
|
||||
});
|
||||
if (gestiuni.length === 0) gestContainer.innerHTML = '<span class="text-muted small">Nicio gestiune disponibilă</span>';
|
||||
}
|
||||
|
||||
const sectieEl = document.getElementById('settIdSectie');
|
||||
@@ -85,7 +86,15 @@ async function loadSettings() {
|
||||
if (el('settIdPol')) el('settIdPol').value = data.id_pol || '';
|
||||
if (el('settIdPolProductie')) el('settIdPolProductie').value = data.id_pol_productie || '';
|
||||
if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || '';
|
||||
if (el('settIdGestiune')) el('settIdGestiune').value = data.id_gestiune || '';
|
||||
// Multi-gestiune checkboxes
|
||||
const gestVal = data.id_gestiune || '';
|
||||
if (gestVal) {
|
||||
const selectedIds = gestVal.split(',').map(s => s.trim());
|
||||
selectedIds.forEach(id => {
|
||||
const chk = document.getElementById('gestChk_' + id);
|
||||
if (chk) chk.checked = true;
|
||||
});
|
||||
}
|
||||
if (el('settGomagApiKey')) el('settGomagApiKey').value = data.gomag_api_key || '';
|
||||
if (el('settGomagApiShop')) el('settGomagApiShop').value = data.gomag_api_shop || '';
|
||||
if (el('settGomagDaysBack')) el('settGomagDaysBack').value = data.gomag_order_days_back || '7';
|
||||
@@ -109,7 +118,7 @@ async function saveSettings() {
|
||||
id_pol: el('settIdPol')?.value?.trim() || '',
|
||||
id_pol_productie: el('settIdPolProductie')?.value?.trim() || '',
|
||||
id_sectie: el('settIdSectie')?.value?.trim() || '',
|
||||
id_gestiune: el('settIdGestiune')?.value?.trim() || '',
|
||||
id_gestiune: Array.from(document.querySelectorAll('#settGestiuniContainer input:checked')).map(c => c.value).join(','),
|
||||
gomag_api_key: el('settGomagApiKey')?.value?.trim() || '',
|
||||
gomag_api_shop: el('settGomagApiShop')?.value?.trim() || '',
|
||||
gomag_order_days_back: el('settGomagDaysBack')?.value?.trim() || '7',
|
||||
|
||||
@@ -39,10 +39,11 @@
|
||||
<div class="card-header py-2 px-3 fw-semibold">Import ROA</div>
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">Gestiune (ID_GESTIUNE)</label>
|
||||
<select class="form-select form-select-sm" id="settIdGestiune">
|
||||
<option value="">— orice gestiune —</option>
|
||||
</select>
|
||||
<label class="form-label mb-0 small">Gestiuni pentru verificare stoc</label>
|
||||
<div id="settGestiuniContainer" class="border rounded p-2" style="max-height:120px;overflow-y:auto;font-size:0.85rem">
|
||||
<span class="text-muted small">Se încarcă...</span>
|
||||
</div>
|
||||
<div class="form-text" style="font-size:0.75rem">Nicio selecție = orice gestiune</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">Secție (ID_SECTIE)</label>
|
||||
@@ -166,5 +167,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/settings.js?v=5"></script>
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/settings.js?v=6"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -61,7 +61,7 @@ CREATE OR REPLACE PACKAGE PACK_IMPORT_COMENZI AS
|
||||
p_id_adresa_facturare IN NUMBER DEFAULT NULL,
|
||||
p_id_pol IN NUMBER DEFAULT NULL,
|
||||
p_id_sectie IN NUMBER DEFAULT NULL,
|
||||
p_id_gestiune IN NUMBER DEFAULT NULL,
|
||||
p_id_gestiune IN VARCHAR2 DEFAULT NULL,
|
||||
v_id_comanda OUT NUMBER);
|
||||
|
||||
-- Functii pentru managementul erorilor (pentru orchestrator VFP)
|
||||
@@ -93,11 +93,11 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
-- Functie helper: selecteaza id_articol corect pentru un CODMAT
|
||||
-- Prioritate: sters=0 AND inactiv=0, preferinta stoc, MAX(id_articol) fallback
|
||||
-- ================================================================
|
||||
FUNCTION resolve_id_articol(p_codmat IN VARCHAR2, p_id_gest IN NUMBER) RETURN NUMBER IS
|
||||
FUNCTION resolve_id_articol(p_codmat IN VARCHAR2, p_id_gest IN VARCHAR2) RETURN NUMBER IS
|
||||
v_result NUMBER;
|
||||
BEGIN
|
||||
IF p_id_gest IS NOT NULL THEN
|
||||
-- Cu gestiune specifica — Oracle poate folosi index pe stoc(id_gestiune, an, luna)
|
||||
-- Cu gestiuni specifice (CSV: "1,3") — split in subquery pentru IN clause
|
||||
BEGIN
|
||||
SELECT id_articol INTO v_result FROM (
|
||||
SELECT na.id_articol
|
||||
@@ -107,7 +107,11 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM stoc s
|
||||
WHERE s.id_articol = na.id_articol
|
||||
AND s.id_gestiune = p_id_gest
|
||||
AND s.id_gestiune IN (
|
||||
SELECT TO_NUMBER(REGEXP_SUBSTR(p_id_gest, '[^,]+', 1, LEVEL))
|
||||
FROM DUAL
|
||||
CONNECT BY LEVEL <= REGEXP_COUNT(p_id_gest, ',') + 1
|
||||
)
|
||||
AND s.an = EXTRACT(YEAR FROM SYSDATE)
|
||||
AND s.luna = EXTRACT(MONTH FROM SYSDATE)
|
||||
AND s.cants + s.cant - s.cante > 0
|
||||
@@ -150,7 +154,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
p_id_adresa_facturare IN NUMBER DEFAULT NULL,
|
||||
p_id_pol IN NUMBER DEFAULT NULL,
|
||||
p_id_sectie IN NUMBER DEFAULT NULL,
|
||||
p_id_gestiune IN NUMBER DEFAULT NULL,
|
||||
p_id_gestiune IN VARCHAR2 DEFAULT NULL,
|
||||
v_id_comanda OUT NUMBER) IS
|
||||
v_data_livrare DATE;
|
||||
v_sku VARCHAR2(100);
|
||||
|
||||
Reference in New Issue
Block a user