Compare commits
2 Commits
a0649279cf
...
43327c4a70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43327c4a70 | ||
|
|
227dabd6d4 |
@@ -134,3 +134,7 @@ 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.
|
||||
|
||||
79
README.md
79
README.md
@@ -193,6 +193,85 @@ gomag-vending/
|
||||
|
||||
---
|
||||
|
||||
## Deploy Windows
|
||||
|
||||
### Instalare initiala
|
||||
|
||||
```powershell
|
||||
# Ruleaza ca Administrator
|
||||
.\deploy.ps1
|
||||
```
|
||||
|
||||
Scriptul `deploy.ps1` face automat: git clone, venv, dependinte, detectare Oracle, `start.bat`, serviciu NSSM, configurare IIS reverse proxy.
|
||||
|
||||
### Update
|
||||
|
||||
```powershell
|
||||
# Ca Administrator
|
||||
.\update.ps1
|
||||
```
|
||||
|
||||
### Configurare `.env` pe Windows
|
||||
|
||||
```ini
|
||||
# api/.env — exemplu Windows
|
||||
ORACLE_USER=VENDING
|
||||
ORACLE_PASSWORD=****
|
||||
ORACLE_DSN=ROA
|
||||
TNS_ADMIN=C:\roa\instantclient_11_2_0_2
|
||||
INSTANTCLIENTPATH=C:\app\Server\product\18.0.0\dbhomeXE\bin
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- `TNS_ADMIN` = folderul care contine `tnsnames.ora` (NU fisierul in sine)
|
||||
- `ORACLE_DSN` = alias-ul exact din `tnsnames.ora`
|
||||
- `INSTANTCLIENTPATH` = calea catre Oracle bin (thick mode)
|
||||
|
||||
### Serviciu Windows (NSSM)
|
||||
|
||||
```powershell
|
||||
nssm restart GoMagVending # restart
|
||||
nssm status GoMagVending # status
|
||||
nssm stop GoMagVending # stop
|
||||
```
|
||||
|
||||
Loguri serviciu: `logs/service_stdout.log`, `logs/service_stderr.log`
|
||||
Loguri aplicatie: `logs/sync_comenzi_*.log`
|
||||
|
||||
### Depanare SSH
|
||||
|
||||
```bash
|
||||
# Conectare SSH (PowerShell remote, cheie publica)
|
||||
ssh -p 22122 gomag@79.119.86.134
|
||||
|
||||
# Verificare .env
|
||||
cmd /c type C:\gomag-vending\api\.env
|
||||
|
||||
# Test conexiune Oracle
|
||||
C:\gomag-vending\venv\Scripts\python.exe -c "import oracledb, os; os.environ['TNS_ADMIN']='C:/roa/instantclient_11_2_0_2'; conn=oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA'); print('Connected!'); conn.close()"
|
||||
|
||||
# Verificare tnsnames.ora
|
||||
cmd /c type C:\roa\instantclient_11_2_0_2\tnsnames.ora
|
||||
|
||||
# Verificare procese Python
|
||||
Get-Process *python* | Select-Object Id,ProcessName,Path
|
||||
|
||||
# Verificare loguri recente
|
||||
Get-ChildItem C:\gomag-vending\logs\*.log | Sort-Object LastWriteTime -Descending | Select-Object -First 3
|
||||
```
|
||||
|
||||
**Nota SSH:** Userul `gomag` nu are drepturi de admin — `nssm restart` si `net stop/start` necesita PowerShell Administrator direct pe server.
|
||||
|
||||
### Probleme frecvente
|
||||
|
||||
| Eroare | Cauza | Solutie |
|
||||
|--------|-------|---------|
|
||||
| `ORA-12154: TNS:could not resolve` | `TNS_ADMIN` gresit sau `tnsnames.ora` nu contine alias-ul DSN | Verifica `TNS_ADMIN` in `.env` + alias in `tnsnames.ora` |
|
||||
| `ORA-04088: LOGON_AUDIT_TRIGGER` + `Nu aveti licenta pentru PYTHON` | Trigger ROA blocheaza executabile nelicențiate | Adauga `python.exe` (calea completa) in ROASUPORT |
|
||||
| `503 Service Unavailable` pe `/api/articles/search` | Oracle pool nu s-a initializat | Verifica logul `sync_comenzi_*.log` pentru eroarea exacta |
|
||||
|
||||
---
|
||||
|
||||
## WSL2 Note
|
||||
|
||||
- `uvicorn --reload` **nu functioneaza** pe `/mnt/e/` (WSL2 limitation) — restarta manual
|
||||
|
||||
@@ -38,7 +38,6 @@ class Settings(BaseSettings):
|
||||
|
||||
# ROA Import Settings
|
||||
ID_POL: int = 0
|
||||
ID_GESTIUNE: int = 0
|
||||
ID_SECTIE: int = 0
|
||||
|
||||
# GoMag API
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from fastapi import APIRouter, Request, BackgroundTasks
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import HTMLResponse
|
||||
@@ -10,6 +13,7 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from ..services import sync_service, scheduler_service, sqlite_service, invoice_service
|
||||
from .. import database
|
||||
|
||||
router = APIRouter(tags=["sync"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
@@ -29,6 +33,10 @@ class AppSettingsUpdate(BaseModel):
|
||||
discount_id_pol: str = ""
|
||||
id_pol: str = ""
|
||||
id_sectie: str = ""
|
||||
gomag_api_key: str = ""
|
||||
gomag_api_shop: str = ""
|
||||
gomag_order_days_back: str = "7"
|
||||
gomag_limit: str = "100"
|
||||
|
||||
|
||||
# API endpoints
|
||||
@@ -457,16 +465,21 @@ async def get_schedule():
|
||||
@router.get("/api/settings")
|
||||
async def get_app_settings():
|
||||
"""Get application settings."""
|
||||
settings = await sqlite_service.get_app_settings()
|
||||
from ..config import settings as config_settings
|
||||
s = await sqlite_service.get_app_settings()
|
||||
return {
|
||||
"transport_codmat": settings.get("transport_codmat", ""),
|
||||
"transport_vat": settings.get("transport_vat", "21"),
|
||||
"discount_codmat": settings.get("discount_codmat", ""),
|
||||
"transport_id_pol": settings.get("transport_id_pol", ""),
|
||||
"discount_vat": settings.get("discount_vat", "19"),
|
||||
"discount_id_pol": settings.get("discount_id_pol", ""),
|
||||
"id_pol": settings.get("id_pol", ""),
|
||||
"id_sectie": settings.get("id_sectie", ""),
|
||||
"transport_codmat": s.get("transport_codmat", ""),
|
||||
"transport_vat": s.get("transport_vat", "21"),
|
||||
"discount_codmat": s.get("discount_codmat", ""),
|
||||
"transport_id_pol": s.get("transport_id_pol", ""),
|
||||
"discount_vat": s.get("discount_vat", "19"),
|
||||
"discount_id_pol": s.get("discount_id_pol", ""),
|
||||
"id_pol": s.get("id_pol", ""),
|
||||
"id_sectie": s.get("id_sectie", ""),
|
||||
"gomag_api_key": s.get("gomag_api_key", "") or config_settings.GOMAG_API_KEY,
|
||||
"gomag_api_shop": s.get("gomag_api_shop", "") or config_settings.GOMAG_API_SHOP,
|
||||
"gomag_order_days_back": s.get("gomag_order_days_back", "") or str(config_settings.GOMAG_ORDER_DAYS_BACK),
|
||||
"gomag_limit": s.get("gomag_limit", "") or str(config_settings.GOMAG_LIMIT),
|
||||
}
|
||||
|
||||
|
||||
@@ -481,4 +494,48 @@ async def update_app_settings(config: AppSettingsUpdate):
|
||||
await sqlite_service.set_app_setting("discount_id_pol", config.discount_id_pol)
|
||||
await sqlite_service.set_app_setting("id_pol", config.id_pol)
|
||||
await sqlite_service.set_app_setting("id_sectie", config.id_sectie)
|
||||
await sqlite_service.set_app_setting("gomag_api_key", config.gomag_api_key)
|
||||
await sqlite_service.set_app_setting("gomag_api_shop", config.gomag_api_shop)
|
||||
await sqlite_service.set_app_setting("gomag_order_days_back", config.gomag_order_days_back)
|
||||
await sqlite_service.set_app_setting("gomag_limit", config.gomag_limit)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.get("/api/settings/sectii")
|
||||
async def get_sectii():
|
||||
"""Get list of sections from Oracle for dropdown."""
|
||||
def _query():
|
||||
conn = database.get_oracle_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id_sectie, sectie FROM nom_sectii WHERE sters=0 AND inactiv=0 ORDER BY id_sectie"
|
||||
)
|
||||
return [{"id": str(row[0]), "label": f"{row[0]} - {row[1]}"} for row in cur]
|
||||
finally:
|
||||
database.pool.release(conn)
|
||||
try:
|
||||
return await asyncio.to_thread(_query)
|
||||
except Exception as e:
|
||||
logger.error(f"get_sectii error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@router.get("/api/settings/politici")
|
||||
async def get_politici():
|
||||
"""Get list of price policies from Oracle for dropdown."""
|
||||
def _query():
|
||||
conn = database.get_oracle_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id_pol, nume_lista_preturi FROM crm_politici_preturi WHERE sters=0 ORDER BY id_pol"
|
||||
)
|
||||
return [{"id": str(row[0]), "label": f"{row[0]} - {row[1]}"} for row in cur]
|
||||
finally:
|
||||
database.pool.release(conn)
|
||||
try:
|
||||
return await asyncio.to_thread(_query)
|
||||
except Exception as e:
|
||||
logger.error(f"get_politici error: {e}")
|
||||
return []
|
||||
|
||||
@@ -16,19 +16,27 @@ logger = logging.getLogger(__name__)
|
||||
async def download_orders(
|
||||
json_dir: str,
|
||||
days_back: int = None,
|
||||
api_key: str = None,
|
||||
api_shop: str = None,
|
||||
limit: int = None,
|
||||
log_fn: Callable[[str], None] = None,
|
||||
) -> dict:
|
||||
"""Download orders from GoMag API and save as JSON files.
|
||||
|
||||
Returns dict with keys: pages, total, files (list of saved file paths).
|
||||
If API keys are not configured, returns immediately with empty result.
|
||||
Optional api_key, api_shop, limit override config.settings values.
|
||||
"""
|
||||
def _log(msg: str):
|
||||
logger.info(msg)
|
||||
if log_fn:
|
||||
log_fn(msg)
|
||||
|
||||
if not settings.GOMAG_API_KEY or not settings.GOMAG_API_SHOP:
|
||||
effective_key = api_key or settings.GOMAG_API_KEY
|
||||
effective_shop = api_shop or settings.GOMAG_API_SHOP
|
||||
effective_limit = limit or settings.GOMAG_LIMIT
|
||||
|
||||
if not effective_key or not effective_shop:
|
||||
_log("GoMag API keys neconfigurați, skip download")
|
||||
return {"pages": 0, "total": 0, "files": []}
|
||||
|
||||
@@ -40,8 +48,8 @@ async def download_orders(
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
headers = {
|
||||
"Apikey": settings.GOMAG_API_KEY,
|
||||
"ApiShop": settings.GOMAG_API_SHOP,
|
||||
"Apikey": effective_key,
|
||||
"ApiShop": effective_shop,
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
@@ -57,7 +65,7 @@ async def download_orders(
|
||||
params = {
|
||||
"startDate": start_date,
|
||||
"page": page,
|
||||
"limit": settings.GOMAG_LIMIT,
|
||||
"limit": effective_limit,
|
||||
}
|
||||
try:
|
||||
response = await client.get(settings.GOMAG_API_URL, headers=headers, params=params)
|
||||
|
||||
@@ -92,7 +92,8 @@ def build_articles_json(items, order=None, settings=None) -> str:
|
||||
articles.append(article_dict)
|
||||
# Discount total with quantity -1 (positive price)
|
||||
if order.discount_total > 0 and discount_codmat:
|
||||
discount_vat = settings.get("discount_vat", "19")
|
||||
# Use GoMag JSON discount VAT if available, fallback to settings
|
||||
discount_vat = getattr(order, 'discount_vat', None) or settings.get("discount_vat", "19")
|
||||
article_dict = {
|
||||
"sku": discount_codmat,
|
||||
"quantity": "-1",
|
||||
|
||||
@@ -57,6 +57,7 @@ class OrderData:
|
||||
total: float = 0.0
|
||||
delivery_cost: float = 0.0
|
||||
discount_total: float = 0.0
|
||||
discount_vat: Optional[str] = None
|
||||
payment_name: str = ""
|
||||
delivery_name: str = ""
|
||||
source_file: str = ""
|
||||
@@ -160,11 +161,14 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
|
||||
# Parse delivery cost
|
||||
delivery_cost = float(delivery.get("total", 0) or 0) if isinstance(delivery, dict) else 0.0
|
||||
|
||||
# Parse discount total (sum of all discount values)
|
||||
# Parse discount total (sum of all discount values) and VAT from first discount item
|
||||
discount_total = 0.0
|
||||
discount_vat = None
|
||||
for d in data.get("discounts", []):
|
||||
if isinstance(d, dict):
|
||||
discount_total += float(d.get("value", 0) or 0)
|
||||
if discount_vat is None and d.get("vat") is not None:
|
||||
discount_vat = str(d["vat"])
|
||||
|
||||
return OrderData(
|
||||
id=str(data.get("id", order_id)),
|
||||
@@ -178,6 +182,7 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
|
||||
total=float(data.get("total", 0) or 0),
|
||||
delivery_cost=delivery_cost,
|
||||
discount_total=discount_total,
|
||||
discount_vat=discount_vat,
|
||||
payment_name=str(payment.get("name", "")) if isinstance(payment, dict) else "",
|
||||
delivery_name=str(delivery.get("name", "")) if isinstance(delivery, dict) else "",
|
||||
source_file=source_file
|
||||
|
||||
@@ -129,8 +129,18 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
# Phase 0: Download orders from GoMag API
|
||||
_update_progress("downloading", "Descărcare comenzi din GoMag API...")
|
||||
_log_line(run_id, "Descărcare comenzi din GoMag API...")
|
||||
# Read GoMag settings from SQLite (override config defaults)
|
||||
dl_settings = await sqlite_service.get_app_settings()
|
||||
gomag_key = dl_settings.get("gomag_api_key") or None
|
||||
gomag_shop = dl_settings.get("gomag_api_shop") or None
|
||||
gomag_days_str = dl_settings.get("gomag_order_days_back")
|
||||
gomag_days = int(gomag_days_str) if gomag_days_str else None
|
||||
gomag_limit_str = dl_settings.get("gomag_limit")
|
||||
gomag_limit = int(gomag_limit_str) if gomag_limit_str else None
|
||||
dl_result = await gomag_client.download_orders(
|
||||
json_dir, log_fn=lambda msg: _log_line(run_id, msg)
|
||||
json_dir, log_fn=lambda msg: _log_line(run_id, msg),
|
||||
api_key=gomag_key, api_shop=gomag_shop,
|
||||
days_back=gomag_days, limit=gomag_limit,
|
||||
)
|
||||
if dl_result["files"]:
|
||||
_log_line(run_id, f"GoMag: {dl_result['total']} comenzi în {dl_result['pages']} pagini → {len(dl_result['files'])} fișiere")
|
||||
|
||||
@@ -1,11 +1,57 @@
|
||||
let settAcTimeout = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadSettings();
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadDropdowns();
|
||||
await loadSettings();
|
||||
wireAutocomplete('settTransportCodmat', 'settTransportAc');
|
||||
wireAutocomplete('settDiscountCodmat', 'settDiscountAc');
|
||||
});
|
||||
|
||||
async function loadDropdowns() {
|
||||
try {
|
||||
const [sectiiRes, politiciRes] = await Promise.all([
|
||||
fetch('/api/settings/sectii'),
|
||||
fetch('/api/settings/politici')
|
||||
]);
|
||||
const sectii = await sectiiRes.json();
|
||||
const politici = await politiciRes.json();
|
||||
|
||||
const sectieEl = document.getElementById('settIdSectie');
|
||||
if (sectieEl) {
|
||||
sectieEl.innerHTML = '<option value="">— selectează secție —</option>';
|
||||
sectii.forEach(s => {
|
||||
sectieEl.innerHTML += `<option value="${escHtml(s.id)}">${escHtml(s.label)}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
const polEl = document.getElementById('settIdPol');
|
||||
if (polEl) {
|
||||
polEl.innerHTML = '<option value="">— selectează politică —</option>';
|
||||
politici.forEach(p => {
|
||||
polEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
const tPolEl = document.getElementById('settTransportIdPol');
|
||||
if (tPolEl) {
|
||||
tPolEl.innerHTML = '<option value="">— implicită —</option>';
|
||||
politici.forEach(p => {
|
||||
tPolEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
const dPolEl = document.getElementById('settDiscountIdPol');
|
||||
if (dPolEl) {
|
||||
dPolEl.innerHTML = '<option value="">— implicită —</option>';
|
||||
politici.forEach(p => {
|
||||
dPolEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('loadDropdowns error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/settings');
|
||||
@@ -19,6 +65,10 @@ async function loadSettings() {
|
||||
if (el('settDiscountIdPol')) el('settDiscountIdPol').value = data.discount_id_pol || '';
|
||||
if (el('settIdPol')) el('settIdPol').value = data.id_pol || '';
|
||||
if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || '';
|
||||
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';
|
||||
if (el('settGomagLimit')) el('settGomagLimit').value = data.gomag_limit || '100';
|
||||
} catch (err) {
|
||||
console.error('loadSettings error:', err);
|
||||
}
|
||||
@@ -35,6 +85,10 @@ async function saveSettings() {
|
||||
discount_id_pol: el('settDiscountIdPol')?.value?.trim() || '',
|
||||
id_pol: el('settIdPol')?.value?.trim() || '',
|
||||
id_sectie: el('settIdSectie')?.value?.trim() || '',
|
||||
gomag_api_key: el('settGomagApiKey')?.value?.trim() || '',
|
||||
gomag_api_shop: el('settGomagApiShop')?.value?.trim() || '',
|
||||
gomag_order_days_back: el('settGomagDaysBack')?.value?.trim() || '7',
|
||||
gomag_limit: el('settGomagLimit')?.value?.trim() || '100',
|
||||
};
|
||||
try {
|
||||
const res = await fetch('/api/settings', {
|
||||
|
||||
@@ -3,42 +3,74 @@
|
||||
{% block nav_settings %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4">Setari</h4>
|
||||
<h4 class="mb-3">Setari</h4>
|
||||
|
||||
<!-- Import ROA Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Import ROA</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">ID Politica de Pret (ID_POL)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settIdPol" placeholder="ex: 1">
|
||||
<small class="text-muted">Lasa gol pentru valoarea implicita din config</small>
|
||||
<div class="row g-3 mb-3">
|
||||
<!-- GoMag API card -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-2 px-3 fw-semibold">GoMag API</div>
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">API Key</label>
|
||||
<input type="text" class="form-control form-control-sm" id="settGomagApiKey" placeholder="4c5e46...">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">Shop URL</label>
|
||||
<input type="text" class="form-control form-control-sm" id="settGomagApiShop" placeholder="https://coffeepoint.ro">
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label mb-0 small">Zile înapoi</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settGomagDaysBack" value="7" min="1">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label mb-0 small">Limită/pagină</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settGomagLimit" value="100" min="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import ROA card -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<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">Secție (ID_SECTIE)</label>
|
||||
<select class="form-select form-select-sm" id="settIdSectie">
|
||||
<option value="">— selectează secție —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">Politică de Preț (ID_POL)</label>
|
||||
<select class="form-select form-select-sm" id="settIdPol">
|
||||
<option value="">— selectează politică —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">ID Sectie (ID_SECTIE)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settIdSectie" placeholder="ex: 1">
|
||||
<small class="text-muted">Lasa gol pentru valoarea implicita din config</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transport Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Transport</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label form-label-sm mb-1">CODMAT Transport</label>
|
||||
<div class="row g-3 mb-3">
|
||||
<!-- Transport card -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-2 px-3 fw-semibold">Transport</div>
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">CODMAT Transport</label>
|
||||
<div class="position-relative">
|
||||
<input type="text" class="form-control form-control-sm" id="settTransportCodmat" placeholder="ex: TRANSPORT" autocomplete="off">
|
||||
<div class="autocomplete-dropdown d-none" id="settTransportAc"></div>
|
||||
</div>
|
||||
<small class="text-muted">Lasa gol pentru a nu adauga transport la import</small>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1">TVA Transport (%)</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label mb-0 small">TVA Transport (%)</label>
|
||||
<select class="form-select form-select-sm" id="settTransportVat">
|
||||
<option value="5">5%</option>
|
||||
<option value="9">9%</option>
|
||||
@@ -46,30 +78,32 @@
|
||||
<option value="21" selected>21%</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">ID Pol Transport (optional)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settTransportIdPol" placeholder="ex: 2">
|
||||
<small class="text-muted">Politica de pret specifica pentru transport</small>
|
||||
<div class="col-6">
|
||||
<label class="form-label mb-0 small">Politică Transport</label>
|
||||
<select class="form-select form-select-sm" id="settTransportIdPol">
|
||||
<option value="">— implicită —</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discount Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Discount</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label form-label-sm mb-1">CODMAT Discount</label>
|
||||
<!-- Discount card -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-2 px-3 fw-semibold">Discount</div>
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">CODMAT Discount</label>
|
||||
<div class="position-relative">
|
||||
<input type="text" class="form-control form-control-sm" id="settDiscountCodmat" placeholder="ex: DISCOUNT" autocomplete="off">
|
||||
<div class="autocomplete-dropdown d-none" id="settDiscountAc"></div>
|
||||
</div>
|
||||
<small class="text-muted">Lasa gol pentru a nu adauga discount la import</small>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1">TVA Discount (%)</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label mb-0 small">TVA Discount (fallback %)</label>
|
||||
<select class="form-select form-select-sm" id="settDiscountVat">
|
||||
<option value="5">5%</option>
|
||||
<option value="9">9%</option>
|
||||
@@ -77,22 +111,25 @@
|
||||
<option value="21">21%</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">ID Pol Discount (optional)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settDiscountIdPol" placeholder="ex: 2">
|
||||
<small class="text-muted">Politica de pret specifica pentru discount</small>
|
||||
<div class="col-6">
|
||||
<label class="form-label mb-0 small">Politică Discount</label>
|
||||
<select class="form-select form-select-sm" id="settDiscountIdPol">
|
||||
<option value="">— implicită —</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button class="btn btn-primary" onclick="saveSettings()">Salveaza Setarile</button>
|
||||
<span id="settSaveResult" class="ms-2"></span>
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-primary btn-sm" onclick="saveSettings()">Salvează Setările</button>
|
||||
<span id="settSaveResult" class="ms-2 small"></span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/settings.js?v=1"></script>
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/settings.js?v=2"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
-- p_json_articole accepta:
|
||||
-- - array JSON: [{"sku":"X","quantity":"1","price":"10","vat":"19"}, ...]
|
||||
-- - obiect JSON: {"sku":"X","quantity":"1","price":"10","vat":"19"}
|
||||
-- Optional per articol: "id_pol":"5" — politica de pret specifica
|
||||
-- (pentru transport/discount cu politica separata de cea a comenzii)
|
||||
-- Valorile sku, quantity, price, vat sunt extrase ca STRING si convertite.
|
||||
-- Daca comanda exista deja (comanda_externa), nu se dubleaza.
|
||||
-- La eroare ridica RAISE_APPLICATION_ERROR(-20001, mesaj).
|
||||
@@ -113,6 +115,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
v_codmat VARCHAR2(50);
|
||||
v_cantitate_roa NUMBER;
|
||||
v_pret_unitar NUMBER;
|
||||
v_id_pol_articol NUMBER; -- id_pol per articol (din JSON), prioritar fata de p_id_pol
|
||||
|
||||
-- pljson
|
||||
l_json_articole CLOB := p_json_articole;
|
||||
@@ -189,6 +192,13 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
v_pret_web := TO_NUMBER(v_json_obj.get_string('price'));
|
||||
v_vat := TO_NUMBER(v_json_obj.get_string('vat'));
|
||||
|
||||
-- id_pol per articol (optional, pentru transport/discount cu politica separata)
|
||||
BEGIN
|
||||
v_id_pol_articol := TO_NUMBER(v_json_obj.get_string('id_pol'));
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN v_id_pol_articol := NULL;
|
||||
END;
|
||||
|
||||
-- STEP 3: Gaseste articolele ROA pentru acest SKU
|
||||
-- Cauta mai intai in ARTICOLE_TERTI (mapari speciale / seturi)
|
||||
v_found_mapping := FALSE;
|
||||
@@ -211,7 +221,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
BEGIN
|
||||
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
|
||||
V_ID_ARTICOL => rec.id_articol,
|
||||
V_ID_POL => p_id_pol,
|
||||
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
|
||||
V_CANTITATE => v_cantitate_roa,
|
||||
V_PRET => v_pret_unitar,
|
||||
V_ID_UTIL => c_id_util,
|
||||
@@ -238,7 +248,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
|
||||
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
|
||||
V_ID_ARTICOL => v_id_articol,
|
||||
V_ID_POL => p_id_pol,
|
||||
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
|
||||
V_CANTITATE => v_cantitate_web,
|
||||
V_PRET => v_pret_unitar,
|
||||
V_ID_UTIL => c_id_util,
|
||||
|
||||
Reference in New Issue
Block a user