feat(invoice+import): refresh facturi, detalii factura, fix duplicate CODMAT + rollback
- PL/SQL: handle duplicate CODMAT in nom_articole with MAX(id_articol) - import_service: add explicit conn.rollback() on Oracle errors - sync_service: auto-fix stale ERROR orders that exist in Oracle - invoice_service: add data_act (invoice date) from vanzari table - sync router: new POST /api/dashboard/refresh-invoices endpoint - order detail: enrich with invoice data (serie, numar, data factura) - dashboard: refresh invoices button (desktop + mobile icon) - quick map modal: compact single-row layout, pre-populate existing mappings - quick map: link on SKU column instead of CODMAT Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,7 @@ CREATE TABLE IF NOT EXISTS orders (
|
||||
factura_total_fara_tva REAL,
|
||||
factura_total_tva REAL,
|
||||
factura_total_cu_tva REAL,
|
||||
factura_data TEXT,
|
||||
invoice_checked_at TEXT,
|
||||
order_total REAL,
|
||||
delivery_cost REAL,
|
||||
@@ -310,6 +311,7 @@ def init_sqlite():
|
||||
("factura_total_fara_tva", "REAL"),
|
||||
("factura_total_tva", "REAL"),
|
||||
("factura_total_cu_tva", "REAL"),
|
||||
("factura_data", "TEXT"),
|
||||
("invoice_checked_at", "TEXT"),
|
||||
("order_total", "REAL"),
|
||||
("delivery_cost", "REAL"),
|
||||
|
||||
@@ -330,6 +330,39 @@ async def order_detail(order_number: str):
|
||||
if sku and sku in codmat_map:
|
||||
item["codmat_details"] = codmat_map[sku]
|
||||
|
||||
# Enrich with invoice data
|
||||
order = detail.get("order", {})
|
||||
if order.get("factura_numar"):
|
||||
order["invoice"] = {
|
||||
"facturat": True,
|
||||
"serie_act": order.get("factura_serie"),
|
||||
"numar_act": order.get("factura_numar"),
|
||||
"data_act": order.get("factura_data"),
|
||||
"total_fara_tva": order.get("factura_total_fara_tva"),
|
||||
"total_tva": order.get("factura_total_tva"),
|
||||
"total_cu_tva": order.get("factura_total_cu_tva"),
|
||||
}
|
||||
elif order.get("id_comanda"):
|
||||
# Check Oracle live
|
||||
try:
|
||||
inv_data = await asyncio.to_thread(
|
||||
invoice_service.check_invoices_for_orders, [order["id_comanda"]]
|
||||
)
|
||||
inv = inv_data.get(order["id_comanda"])
|
||||
if inv and inv.get("facturat"):
|
||||
order["invoice"] = inv
|
||||
await sqlite_service.update_order_invoice(
|
||||
order_number,
|
||||
serie=inv.get("serie_act"),
|
||||
numar=str(inv.get("numar_act", "")),
|
||||
total_fara_tva=inv.get("total_fara_tva"),
|
||||
total_tva=inv.get("total_tva"),
|
||||
total_cu_tva=inv.get("total_cu_tva"),
|
||||
data_act=inv.get("data_act"),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return detail
|
||||
|
||||
|
||||
@@ -372,6 +405,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
||||
"total_fara_tva": o.get("factura_total_fara_tva"),
|
||||
"total_tva": o.get("factura_total_tva"),
|
||||
"total_cu_tva": o.get("factura_total_cu_tva"),
|
||||
"data_act": o.get("factura_data"),
|
||||
}
|
||||
else:
|
||||
o["invoice"] = None
|
||||
@@ -388,6 +422,18 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
||||
idc = o.get("id_comanda")
|
||||
if idc and idc in invoice_data:
|
||||
o["invoice"] = invoice_data[idc]
|
||||
# Update SQLite cache so counts stay accurate
|
||||
inv = invoice_data[idc]
|
||||
if inv.get("facturat"):
|
||||
await sqlite_service.update_order_invoice(
|
||||
o["order_number"],
|
||||
serie=inv.get("serie_act"),
|
||||
numar=str(inv.get("numar_act", "")),
|
||||
total_fara_tva=inv.get("total_fara_tva"),
|
||||
total_tva=inv.get("total_tva"),
|
||||
total_cu_tva=inv.get("total_cu_tva"),
|
||||
data_act=inv.get("data_act"),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -400,11 +446,14 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
||||
|
||||
# Use counts from sqlite_service (already period-scoped)
|
||||
counts = result.get("counts", {})
|
||||
# Prefer SQLite-based uninvoiced count (covers full period, not just current page)
|
||||
counts["nefacturate"] = counts.get("uninvoiced_sqlite", sum(
|
||||
# Count newly-cached invoices found during this request
|
||||
newly_invoiced = sum(1 for o in uncached_orders if o.get("invoice") and o["invoice"].get("facturat"))
|
||||
# Adjust uninvoiced count: start from SQLite count, subtract newly-found invoices
|
||||
uninvoiced_base = counts.get("uninvoiced_sqlite", sum(
|
||||
1 for o in all_orders
|
||||
if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice")
|
||||
))
|
||||
counts["nefacturate"] = max(0, uninvoiced_base - newly_invoiced)
|
||||
imported_total = counts.get("imported_all") or counts.get("imported", 0)
|
||||
counts["facturate"] = max(0, imported_total - counts["nefacturate"])
|
||||
counts.setdefault("total", counts.get("imported", 0) + counts.get("skipped", 0) + counts.get("error", 0))
|
||||
@@ -441,6 +490,38 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/api/dashboard/refresh-invoices")
|
||||
async def refresh_invoices():
|
||||
"""Force-refresh invoice status from Oracle for all uninvoiced imported orders."""
|
||||
try:
|
||||
uninvoiced = await sqlite_service.get_uninvoiced_imported_orders()
|
||||
if not uninvoiced:
|
||||
return {"updated": 0, "message": "Nicio comanda de verificat"}
|
||||
|
||||
id_comanda_list = [o["id_comanda"] for o in uninvoiced]
|
||||
invoice_data = await asyncio.to_thread(
|
||||
invoice_service.check_invoices_for_orders, id_comanda_list
|
||||
)
|
||||
id_to_order = {o["id_comanda"]: o["order_number"] for o in uninvoiced}
|
||||
updated = 0
|
||||
for idc, inv in invoice_data.items():
|
||||
order_num = id_to_order.get(idc)
|
||||
if order_num and inv.get("facturat"):
|
||||
await sqlite_service.update_order_invoice(
|
||||
order_num,
|
||||
serie=inv.get("serie_act"),
|
||||
numar=str(inv.get("numar_act", "")),
|
||||
total_fara_tva=inv.get("total_fara_tva"),
|
||||
total_tva=inv.get("total_tva"),
|
||||
total_cu_tva=inv.get("total_cu_tva"),
|
||||
data_act=inv.get("data_act"),
|
||||
)
|
||||
updated += 1
|
||||
return {"updated": updated, "checked": len(uninvoiced)}
|
||||
except Exception as e:
|
||||
return {"error": str(e), "updated": 0}
|
||||
|
||||
|
||||
@router.put("/api/sync/schedule")
|
||||
async def update_schedule(config: ScheduleConfig):
|
||||
"""Update scheduler configuration."""
|
||||
|
||||
@@ -128,6 +128,7 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
"error": None
|
||||
}
|
||||
|
||||
conn = None
|
||||
try:
|
||||
order_number = clean_web_text(order.number)
|
||||
order_date = convert_web_date(order.date)
|
||||
@@ -138,7 +139,7 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
|
||||
if database.pool is None:
|
||||
raise RuntimeError("Oracle pool not initialized")
|
||||
with database.pool.acquire() as conn:
|
||||
conn = database.pool.acquire()
|
||||
with conn.cursor() as cur:
|
||||
# Step 1: Process partner — use shipping person data for name
|
||||
id_partener = cur.var(oracledb.DB_TYPE_NUMBER)
|
||||
@@ -272,8 +273,24 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
error_msg = str(e)
|
||||
result["error"] = error_msg
|
||||
logger.error(f"Oracle error importing order {order.number}: {error_msg}")
|
||||
if conn:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
logger.error(f"Error importing order {order.number}: {e}")
|
||||
if conn:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if conn:
|
||||
try:
|
||||
database.pool.release(conn)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
@@ -22,7 +22,8 @@ def check_invoices_for_orders(id_comanda_list: list) -> dict:
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT id_comanda, numar_act, serie_act,
|
||||
total_fara_tva, total_tva, total_cu_tva
|
||||
total_fara_tva, total_tva, total_cu_tva,
|
||||
TO_CHAR(data_act, 'YYYY-MM-DD') AS data_act
|
||||
FROM vanzari
|
||||
WHERE id_comanda IN ({placeholders}) AND sters = 0
|
||||
""", params)
|
||||
@@ -34,6 +35,7 @@ def check_invoices_for_orders(id_comanda_list: list) -> dict:
|
||||
"total_fara_tva": float(row[3]) if row[3] else 0,
|
||||
"total_tva": float(row[4]) if row[4] else 0,
|
||||
"total_cu_tva": float(row[5]) if row[5] else 0,
|
||||
"data_act": row[6],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Invoice check failed (table may not exist): {e}")
|
||||
|
||||
@@ -759,7 +759,8 @@ async def get_uninvoiced_imported_orders() -> list:
|
||||
|
||||
async def update_order_invoice(order_number: str, serie: str = None,
|
||||
numar: str = None, total_fara_tva: float = None,
|
||||
total_tva: float = None, total_cu_tva: float = None):
|
||||
total_tva: float = None, total_cu_tva: float = None,
|
||||
data_act: str = None):
|
||||
"""Cache invoice data from Oracle onto the order record."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
@@ -770,10 +771,11 @@ async def update_order_invoice(order_number: str, serie: str = None,
|
||||
factura_total_fara_tva = ?,
|
||||
factura_total_tva = ?,
|
||||
factura_total_cu_tva = ?,
|
||||
factura_data = ?,
|
||||
invoice_checked_at = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE order_number = ?
|
||||
""", (serie, numar, total_fara_tva, total_tva, total_cu_tva, order_number))
|
||||
""", (serie, numar, total_fara_tva, total_tva, total_cu_tva, data_act, order_number))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
@@ -91,6 +91,40 @@ def _derive_customer_info(order):
|
||||
return shipping_name, billing_name, customer, payment_method, delivery_method
|
||||
|
||||
|
||||
async def _fix_stale_error_orders(existing_map: dict, run_id: str):
|
||||
"""Fix orders stuck in ERROR status that are actually in Oracle.
|
||||
|
||||
This can happen when a previous import committed partially (no rollback on error).
|
||||
If the order exists in Oracle COMENZI, update SQLite status to ALREADY_IMPORTED.
|
||||
"""
|
||||
from ..database import get_sqlite
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"SELECT order_number FROM orders WHERE status = 'ERROR'"
|
||||
)
|
||||
error_orders = [row["order_number"] for row in await cursor.fetchall()]
|
||||
fixed = 0
|
||||
for order_number in error_orders:
|
||||
if order_number in existing_map:
|
||||
id_comanda = existing_map[order_number]
|
||||
await db.execute("""
|
||||
UPDATE orders SET
|
||||
status = 'ALREADY_IMPORTED',
|
||||
id_comanda = ?,
|
||||
error_message = NULL,
|
||||
updated_at = datetime('now')
|
||||
WHERE order_number = ? AND status = 'ERROR'
|
||||
""", (id_comanda, order_number))
|
||||
fixed += 1
|
||||
_log_line(run_id, f"#{order_number} → status corectat ERROR → ALREADY_IMPORTED (ID: {id_comanda})")
|
||||
if fixed:
|
||||
await db.commit()
|
||||
logger.info(f"Fixed {fixed} stale ERROR orders that exist in Oracle")
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None) -> dict:
|
||||
"""Run a full sync cycle. Returns summary dict."""
|
||||
global _current_sync
|
||||
@@ -191,6 +225,10 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
validation_service.check_orders_in_roa, min_date, conn
|
||||
)
|
||||
|
||||
# Step 2a-fix: Fix ERROR orders that are actually in Oracle
|
||||
# (can happen if previous import committed partially without rollback)
|
||||
await _fix_stale_error_orders(existing_map, run_id)
|
||||
|
||||
# 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)
|
||||
@@ -447,6 +485,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
total_fara_tva=inv.get("total_fara_tva"),
|
||||
total_tva=inv.get("total_tva"),
|
||||
total_cu_tva=inv.get("total_cu_tva"),
|
||||
data_act=inv.get("data_act"),
|
||||
)
|
||||
invoices_updated += 1
|
||||
if invoices_updated:
|
||||
|
||||
@@ -349,6 +349,18 @@ body {
|
||||
#quickMapModal + .modal-backdrop,
|
||||
.modal-backdrop ~ .modal-backdrop { z-index: 1055; }
|
||||
|
||||
/* ── Quick Map compact lines ─────────────────────── */
|
||||
.qm-line { border-bottom: 1px solid #e5e7eb; padding: 6px 0; }
|
||||
.qm-line:last-child { border-bottom: none; }
|
||||
.qm-row { display: flex; gap: 6px; align-items: center; }
|
||||
.qm-codmat-wrap { flex: 1; min-width: 0; }
|
||||
.qm-rm-btn { padding: 2px 6px; line-height: 1; }
|
||||
#qmCodmatLines .qm-selected:empty { display: none; }
|
||||
#quickMapModal .modal-body { padding-top: 12px; padding-bottom: 8px; }
|
||||
#quickMapModal .modal-header { padding: 10px 16px; }
|
||||
#quickMapModal .modal-header h5 { font-size: 0.95rem; margin: 0; }
|
||||
#quickMapModal .modal-footer { padding: 8px 16px; }
|
||||
|
||||
/* ── Deleted mapping rows ────────────────────────── */
|
||||
tr.mapping-deleted td {
|
||||
text-decoration: line-through;
|
||||
|
||||
@@ -460,6 +460,29 @@ function renderCodmatCell(item) {
|
||||
).join('');
|
||||
}
|
||||
|
||||
// ── Refresh Invoices ──────────────────────────────
|
||||
|
||||
async function refreshInvoices() {
|
||||
const btn = document.getElementById('btnRefreshInvoices');
|
||||
const btnM = document.getElementById('btnRefreshInvoicesMobile');
|
||||
if (btn) { btn.disabled = true; btn.textContent = '⟳ Se verifica...'; }
|
||||
if (btnM) { btnM.disabled = true; }
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/refresh-invoices', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
alert('Eroare: ' + data.error);
|
||||
} else {
|
||||
loadDashOrders();
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Eroare: ' + err.message);
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '↻ Facturi'; }
|
||||
if (btnM) { btnM.disabled = false; }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Order Detail Modal ────────────────────────────
|
||||
|
||||
async function openDashOrderDetail(orderNumber) {
|
||||
@@ -473,6 +496,8 @@ async function openDashOrderDetail(orderNumber) {
|
||||
document.getElementById('detailIdAdresaLivr').textContent = '-';
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="6" class="text-center">Se incarca...</td></tr>';
|
||||
document.getElementById('detailError').style.display = 'none';
|
||||
const invInfo = document.getElementById('detailInvoiceInfo');
|
||||
if (invInfo) invInfo.style.display = 'none';
|
||||
const detailItemsTotal = document.getElementById('detailItemsTotal');
|
||||
if (detailItemsTotal) detailItemsTotal.textContent = '-';
|
||||
const detailOrderTotal = document.getElementById('detailOrderTotal');
|
||||
@@ -503,6 +528,19 @@ async function openDashOrderDetail(orderNumber) {
|
||||
document.getElementById('detailIdAdresaFact').textContent = order.id_adresa_facturare || '-';
|
||||
document.getElementById('detailIdAdresaLivr').textContent = order.id_adresa_livrare || '-';
|
||||
|
||||
// Invoice info
|
||||
const invInfo = document.getElementById('detailInvoiceInfo');
|
||||
const inv = order.invoice;
|
||||
if (inv && inv.facturat) {
|
||||
const serie = inv.serie_act || '';
|
||||
const numar = inv.numar_act || '';
|
||||
document.getElementById('detailInvoiceNumber').textContent = serie ? `${serie} ${numar}` : numar;
|
||||
document.getElementById('detailInvoiceDate').textContent = inv.data_act ? fmtDate(inv.data_act) : '-';
|
||||
if (invInfo) invInfo.style.display = '';
|
||||
} else {
|
||||
if (invInfo) invInfo.style.display = 'none';
|
||||
}
|
||||
|
||||
if (order.error_message) {
|
||||
document.getElementById('detailError').textContent = order.error_message;
|
||||
document.getElementById('detailError').style.display = '';
|
||||
@@ -525,18 +563,21 @@ async function openDashOrderDetail(orderNumber) {
|
||||
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
|
||||
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
|
||||
|
||||
// Store items for quick map pre-population
|
||||
window._detailItems = items;
|
||||
|
||||
// Mobile article flat list
|
||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||
if (mobileContainer) {
|
||||
mobileContainer.innerHTML = '<div class="detail-item-flat">' + items.map((item, idx) => {
|
||||
const codmatList = item.codmat_details?.length
|
||||
? item.codmat_details.map(d => `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(d.codmat)}</span>`).join(' ')
|
||||
: `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(item.codmat || '–')}</span>`;
|
||||
const codmatText = item.codmat_details?.length
|
||||
? item.codmat_details.map(d => `<code>${esc(d.codmat)}</code>`).join(' ')
|
||||
: `<code>${esc(item.codmat || '–')}</code>`;
|
||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||||
return `<div class="dif-item">
|
||||
<div class="dif-row">
|
||||
<span class="dif-sku">${esc(item.sku)}</span>
|
||||
${codmatList}
|
||||
<span class="dif-sku dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}', ${idx})">${esc(item.sku)}</span>
|
||||
${codmatText}
|
||||
</div>
|
||||
<div class="dif-row">
|
||||
<span class="dif-name">${esc(item.product_name || '–')}</span>
|
||||
@@ -547,13 +588,12 @@ async function openDashOrderDetail(orderNumber) {
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
||||
document.getElementById('detailItemsBody').innerHTML = items.map((item, idx) => {
|
||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||||
const codmatCell = `<span class="codmat-link" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}')" title="Click pentru mapare">${renderCodmatCell(item)}</span>`;
|
||||
return `<tr>
|
||||
<td><code>${esc(item.sku)}</code></td>
|
||||
<td><code class="codmat-link" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}', ${idx})" title="Click pentru mapare">${esc(item.sku)}</code></td>
|
||||
<td>${esc(item.product_name || '-')}</td>
|
||||
<td>${codmatCell}</td>
|
||||
<td>${renderCodmatCell(item)}</td>
|
||||
<td>${item.quantity || 0}</td>
|
||||
<td>${item.price != null ? Number(item.price).toFixed(2) : '-'}</td>
|
||||
<td class="text-end">${valoare}</td>
|
||||
@@ -567,7 +607,7 @@ async function openDashOrderDetail(orderNumber) {
|
||||
|
||||
// ── Quick Map Modal ───────────────────────────────
|
||||
|
||||
function openQuickMap(sku, productName, orderNumber) {
|
||||
function openQuickMap(sku, productName, orderNumber, itemIdx) {
|
||||
currentQmSku = sku;
|
||||
currentQmOrderNumber = orderNumber;
|
||||
document.getElementById('qmSku').textContent = sku;
|
||||
@@ -576,36 +616,41 @@ function openQuickMap(sku, productName, orderNumber) {
|
||||
|
||||
const container = document.getElementById('qmCodmatLines');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Pre-populate with existing codmat_details if available
|
||||
const item = (window._detailItems || [])[itemIdx];
|
||||
const details = item?.codmat_details;
|
||||
if (details && details.length > 0) {
|
||||
details.forEach(d => {
|
||||
addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, procent: d.procent_pret, denumire: d.denumire });
|
||||
});
|
||||
} else {
|
||||
addQmCodmatLine();
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('quickMapModal')).show();
|
||||
}
|
||||
|
||||
function addQmCodmatLine() {
|
||||
function addQmCodmatLine(prefill) {
|
||||
const container = document.getElementById('qmCodmatLines');
|
||||
const idx = container.children.length;
|
||||
const codmatVal = prefill?.codmat || '';
|
||||
const cantVal = prefill?.cantitate || 1;
|
||||
const pctVal = prefill?.procent || 100;
|
||||
const denumireVal = prefill?.denumire || '';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border rounded p-2 mb-2 qm-line';
|
||||
div.className = 'qm-line';
|
||||
div.innerHTML = `
|
||||
<div class="mb-2 position-relative">
|
||||
<label class="form-label form-label-sm mb-1">CODMAT (Articol ROA)</label>
|
||||
<input type="text" class="form-control form-control-sm qm-codmat" placeholder="Cauta codmat sau denumire..." autocomplete="off">
|
||||
<div class="qm-row">
|
||||
<div class="qm-codmat-wrap position-relative">
|
||||
<input type="text" class="form-control form-control-sm qm-codmat" placeholder="CODMAT..." autocomplete="off" value="${escHtml(codmatVal)}">
|
||||
<div class="autocomplete-dropdown d-none qm-ac-dropdown"></div>
|
||||
<small class="text-muted qm-selected"></small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<label class="form-label form-label-sm mb-1">Cantitate ROA</label>
|
||||
<input type="number" class="form-control form-control-sm qm-cantitate" value="1" step="0.001" min="0.001">
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<label class="form-label form-label-sm mb-1">Procent Pret (%)</label>
|
||||
<input type="number" class="form-control form-control-sm qm-procent" value="100" step="0.01" min="0" max="100">
|
||||
</div>
|
||||
<div class="col-2 d-flex align-items-end">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.qm-line').remove()"><i class="bi bi-x"></i></button>` : ''}
|
||||
</div>
|
||||
<input type="number" class="form-control form-control-sm qm-cantitate" value="${cantVal}" step="0.001" min="0.001" title="Cantitate ROA" style="width:70px">
|
||||
<input type="number" class="form-control form-control-sm qm-procent" value="${pctVal}" step="0.01" min="0" max="100" title="Procent %" style="width:70px">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger qm-rm-btn" onclick="this.closest('.qm-line').remove()"><i class="bi bi-x"></i></button>` : '<span style="width:30px"></span>'}
|
||||
</div>
|
||||
<div class="qm-selected text-muted" style="font-size:0.75rem;padding-left:2px">${escHtml(denumireVal)}</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
|
||||
{% set rp = request.scope.get('root_path', '') %}
|
||||
<link href="{{ rp }}/static/css/style.css?v=9" rel="stylesheet">
|
||||
<link href="{{ rp }}/static/css/style.css?v=10" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Top Navbar -->
|
||||
|
||||
@@ -69,10 +69,14 @@
|
||||
<button class="filter-pill d-none d-md-inline-flex" data-status="ERROR">Erori <span class="filter-count fc-red" id="cntErr">0</span></button>
|
||||
<button class="filter-pill d-none d-md-inline-flex" data-status="INVOICED">Facturate <span class="filter-count fc-green" id="cntFact">0</span></button>
|
||||
<button class="filter-pill d-none d-md-inline-flex" data-status="UNINVOICED">Nefacturate <span class="filter-count fc-red" id="cntNef">0</span></button>
|
||||
<button class="btn btn-sm btn-outline-secondary d-none d-md-inline-flex align-items-center gap-1" id="btnRefreshInvoices" onclick="refreshInvoices()" title="Actualizeaza status facturi din Oracle">↻ Facturi</button>
|
||||
<!-- Search (integrated, end of row) -->
|
||||
<input type="search" id="orderSearch" placeholder="Cauta..." class="search-input">
|
||||
</div>
|
||||
<div class="d-md-none mb-2" id="dashMobileSeg"></div>
|
||||
<div class="d-md-none mb-2 d-flex align-items-center gap-2">
|
||||
<div class="flex-grow-1" id="dashMobileSeg"></div>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnRefreshInvoicesMobile" onclick="refreshInvoices()" title="Actualizeaza facturi" style="padding:4px 8px; font-size:1rem; line-height:1">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dashPaginationTop" class="pag-strip"></div>
|
||||
<div class="card-body p-0">
|
||||
@@ -115,6 +119,10 @@
|
||||
<small class="text-muted">Client:</small> <strong id="detailCustomer"></strong><br>
|
||||
<small class="text-muted">Data comanda:</small> <span id="detailDate"></span><br>
|
||||
<small class="text-muted">Status:</small> <span id="detailStatus"></span>
|
||||
<div id="detailInvoiceInfo" style="display:none; margin-top:4px;">
|
||||
<small class="text-muted">Factura:</small> <strong id="detailInvoiceNumber"></strong>
|
||||
<br><small class="text-muted">Data factura:</small> <span id="detailInvoiceDate"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">ID Comanda ROA:</small> <span id="detailIdComanda">-</span><br>
|
||||
@@ -164,14 +172,20 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Produs web:</small> <strong id="qmProductName"></strong>
|
||||
<div style="margin-bottom:8px; font-size:0.85rem">
|
||||
<small class="text-muted">Produs:</small> <strong id="qmProductName"></strong>
|
||||
</div>
|
||||
<div class="qm-row" style="font-size:0.7rem; color:#9ca3af; padding:0 0 2px">
|
||||
<span style="flex:1">CODMAT</span>
|
||||
<span style="width:70px">Cant.</span>
|
||||
<span style="width:70px">%</span>
|
||||
<span style="width:30px"></span>
|
||||
</div>
|
||||
<div id="qmCodmatLines">
|
||||
<!-- Dynamic CODMAT lines -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addQmCodmatLine()">
|
||||
<i class="bi bi-plus"></i> Adauga CODMAT
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addQmCodmatLine()" style="font-size:0.8rem; padding:2px 10px">
|
||||
+ CODMAT
|
||||
</button>
|
||||
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
|
||||
</div>
|
||||
@@ -185,5 +199,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=9"></script>
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=11"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -203,9 +203,9 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
-- Cauta mai intai in ARTICOLE_TERTI (mapari speciale / seturi)
|
||||
v_found_mapping := FALSE;
|
||||
|
||||
FOR rec IN (SELECT at.codmat, at.cantitate_roa, at.procent_pret, na.id_articol
|
||||
FOR rec IN (SELECT at.codmat, at.cantitate_roa, at.procent_pret,
|
||||
(SELECT MAX(na.id_articol) FROM nom_articole na WHERE na.codmat = at.codmat) AS id_articol
|
||||
FROM articole_terti at
|
||||
JOIN nom_articole na ON na.codmat = at.codmat
|
||||
WHERE at.sku = v_sku
|
||||
AND at.activ = 1
|
||||
AND at.sters = 0
|
||||
@@ -242,7 +242,8 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
SELECT id_articol, codmat
|
||||
INTO v_id_articol, v_codmat
|
||||
FROM nom_articole
|
||||
WHERE codmat = v_sku;
|
||||
WHERE codmat = v_sku
|
||||
AND id_articol = (SELECT MAX(id_articol) FROM nom_articole WHERE codmat = v_sku);
|
||||
|
||||
v_pret_unitar := NVL(v_pret_web, 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user