Compare commits
3 Commits
137c4a8b0b
...
ee60a17f00
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee60a17f00 | ||
|
|
926543a2e4 | ||
|
|
25aa9e544c |
@@ -104,7 +104,9 @@ CREATE TABLE IF NOT EXISTS orders (
|
||||
factura_total_tva REAL,
|
||||
factura_total_cu_tva REAL,
|
||||
invoice_checked_at TEXT,
|
||||
order_total REAL
|
||||
order_total REAL,
|
||||
delivery_cost REAL,
|
||||
discount_total REAL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_date ON orders(order_date);
|
||||
@@ -140,6 +142,11 @@ CREATE TABLE IF NOT EXISTS web_products (
|
||||
order_count INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS order_items (
|
||||
order_number TEXT,
|
||||
sku TEXT,
|
||||
@@ -303,6 +310,8 @@ def init_sqlite():
|
||||
("factura_total_cu_tva", "REAL"),
|
||||
("invoice_checked_at", "TEXT"),
|
||||
("order_total", "REAL"),
|
||||
("delivery_cost", "REAL"),
|
||||
("discount_total", "REAL"),
|
||||
]:
|
||||
if col not in order_cols:
|
||||
conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}")
|
||||
|
||||
@@ -52,6 +52,7 @@ class MappingLine(BaseModel):
|
||||
class MappingBatchCreate(BaseModel):
|
||||
sku: str
|
||||
mappings: list[MappingLine]
|
||||
auto_restore: bool = False
|
||||
|
||||
# HTML page
|
||||
@router.get("/mappings", response_class=HTMLResponse)
|
||||
@@ -141,7 +142,7 @@ async def create_batch_mapping(data: MappingBatchCreate):
|
||||
try:
|
||||
results = []
|
||||
for m in data.mappings:
|
||||
r = mapping_service.create_mapping(data.sku, m.codmat, m.cantitate_roa, m.procent_pret)
|
||||
r = mapping_service.create_mapping(data.sku, m.codmat, m.cantitate_roa, m.procent_pret, auto_restore=data.auto_restore)
|
||||
results.append(r)
|
||||
# Mark SKU as resolved in missing_skus tracking
|
||||
await sqlite_service.resolve_missing_sku(data.sku)
|
||||
|
||||
@@ -20,6 +20,12 @@ class ScheduleConfig(BaseModel):
|
||||
interval_minutes: int = 5
|
||||
|
||||
|
||||
class AppSettingsUpdate(BaseModel):
|
||||
transport_codmat: str = ""
|
||||
transport_vat: str = "21"
|
||||
discount_codmat: str = ""
|
||||
|
||||
|
||||
# API endpoints
|
||||
@router.post("/api/sync/start")
|
||||
async def start_sync(background_tasks: BackgroundTasks):
|
||||
@@ -429,3 +435,23 @@ async def update_schedule(config: ScheduleConfig):
|
||||
async def get_schedule():
|
||||
"""Get current scheduler status."""
|
||||
return scheduler_service.get_scheduler_status()
|
||||
|
||||
|
||||
@router.get("/api/settings")
|
||||
async def get_app_settings():
|
||||
"""Get application settings."""
|
||||
settings = 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", ""),
|
||||
}
|
||||
|
||||
|
||||
@router.put("/api/settings")
|
||||
async def update_app_settings(config: AppSettingsUpdate):
|
||||
"""Update application settings."""
|
||||
await sqlite_service.set_app_setting("transport_codmat", config.transport_codmat)
|
||||
await sqlite_service.set_app_setting("transport_vat", config.transport_vat)
|
||||
await sqlite_service.set_app_setting("discount_codmat", config.discount_codmat)
|
||||
return {"success": True}
|
||||
|
||||
@@ -60,8 +60,9 @@ def format_address_for_oracle(address: str, city: str, region: str) -> str:
|
||||
return f"JUD:{region_clean};{city_clean};{address_clean}"
|
||||
|
||||
|
||||
def build_articles_json(items) -> str:
|
||||
"""Build JSON string for Oracle PACK_IMPORT_COMENZI.importa_comanda."""
|
||||
def build_articles_json(items, order=None, settings=None) -> str:
|
||||
"""Build JSON string for Oracle PACK_IMPORT_COMENZI.importa_comanda.
|
||||
Includes transport and discount as extra articles if configured."""
|
||||
articles = []
|
||||
for item in items:
|
||||
articles.append({
|
||||
@@ -71,10 +72,35 @@ def build_articles_json(items) -> str:
|
||||
"vat": str(item.vat),
|
||||
"name": clean_web_text(item.name)
|
||||
})
|
||||
|
||||
if order and settings:
|
||||
transport_codmat = settings.get("transport_codmat", "")
|
||||
transport_vat = settings.get("transport_vat", "21")
|
||||
discount_codmat = settings.get("discount_codmat", "")
|
||||
|
||||
# Transport as article with quantity +1
|
||||
if order.delivery_cost > 0 and transport_codmat:
|
||||
articles.append({
|
||||
"sku": transport_codmat,
|
||||
"quantity": "1",
|
||||
"price": str(order.delivery_cost),
|
||||
"vat": transport_vat,
|
||||
"name": "Transport"
|
||||
})
|
||||
# Discount total with quantity -1 (positive price)
|
||||
if order.discount_total > 0 and discount_codmat:
|
||||
articles.append({
|
||||
"sku": discount_codmat,
|
||||
"quantity": "-1",
|
||||
"price": str(order.discount_total),
|
||||
"vat": "21",
|
||||
"name": "Discount"
|
||||
})
|
||||
|
||||
return json.dumps(articles)
|
||||
|
||||
|
||||
def import_single_order(order, id_pol: int = None, id_sectie: int = None) -> dict:
|
||||
def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None) -> dict:
|
||||
"""Import a single order into Oracle ROA.
|
||||
|
||||
Returns dict with:
|
||||
@@ -203,7 +229,7 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None) -> dic
|
||||
result["id_adresa_livrare"] = int(addr_livr_id)
|
||||
|
||||
# Step 4: Build articles JSON and import order
|
||||
articles_json = build_articles_json(order.items)
|
||||
articles_json = build_articles_json(order.items, order, app_settings)
|
||||
|
||||
# Use CLOB for the JSON
|
||||
clob_var = cur.var(oracledb.DB_TYPE_CLOB)
|
||||
|
||||
@@ -145,8 +145,11 @@ def get_mappings(search: str = "", page: int = 1, per_page: int = 50,
|
||||
"counts": counts,
|
||||
}
|
||||
|
||||
def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret: float = 100):
|
||||
"""Create a new mapping. Returns dict or raises HTTPException on duplicate."""
|
||||
def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret: float = 100, auto_restore: bool = False):
|
||||
"""Create a new mapping. Returns dict or raises HTTPException on duplicate.
|
||||
|
||||
When auto_restore=True, soft-deleted records are restored+updated instead of raising 409.
|
||||
"""
|
||||
if not sku or not sku.strip():
|
||||
raise HTTPException(status_code=400, detail="SKU este obligatoriu")
|
||||
if not codmat or not codmat.strip():
|
||||
@@ -170,11 +173,22 @@ def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret
|
||||
WHERE sku = :sku AND codmat = :codmat AND sters = 1
|
||||
""", {"sku": sku, "codmat": codmat})
|
||||
if cur.fetchone()[0] > 0:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Maparea a fost ștearsă anterior",
|
||||
headers={"X-Can-Restore": "true"}
|
||||
)
|
||||
if auto_restore:
|
||||
cur.execute("""
|
||||
UPDATE ARTICOLE_TERTI SET sters = 0, activ = 1,
|
||||
cantitate_roa = :cantitate_roa, procent_pret = :procent_pret,
|
||||
data_modif = SYSDATE
|
||||
WHERE sku = :sku AND codmat = :codmat AND sters = 1
|
||||
""", {"sku": sku, "codmat": codmat,
|
||||
"cantitate_roa": cantitate_roa, "procent_pret": procent_pret})
|
||||
conn.commit()
|
||||
return {"sku": sku, "codmat": codmat}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Maparea a fost ștearsă anterior",
|
||||
headers={"X-Can-Restore": "true"}
|
||||
)
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO ARTICOLE_TERTI (sku, codmat, cantitate_roa, procent_pret, activ, sters, data_creare, id_util_creare)
|
||||
|
||||
@@ -55,6 +55,8 @@ class OrderData:
|
||||
billing: OrderBilling = field(default_factory=OrderBilling)
|
||||
shipping: Optional[OrderShipping] = None
|
||||
total: float = 0.0
|
||||
delivery_cost: float = 0.0
|
||||
discount_total: float = 0.0
|
||||
payment_name: str = ""
|
||||
delivery_name: str = ""
|
||||
source_file: str = ""
|
||||
@@ -155,6 +157,15 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
|
||||
payment = data.get("payment", {}) or {}
|
||||
delivery = data.get("delivery", {}) or {}
|
||||
|
||||
# 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)
|
||||
discount_total = 0.0
|
||||
for d in data.get("discounts", []):
|
||||
if isinstance(d, dict):
|
||||
discount_total += float(d.get("value", 0) or 0)
|
||||
|
||||
return OrderData(
|
||||
id=str(data.get("id", order_id)),
|
||||
number=str(data.get("number", "")),
|
||||
@@ -165,6 +176,8 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
|
||||
billing=billing,
|
||||
shipping=shipping,
|
||||
total=float(data.get("total", 0) or 0),
|
||||
delivery_cost=delivery_cost,
|
||||
discount_total=discount_total,
|
||||
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
|
||||
|
||||
@@ -51,7 +51,8 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
||||
missing_skus: list = None, items_count: int = 0,
|
||||
shipping_name: str = None, billing_name: str = None,
|
||||
payment_method: str = None, delivery_method: str = None,
|
||||
order_total: float = None):
|
||||
order_total: float = None,
|
||||
delivery_cost: float = None, discount_total: float = None):
|
||||
"""Upsert a single order — one row per order_number, status updated in place."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
@@ -60,8 +61,9 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
||||
(order_number, order_date, customer_name, status,
|
||||
id_comanda, id_partener, error_message, missing_skus, items_count,
|
||||
last_sync_run_id, shipping_name, billing_name,
|
||||
payment_method, delivery_method, order_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
payment_method, delivery_method, order_total,
|
||||
delivery_cost, discount_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(order_number) DO UPDATE SET
|
||||
status = CASE
|
||||
WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED'
|
||||
@@ -82,12 +84,15 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
||||
payment_method = COALESCE(excluded.payment_method, orders.payment_method),
|
||||
delivery_method = COALESCE(excluded.delivery_method, orders.delivery_method),
|
||||
order_total = COALESCE(excluded.order_total, orders.order_total),
|
||||
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
|
||||
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
|
||||
updated_at = datetime('now')
|
||||
""", (order_number, order_date, customer_name, status,
|
||||
id_comanda, id_partener, error_message,
|
||||
json.dumps(missing_skus) if missing_skus else None,
|
||||
items_count, sync_run_id, shipping_name, billing_name,
|
||||
payment_method, delivery_method, order_total))
|
||||
payment_method, delivery_method, order_total,
|
||||
delivery_cost, discount_total))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
@@ -112,7 +117,7 @@ async def save_orders_batch(orders_data: list[dict]):
|
||||
Each dict must have: sync_run_id, order_number, order_date, customer_name, status,
|
||||
id_comanda, id_partener, error_message, missing_skus (list|None), items_count,
|
||||
shipping_name, billing_name, payment_method, delivery_method, status_at_run,
|
||||
items (list of item dicts).
|
||||
items (list of item dicts), delivery_cost (optional), discount_total (optional).
|
||||
"""
|
||||
if not orders_data:
|
||||
return
|
||||
@@ -124,8 +129,9 @@ async def save_orders_batch(orders_data: list[dict]):
|
||||
(order_number, order_date, customer_name, status,
|
||||
id_comanda, id_partener, error_message, missing_skus, items_count,
|
||||
last_sync_run_id, shipping_name, billing_name,
|
||||
payment_method, delivery_method, order_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
payment_method, delivery_method, order_total,
|
||||
delivery_cost, discount_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(order_number) DO UPDATE SET
|
||||
status = CASE
|
||||
WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED'
|
||||
@@ -146,6 +152,8 @@ async def save_orders_batch(orders_data: list[dict]):
|
||||
payment_method = COALESCE(excluded.payment_method, orders.payment_method),
|
||||
delivery_method = COALESCE(excluded.delivery_method, orders.delivery_method),
|
||||
order_total = COALESCE(excluded.order_total, orders.order_total),
|
||||
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
|
||||
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
|
||||
updated_at = datetime('now')
|
||||
""", [
|
||||
(d["order_number"], d["order_date"], d["customer_name"], d["status"],
|
||||
@@ -154,7 +162,8 @@ async def save_orders_batch(orders_data: list[dict]):
|
||||
d.get("items_count", 0), d["sync_run_id"],
|
||||
d.get("shipping_name"), d.get("billing_name"),
|
||||
d.get("payment_method"), d.get("delivery_method"),
|
||||
d.get("order_total"))
|
||||
d.get("order_total"),
|
||||
d.get("delivery_cost"), d.get("discount_total"))
|
||||
for d in orders_data
|
||||
])
|
||||
|
||||
@@ -768,3 +777,29 @@ async def update_order_invoice(order_number: str, serie: str = None,
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
# ── App Settings ─────────────────────────────────
|
||||
|
||||
async def get_app_settings() -> dict:
|
||||
"""Get all app settings as a dict."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
cursor = await db.execute("SELECT key, value FROM app_settings")
|
||||
rows = await cursor.fetchall()
|
||||
return {row["key"]: row["value"] for row in rows}
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def set_app_setting(key: str, value: str):
|
||||
"""Set a single app setting value."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
INSERT OR REPLACE INTO app_settings (key, value)
|
||||
VALUES (?, ?)
|
||||
""", (key, value))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
@@ -287,6 +287,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
"shipping_name": shipping_name, "billing_name": billing_name,
|
||||
"payment_method": payment_method, "delivery_method": delivery_method,
|
||||
"order_total": order.total or None,
|
||||
"delivery_cost": order.delivery_cost or None,
|
||||
"discount_total": order.discount_total or None,
|
||||
"items": order_items_data,
|
||||
})
|
||||
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → DEJA IMPORTAT (ID: {id_comanda_roa})")
|
||||
@@ -315,6 +317,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
"shipping_name": shipping_name, "billing_name": billing_name,
|
||||
"payment_method": payment_method, "delivery_method": delivery_method,
|
||||
"order_total": order.total or None,
|
||||
"delivery_cost": order.delivery_cost or None,
|
||||
"discount_total": order.discount_total or None,
|
||||
"items": order_items_data,
|
||||
})
|
||||
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → OMIS (lipsa: {', '.join(missing_skus)})")
|
||||
@@ -327,6 +331,9 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
imported_count = 0
|
||||
error_count = 0
|
||||
|
||||
# Load app settings for transport/discount CODMAT config
|
||||
app_settings = await sqlite_service.get_app_settings()
|
||||
|
||||
for i, order in enumerate(truly_importable):
|
||||
shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order)
|
||||
|
||||
@@ -338,7 +345,8 @@ 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
|
||||
order, id_pol=id_pol, id_sectie=id_sectie,
|
||||
app_settings=app_settings
|
||||
)
|
||||
|
||||
# Build order items data for storage (R9)
|
||||
@@ -368,6 +376,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
payment_method=payment_method,
|
||||
delivery_method=delivery_method,
|
||||
order_total=order.total or None,
|
||||
delivery_cost=order.delivery_cost or None,
|
||||
discount_total=order.discount_total or None,
|
||||
)
|
||||
await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED")
|
||||
# Store ROA address IDs (R9)
|
||||
@@ -394,6 +404,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
payment_method=payment_method,
|
||||
delivery_method=delivery_method,
|
||||
order_total=order.total or None,
|
||||
delivery_cost=order.delivery_cost or None,
|
||||
discount_total=order.discount_total or None,
|
||||
)
|
||||
await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR")
|
||||
await sqlite_service.add_order_items(order.number, order_items_data)
|
||||
|
||||
@@ -313,10 +313,10 @@ async function loadDashOrders() {
|
||||
<td>${dateStr}</td>
|
||||
${renderClientCell(o)}
|
||||
<td>${o.items_count || 0}</td>
|
||||
<td class="text-end">${orderTotal}</td>
|
||||
<td class="text-nowrap">${statusDot(o.status)} ${statusLabelText(o.status)}</td>
|
||||
<td>${o.id_comanda || '-'}</td>
|
||||
<td>${invoiceBadge}</td>
|
||||
<td>${orderTotal}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -479,6 +479,10 @@ async function openDashOrderDetail(orderNumber) {
|
||||
if (detailItemsTotal) detailItemsTotal.textContent = '-';
|
||||
const detailOrderTotal = document.getElementById('detailOrderTotal');
|
||||
if (detailOrderTotal) detailOrderTotal.textContent = '-';
|
||||
const deliveryWrap = document.getElementById('detailDeliveryWrap');
|
||||
if (deliveryWrap) deliveryWrap.style.display = 'none';
|
||||
const discountWrap = document.getElementById('detailDiscountWrap');
|
||||
if (discountWrap) discountWrap.style.display = 'none';
|
||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||
if (mobileContainer) mobileContainer.innerHTML = '';
|
||||
|
||||
@@ -510,6 +514,22 @@ async function openDashOrderDetail(orderNumber) {
|
||||
document.getElementById('detailError').style.display = '';
|
||||
}
|
||||
|
||||
// Show delivery cost
|
||||
const dlvWrap = document.getElementById('detailDeliveryWrap');
|
||||
const dlvEl = document.getElementById('detailDeliveryCost');
|
||||
if (order.delivery_cost && Number(order.delivery_cost) > 0) {
|
||||
if (dlvEl) dlvEl.textContent = Number(order.delivery_cost).toFixed(2) + ' lei';
|
||||
if (dlvWrap) dlvWrap.style.display = '';
|
||||
}
|
||||
|
||||
// Show discount
|
||||
const dscWrap = document.getElementById('detailDiscountWrap');
|
||||
const dscEl = document.getElementById('detailDiscount');
|
||||
if (order.discount_total && Number(order.discount_total) > 0) {
|
||||
if (dscEl) dscEl.textContent = '-' + Number(order.discount_total).toFixed(2) + ' lei';
|
||||
if (dscWrap) dscWrap.style.display = '';
|
||||
}
|
||||
|
||||
const items = data.items || [];
|
||||
if (items.length === 0) {
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center text-muted">Niciun articol</td></tr>';
|
||||
@@ -709,3 +729,81 @@ async function saveQuickMapping() {
|
||||
alert('Eroare: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── App Settings ─────────────────────────────────
|
||||
|
||||
let settAcTimeout = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadAppSettings();
|
||||
wireSettingsAutocomplete('settTransportCodmat', 'settTransportAc');
|
||||
wireSettingsAutocomplete('settDiscountCodmat', 'settDiscountAc');
|
||||
});
|
||||
|
||||
async function loadAppSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/settings');
|
||||
const data = await res.json();
|
||||
const el = (id) => document.getElementById(id);
|
||||
if (el('settTransportCodmat')) el('settTransportCodmat').value = data.transport_codmat || '';
|
||||
if (el('settTransportVat')) el('settTransportVat').value = data.transport_vat || '21';
|
||||
if (el('settDiscountCodmat')) el('settDiscountCodmat').value = data.discount_codmat || '';
|
||||
} catch (err) {
|
||||
console.error('loadAppSettings error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAppSettings() {
|
||||
const transport_codmat = document.getElementById('settTransportCodmat')?.value?.trim() || '';
|
||||
const transport_vat = document.getElementById('settTransportVat')?.value || '21';
|
||||
const discount_codmat = document.getElementById('settDiscountCodmat')?.value?.trim() || '';
|
||||
try {
|
||||
const res = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ transport_codmat, transport_vat, discount_codmat })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
alert('Setari salvate!');
|
||||
} else {
|
||||
alert('Eroare: ' + JSON.stringify(data));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Eroare salvare setari: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function wireSettingsAutocomplete(inputId, dropdownId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const dropdown = document.getElementById(dropdownId);
|
||||
if (!input || !dropdown) return;
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(settAcTimeout);
|
||||
settAcTimeout = setTimeout(async () => {
|
||||
const q = input.value.trim();
|
||||
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
||||
try {
|
||||
const res = await fetch(`/api/articles/search?q=${encodeURIComponent(q)}`);
|
||||
const data = await res.json();
|
||||
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
||||
dropdown.innerHTML = data.results.map(r =>
|
||||
`<div class="autocomplete-item" onmousedown="settSelectArticle('${inputId}', '${dropdownId}', '${esc(r.codmat)}')">
|
||||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
dropdown.classList.remove('d-none');
|
||||
} catch { dropdown.classList.add('d-none'); }
|
||||
}, 250);
|
||||
});
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||
});
|
||||
}
|
||||
|
||||
function settSelectArticle(inputId, dropdownId, codmat) {
|
||||
document.getElementById(inputId).value = codmat;
|
||||
document.getElementById(dropdownId).classList.add('d-none');
|
||||
}
|
||||
|
||||
@@ -162,8 +162,8 @@ async function loadRunOrders(runId, statusFilter, page) {
|
||||
<td><code>${esc(o.order_number)}</code></td>
|
||||
<td>${esc(o.customer_name)}</td>
|
||||
<td>${o.items_count || 0}</td>
|
||||
<td class="text-end">${orderTotal}</td>
|
||||
<td class="text-nowrap">${statusDot(o.status)} ${logStatusText(o.status)}</td>
|
||||
<td>${orderTotal}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -324,6 +324,10 @@ async function openOrderDetail(orderNumber) {
|
||||
if (detailItemsTotal) detailItemsTotal.textContent = '-';
|
||||
const detailOrderTotal = document.getElementById('detailOrderTotal');
|
||||
if (detailOrderTotal) detailOrderTotal.textContent = '-';
|
||||
const deliveryWrap = document.getElementById('detailDeliveryWrap');
|
||||
if (deliveryWrap) deliveryWrap.style.display = 'none';
|
||||
const discountWrap = document.getElementById('detailDiscountWrap');
|
||||
if (discountWrap) discountWrap.style.display = 'none';
|
||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||
if (mobileContainer) mobileContainer.innerHTML = '';
|
||||
|
||||
@@ -355,6 +359,22 @@ async function openOrderDetail(orderNumber) {
|
||||
document.getElementById('detailError').style.display = '';
|
||||
}
|
||||
|
||||
// Show delivery cost
|
||||
const dlvWrap = document.getElementById('detailDeliveryWrap');
|
||||
const dlvEl = document.getElementById('detailDeliveryCost');
|
||||
if (order.delivery_cost && Number(order.delivery_cost) > 0) {
|
||||
if (dlvEl) dlvEl.textContent = Number(order.delivery_cost).toFixed(2) + ' lei';
|
||||
if (dlvWrap) dlvWrap.style.display = '';
|
||||
}
|
||||
|
||||
// Show discount
|
||||
const dscWrap = document.getElementById('detailDiscountWrap');
|
||||
const dscEl = document.getElementById('detailDiscount');
|
||||
if (order.discount_total && Number(order.discount_total) > 0) {
|
||||
if (dscEl) dscEl.textContent = '-' + Number(order.discount_total).toFixed(2) + ' lei';
|
||||
if (dscWrap) dscWrap.style.display = '';
|
||||
}
|
||||
|
||||
const items = data.items || [];
|
||||
if (items.length === 0) {
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center text-muted">Niciun articol</td></tr>';
|
||||
|
||||
@@ -280,7 +280,7 @@ async function openEditModal(sku, codmat, cantitate, procent) {
|
||||
editingMapping = { sku, codmat };
|
||||
document.getElementById('addModalTitle').textContent = 'Editare Mapare';
|
||||
document.getElementById('inputSku').value = sku;
|
||||
document.getElementById('inputSku').readOnly = true;
|
||||
document.getElementById('inputSku').readOnly = false;
|
||||
document.getElementById('pctWarning').style.display = 'none';
|
||||
|
||||
const container = document.getElementById('codmatLines');
|
||||
@@ -292,6 +292,15 @@ async function openEditModal(sku, codmat, cantitate, procent) {
|
||||
const data = await res.json();
|
||||
const allMappings = (data.mappings || []).filter(m => m.sku === sku && !m.sters);
|
||||
|
||||
// Show product name if available
|
||||
const productName = allMappings[0]?.product_name || '';
|
||||
const productNameEl = document.getElementById('addModalProductName');
|
||||
const productNameText = document.getElementById('inputProductName');
|
||||
if (productName && productNameEl && productNameText) {
|
||||
productNameText.textContent = productName;
|
||||
productNameEl.style.display = '';
|
||||
}
|
||||
|
||||
if (allMappings.length === 0) {
|
||||
// Fallback to single line with passed values
|
||||
addCodmatLine();
|
||||
@@ -307,6 +316,9 @@ async function openEditModal(sku, codmat, cantitate, procent) {
|
||||
const lines = container.querySelectorAll('.codmat-line');
|
||||
const line = lines[lines.length - 1];
|
||||
line.querySelector('.cl-codmat').value = m.codmat;
|
||||
if (m.denumire) {
|
||||
line.querySelector('.cl-selected').textContent = m.denumire;
|
||||
}
|
||||
line.querySelector('.cl-cantitate').value = m.cantitate_roa;
|
||||
line.querySelector('.cl-procent').value = m.procent_pret;
|
||||
}
|
||||
@@ -436,22 +448,23 @@ async function saveMapping() {
|
||||
});
|
||||
} else {
|
||||
// Multi-CODMAT set: delete all existing then create new batch
|
||||
const existRes = await fetch(`/api/mappings?search=${encodeURIComponent(editingMapping.sku)}&per_page=100`);
|
||||
const oldSku = editingMapping.sku;
|
||||
const existRes = await fetch(`/api/mappings?search=${encodeURIComponent(oldSku)}&per_page=100`);
|
||||
const existData = await existRes.json();
|
||||
const existing = (existData.mappings || []).filter(m => m.sku === editingMapping.sku && !m.sters);
|
||||
const existing = (existData.mappings || []).filter(m => m.sku === oldSku && !m.sters);
|
||||
|
||||
// Delete each existing CODMAT
|
||||
// Delete each existing CODMAT for old SKU
|
||||
for (const m of existing) {
|
||||
await fetch(`/api/mappings/${encodeURIComponent(m.sku)}/${encodeURIComponent(m.codmat)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// Create new batch
|
||||
// Create new batch with auto_restore (handles just-soft-deleted records)
|
||||
res = await fetch('/api/mappings/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku, mappings })
|
||||
body: JSON.stringify({ sku, mappings, auto_restore: true })
|
||||
});
|
||||
}
|
||||
} else if (mappings.length === 1) {
|
||||
|
||||
@@ -6,17 +6,19 @@
|
||||
<title>{% block title %}GoMag Import Manager{% endblock %}</title>
|
||||
<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">
|
||||
<link href="/static/css/style.css?v=5" rel="stylesheet">
|
||||
<link href="{{ request.url_for('static', path='css/style.css') }}?v=8" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Top Navbar -->
|
||||
{% set rp = request.scope.get('root_path', '') %}
|
||||
<nav class="top-navbar">
|
||||
<div class="navbar-brand">GoMag Import</div>
|
||||
<div class="navbar-links">
|
||||
<a href="/" class="nav-tab {% block nav_dashboard %}{% endblock %}"><span class="d-none d-md-inline">Dashboard</span><span class="d-md-none">Acasa</span></a>
|
||||
<a href="/mappings" class="nav-tab {% block nav_mappings %}{% endblock %}"><span class="d-none d-md-inline">Mapari SKU</span><span class="d-md-none">Mapari</span></a>
|
||||
<a href="/missing-skus" class="nav-tab {% block nav_missing %}{% endblock %}"><span class="d-none d-md-inline">SKU-uri Lipsa</span><span class="d-md-none">Lipsa</span></a>
|
||||
<a href="/logs" class="nav-tab {% block nav_logs %}{% endblock %}"><span class="d-none d-md-inline">Jurnale Import</span><span class="d-md-none">Jurnale</span></a>
|
||||
<a href="{{ rp }}/" class="nav-tab {% block nav_dashboard %}{% endblock %}"><span class="d-none d-md-inline">Dashboard</span><span class="d-md-none">Acasa</span></a>
|
||||
<a href="{{ rp }}/mappings" class="nav-tab {% block nav_mappings %}{% endblock %}"><span class="d-none d-md-inline">Mapari SKU</span><span class="d-md-none">Mapari</span></a>
|
||||
<a href="{{ rp }}/missing-skus" class="nav-tab {% block nav_missing %}{% endblock %}"><span class="d-none d-md-inline">SKU-uri Lipsa</span><span class="d-md-none">Lipsa</span></a>
|
||||
<a href="{{ rp }}/logs" class="nav-tab {% block nav_logs %}{% endblock %}"><span class="d-none d-md-inline">Jurnale Import</span><span class="d-md-none">Jurnale</span></a>
|
||||
<a href="{{ rp }}/settings" class="nav-tab {% block nav_settings %}{% endblock %}"><span class="d-none d-md-inline">Setari</span><span class="d-md-none">Setari</span></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -26,7 +28,7 @@
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/shared.js?v=5"></script>
|
||||
<script src="{{ request.url_for('static', path='js/shared.js') }}?v=8"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -67,7 +67,8 @@
|
||||
<button class="filter-pill d-none d-md-inline-flex" data-status="IMPORTED">Importat <span class="filter-count fc-green" id="cntImp">0</span></button>
|
||||
<button class="filter-pill d-none d-md-inline-flex" data-status="SKIPPED">Omise <span class="filter-count fc-yellow" id="cntSkip">0</span></button>
|
||||
<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="UNINVOICED">Nefact. <span class="filter-count fc-neutral" id="cntNef">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>
|
||||
<!-- Search (integrated, end of row) -->
|
||||
<input type="search" id="orderSearch" placeholder="Cauta..." class="search-input">
|
||||
</div>
|
||||
@@ -80,18 +81,19 @@
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" onclick="dashSortBy('order_number')">Nr Comanda <span class="sort-icon" data-col="order_number"></span></th>
|
||||
<th style="width:24px"></th>
|
||||
<th class="sortable" onclick="dashSortBy('order_date')">Data <span class="sort-icon" data-col="order_date"></span></th>
|
||||
<th class="sortable" onclick="dashSortBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th>
|
||||
<th class="sortable" onclick="dashSortBy('order_number')">Nr Comanda <span class="sort-icon" data-col="order_number"></span></th>
|
||||
<th class="sortable" onclick="dashSortBy('items_count')">Art. <span class="sort-icon" data-col="items_count"></span></th>
|
||||
<th class="sortable" onclick="dashSortBy('status')">Status Import <span class="sort-icon" data-col="status"></span></th>
|
||||
<th>ID ROA</th>
|
||||
<th>Factura</th>
|
||||
<th>Total</th>
|
||||
<th class="text-end">Transport</th>
|
||||
<th class="text-end">Discount</th>
|
||||
<th class="text-end">Total</th>
|
||||
<th style="width:28px" title="Facturat">F</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dashOrdersBody">
|
||||
<tr><td colspan="8" class="text-center text-muted py-3">Se incarca...</td></tr>
|
||||
<tr><td colspan="9" class="text-center text-muted py-3">Se incarca...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -121,9 +123,11 @@
|
||||
<small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-3 mb-2" id="detailTotals">
|
||||
<span><small class="text-muted">Valoare articole:</small> <strong id="detailItemsTotal">-</strong></span>
|
||||
<span><small class="text-muted">Total comanda:</small> <strong id="detailOrderTotal">-</strong></span>
|
||||
<div id="detailTotals" class="d-flex gap-3 mb-2 flex-wrap" style="font-size:0.875rem">
|
||||
<span><small class="text-muted">Valoare:</small> <strong id="detailItemsTotal">-</strong></span>
|
||||
<span id="detailDiscountWrap"><small class="text-muted">Discount:</small> <strong id="detailDiscount">-</strong></span>
|
||||
<span id="detailDeliveryWrap"><small class="text-muted">Transport:</small> <strong id="detailDeliveryCost">-</strong></span>
|
||||
<span><small class="text-muted">Total:</small> <strong id="detailOrderTotal">-</strong></span>
|
||||
</div>
|
||||
<div class="table-responsive d-none d-md-block">
|
||||
<table class="table table-sm table-bordered mb-0">
|
||||
@@ -131,12 +135,10 @@
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Produs</th>
|
||||
<th>CODMAT</th>
|
||||
<th>Cant.</th>
|
||||
<th>Pret</th>
|
||||
<th>TVA</th>
|
||||
<th>CODMAT</th>
|
||||
<th>Status</th>
|
||||
<th>Actiune</th>
|
||||
<th class="text-end">Valoare</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="detailItemsBody">
|
||||
@@ -183,5 +185,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/dashboard.js?v=6"></script>
|
||||
<script src="{{ request.url_for('static', path='js/dashboard.js') }}?v=9"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -67,17 +67,19 @@
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:24px"></th>
|
||||
<th>#</th>
|
||||
<th class="sortable" onclick="sortOrdersBy('order_date')">Data comanda <span class="sort-icon" data-col="order_date"></span></th>
|
||||
<th class="sortable" onclick="sortOrdersBy('order_number')">Nr. comanda <span class="sort-icon" data-col="order_number"></span></th>
|
||||
<th class="sortable" onclick="sortOrdersBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th>
|
||||
<th class="sortable" onclick="sortOrdersBy('items_count')">Articole <span class="sort-icon" data-col="items_count"></span></th>
|
||||
<th class="sortable" onclick="sortOrdersBy('status')">Status <span class="sort-icon" data-col="status"></span></th>
|
||||
<th>Total</th>
|
||||
<th class="text-end">Transport</th>
|
||||
<th class="text-end">Discount</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="runOrdersBody">
|
||||
<tr><td colspan="7" class="text-center text-muted py-3">Selecteaza un sync run</td></tr>
|
||||
<tr><td colspan="9" class="text-center text-muted py-3">Selecteaza un sync run</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -116,9 +118,11 @@
|
||||
<small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-3 mb-2" id="detailTotals">
|
||||
<span><small class="text-muted">Valoare articole:</small> <strong id="detailItemsTotal">-</strong></span>
|
||||
<span><small class="text-muted">Total comanda:</small> <strong id="detailOrderTotal">-</strong></span>
|
||||
<div id="detailTotals" class="d-flex gap-3 mb-2 flex-wrap" style="font-size:0.875rem">
|
||||
<span><small class="text-muted">Valoare:</small> <strong id="detailItemsTotal">-</strong></span>
|
||||
<span id="detailDiscountWrap"><small class="text-muted">Discount:</small> <strong id="detailDiscount">-</strong></span>
|
||||
<span id="detailDeliveryWrap"><small class="text-muted">Transport:</small> <strong id="detailDeliveryCost">-</strong></span>
|
||||
<span><small class="text-muted">Total:</small> <strong id="detailOrderTotal">-</strong></span>
|
||||
</div>
|
||||
<div class="table-responsive d-none d-md-block">
|
||||
<table class="table table-sm table-bordered mb-0">
|
||||
@@ -126,12 +130,10 @@
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Produs</th>
|
||||
<th>CODMAT</th>
|
||||
<th>Cant.</th>
|
||||
<th>Pret</th>
|
||||
<th>TVA</th>
|
||||
<th>CODMAT</th>
|
||||
<th>Status</th>
|
||||
<th>Actiune</th>
|
||||
<th class="text-end">Valoare</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="detailItemsBody">
|
||||
@@ -181,5 +183,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/logs.js?v=6"></script>
|
||||
<script src="{{ request.url_for('static', path='js/logs.js') }}?v=9"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -154,5 +154,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/mappings.js?v=5"></script>
|
||||
<script src="{{ request.url_for('static', path='js/mappings.js') }}?v=7"></script>
|
||||
{% endblock %}
|
||||
|
||||
98
api/app/templates/settings.html
Normal file
98
api/app/templates/settings.html
Normal file
@@ -0,0 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Setari - GoMag Import{% endblock %}
|
||||
{% block nav_settings %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4">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>
|
||||
<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="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>
|
||||
<select class="form-select form-select-sm" id="settTransportVat">
|
||||
<option value="5">5%</option>
|
||||
<option value="9">9%</option>
|
||||
<option value="19">19%</option>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
<select class="form-select form-select-sm" id="settDiscountVat">
|
||||
<option value="5">5%</option>
|
||||
<option value="9">9%</option>
|
||||
<option value="19" selected>19%</option>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.url_for('static', path='js/settings.js') }}?v=1"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user