feat(orders): add resync and delete order buttons
Resync soft-deletes from Oracle then re-imports from GoMag with fresh article data. Delete soft-deletes and marks DELETED_IN_ROA. Both have invoice safety gates (refuse if invoiced or Oracle unavailable). UI: split modal footer (Delete left, Resync+Close right), inline confirm pattern (no native confirm()), dashboard row hover action icons, disabled+tooltip for invoiced orders. 8 unit tests for safety gates and happy paths. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,37 +7,13 @@ from datetime import datetime, timedelta
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def retry_single_order(order_number: str, app_settings: dict) -> dict:
|
||||
"""Re-download and re-import a single order from GoMag.
|
||||
|
||||
Steps:
|
||||
1. Read order from SQLite to get order_date / customer_name
|
||||
2. Check sync lock (no retry during active sync)
|
||||
3. Download narrow date range from GoMag (order_date ± 1 day)
|
||||
4. Find the specific order in downloaded data
|
||||
5. Run import_single_order()
|
||||
6. Update status in SQLite
|
||||
async def _download_and_reimport(order_number: str, order_date_str: str, customer_name: str, app_settings: dict) -> dict:
|
||||
"""Download order from GoMag and re-import it into Oracle.
|
||||
|
||||
Does NOT check status guard — caller is responsible.
|
||||
Returns: {"success": bool, "message": str, "status": str|None}
|
||||
"""
|
||||
from . import sqlite_service, sync_service, gomag_client, import_service, order_reader
|
||||
|
||||
# Check sync lock
|
||||
if sync_service._sync_lock.locked():
|
||||
return {"success": False, "message": "Sync in curs — asteapta finalizarea"}
|
||||
|
||||
# Get order from SQLite
|
||||
detail = await sqlite_service.get_order_detail(order_number)
|
||||
if not detail:
|
||||
return {"success": False, "message": "Comanda nu a fost gasita"}
|
||||
|
||||
order_data = detail["order"]
|
||||
status = order_data.get("status", "")
|
||||
if status not in ("ERROR", "SKIPPED"):
|
||||
return {"success": False, "message": f"Retry permis doar pentru ERROR/SKIPPED (status actual: {status})"}
|
||||
|
||||
order_date_str = order_data.get("order_date", "")
|
||||
customer_name = order_data.get("customer_name", "")
|
||||
from . import sqlite_service, gomag_client, import_service, order_reader
|
||||
|
||||
# Parse order date for narrow download window
|
||||
try:
|
||||
@@ -129,3 +105,180 @@ async def retry_single_order(order_number: str, app_settings: dict) -> dict:
|
||||
error_message=f"Retry: {error}",
|
||||
)
|
||||
return {"success": False, "message": f"Import esuat: {error}", "status": "ERROR"}
|
||||
|
||||
|
||||
async def retry_single_order(order_number: str, app_settings: dict) -> dict:
|
||||
"""Re-download and re-import a single order from GoMag.
|
||||
|
||||
Steps:
|
||||
1. Read order from SQLite to get order_date / customer_name
|
||||
2. Check sync lock (no retry during active sync)
|
||||
3. Download narrow date range from GoMag (order_date ± 1 day)
|
||||
4. Find the specific order in downloaded data
|
||||
5. Run import_single_order()
|
||||
6. Update status in SQLite
|
||||
|
||||
Returns: {"success": bool, "message": str, "status": str|None}
|
||||
"""
|
||||
from . import sqlite_service, sync_service
|
||||
|
||||
# Check sync lock
|
||||
if sync_service._sync_lock.locked():
|
||||
return {"success": False, "message": "Sync in curs — asteapta finalizarea"}
|
||||
|
||||
# Get order from SQLite
|
||||
detail = await sqlite_service.get_order_detail(order_number)
|
||||
if not detail:
|
||||
return {"success": False, "message": "Comanda nu a fost gasita"}
|
||||
|
||||
order_data = detail["order"]
|
||||
status = order_data.get("status", "")
|
||||
if status not in ("ERROR", "SKIPPED", "DELETED_IN_ROA"):
|
||||
return {"success": False, "message": f"Retry permis doar pentru ERROR/SKIPPED/DELETED_IN_ROA (status actual: {status})"}
|
||||
|
||||
order_date_str = order_data.get("order_date", "")
|
||||
customer_name = order_data.get("customer_name", "")
|
||||
|
||||
return await _download_and_reimport(order_number, order_date_str, customer_name, app_settings)
|
||||
|
||||
|
||||
async def resync_single_order(order_number: str, app_settings: dict) -> dict:
|
||||
"""Soft-delete an imported order from Oracle then re-import it from GoMag.
|
||||
|
||||
Steps:
|
||||
1. Check sync lock
|
||||
2. Load order from SQLite
|
||||
3. Validate status is IMPORTED/ALREADY_IMPORTED with id_comanda
|
||||
4. Invoice safety gate (check Oracle for invoices)
|
||||
5. Soft-delete from Oracle
|
||||
6. Mark DELETED_IN_ROA in SQLite
|
||||
7. Re-import via _download_and_reimport
|
||||
|
||||
Returns: {"success": bool, "message": str, "status": str|None}
|
||||
"""
|
||||
from . import sqlite_service, sync_service, import_service, invoice_service
|
||||
from .. import database
|
||||
|
||||
# Check sync lock
|
||||
if sync_service._sync_lock.locked():
|
||||
return {"success": False, "message": "Sync in curs — asteapta finalizarea"}
|
||||
|
||||
# Get order from SQLite
|
||||
detail = await sqlite_service.get_order_detail(order_number)
|
||||
if not detail:
|
||||
return {"success": False, "message": "Comanda nu a fost gasita"}
|
||||
|
||||
order_data = detail["order"]
|
||||
status = order_data.get("status", "")
|
||||
id_comanda = order_data.get("id_comanda")
|
||||
|
||||
if status not in ("IMPORTED", "ALREADY_IMPORTED") or not id_comanda:
|
||||
return {"success": False, "message": f"Resync permis doar pentru IMPORTED/ALREADY_IMPORTED cu id_comanda (status actual: {status})"}
|
||||
|
||||
# Invoice safety gate
|
||||
if database.pool is None:
|
||||
return {"success": False, "message": "Oracle indisponibil"}
|
||||
|
||||
if order_data.get("factura_numar"):
|
||||
return {"success": False, "message": "Comanda este facturata"}
|
||||
|
||||
try:
|
||||
invoice_result = await asyncio.to_thread(
|
||||
invoice_service.check_invoices_for_orders, [id_comanda]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Invoice check failed for {order_number}: {e}")
|
||||
return {"success": False, "message": "Nu se poate verifica factura — Oracle indisponibil"}
|
||||
|
||||
if invoice_result.get(id_comanda):
|
||||
return {"success": False, "message": "Comanda este facturata"}
|
||||
|
||||
# Soft-delete from Oracle
|
||||
try:
|
||||
delete_result = await asyncio.to_thread(
|
||||
import_service.soft_delete_order_in_roa, id_comanda
|
||||
)
|
||||
if not delete_result.get("success"):
|
||||
return {"success": False, "message": f"Eroare stergere din Oracle: {delete_result.get('error', 'Unknown')}"}
|
||||
except Exception as e:
|
||||
logger.error(f"Soft-delete failed for {order_number} (id_comanda={id_comanda}): {e}")
|
||||
return {"success": False, "message": f"Eroare stergere din Oracle: {e}"}
|
||||
|
||||
# Mark deleted in SQLite
|
||||
await sqlite_service.mark_order_deleted_in_roa(order_number)
|
||||
|
||||
order_date_str = order_data.get("order_date", "")
|
||||
customer_name = order_data.get("customer_name", "")
|
||||
|
||||
# Re-import
|
||||
reimport_result = await _download_and_reimport(order_number, order_date_str, customer_name, app_settings)
|
||||
if not reimport_result.get("success"):
|
||||
logger.warning(f"Resync: order {order_number} deleted from Oracle but reimport failed")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Comanda stearsa din Oracle dar reimportul a esuat — foloseste Reimporta pentru a reincerca",
|
||||
}
|
||||
|
||||
return reimport_result
|
||||
|
||||
|
||||
async def delete_single_order(order_number: str) -> dict:
|
||||
"""Soft-delete an imported order from Oracle without re-importing.
|
||||
|
||||
Same invoice safety gate as resync_single_order.
|
||||
|
||||
Returns: {"success": bool, "message": str}
|
||||
"""
|
||||
from . import sqlite_service, sync_service, import_service, invoice_service
|
||||
from .. import database
|
||||
|
||||
# Check sync lock
|
||||
if sync_service._sync_lock.locked():
|
||||
return {"success": False, "message": "Sync in curs — asteapta finalizarea"}
|
||||
|
||||
# Get order from SQLite
|
||||
detail = await sqlite_service.get_order_detail(order_number)
|
||||
if not detail:
|
||||
return {"success": False, "message": "Comanda nu a fost gasita"}
|
||||
|
||||
order_data = detail["order"]
|
||||
status = order_data.get("status", "")
|
||||
id_comanda = order_data.get("id_comanda")
|
||||
|
||||
if status not in ("IMPORTED", "ALREADY_IMPORTED") or not id_comanda:
|
||||
return {"success": False, "message": f"Stergere permisa doar pentru IMPORTED/ALREADY_IMPORTED cu id_comanda (status actual: {status})"}
|
||||
|
||||
# Invoice safety gate
|
||||
if database.pool is None:
|
||||
return {"success": False, "message": "Oracle indisponibil"}
|
||||
|
||||
if order_data.get("factura_numar"):
|
||||
return {"success": False, "message": "Comanda este facturata"}
|
||||
|
||||
try:
|
||||
invoice_result = await asyncio.to_thread(
|
||||
invoice_service.check_invoices_for_orders, [id_comanda]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Invoice check failed for {order_number}: {e}")
|
||||
return {"success": False, "message": "Nu se poate verifica factura — Oracle indisponibil"}
|
||||
|
||||
if invoice_result.get(id_comanda):
|
||||
return {"success": False, "message": "Comanda este facturata"}
|
||||
|
||||
# Soft-delete from Oracle
|
||||
try:
|
||||
delete_result = await asyncio.to_thread(
|
||||
import_service.soft_delete_order_in_roa, id_comanda
|
||||
)
|
||||
if not delete_result.get("success"):
|
||||
return {"success": False, "message": f"Eroare stergere din Oracle: {delete_result.get('error', 'Unknown')}"}
|
||||
except Exception as e:
|
||||
logger.error(f"Soft-delete failed for {order_number} (id_comanda={id_comanda}): {e}")
|
||||
return {"success": False, "message": f"Eroare stergere din Oracle: {e}"}
|
||||
|
||||
# Mark deleted in SQLite
|
||||
await sqlite_service.mark_order_deleted_in_roa(order_number)
|
||||
|
||||
logger.info(f"Order {order_number} (id_comanda={id_comanda}) deleted from ROA")
|
||||
return {"success": True, "message": "Comanda stearsa din ROA"}
|
||||
|
||||
Reference in New Issue
Block a user