feat(loads): 2h import timeout + forbid loading more than one year

- Raise purchase/sales/VAT bulk-import job timeout 1200s -> 7200s (2h), matching
  the reference/items/units loaders. AMAS and Company are unchanged.
- Add utils.exceeds_one_year() (parses both 'YYYY-MM-DD' and 'DD-MM-YYYY HH:MM')
  and reject date ranges spanning over one year on every e-taxes load except
  Company data: server-side in the reference/items/units/VAT entry points, and
  client-side in all 7 load dialogs for immediate feedback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-06-17 09:45:01 +00:00
parent 93c6f13325
commit c29ac5bd1b
11 changed files with 74 additions and 5 deletions

View File

@ -12,7 +12,7 @@ import time
# Импорты из модуля аутентификации # Импорты из модуля аутентификации
from invoice_az.auth import record_etaxes_activity, get_default_asan_login from invoice_az.auth import record_etaxes_activity, get_default_asan_login
from invoice_az import background_tasks from invoice_az import background_tasks
from .utils import resolve_customer_group from .utils import resolve_customer_group, exceeds_one_year
from .sales_api import get_sales_invoices from .sales_api import get_sales_invoices
@ -4389,7 +4389,7 @@ def import_bulk_purchase_invoices(invoice_ids, token, warehouse=None):
user=user, user=user,
bg_job_id=job_id, bg_job_id=job_id,
queue="default", queue="default",
timeout=1200, timeout=7200,
) )
return {"success": True, "enqueued": True, "total": len(invoice_ids), "job_id": job_id} return {"success": True, "enqueued": True, "total": len(invoice_ids), "job_id": job_id}
@ -4530,6 +4530,8 @@ def start_reference_data_loading_bg(date_from, date_to, load_items=0,
return {"success": False, "message": "Invalid date range."} return {"success": False, "message": "Invalid date range."}
if df > dt: if df > dt:
return {"success": False, "message": "Date From cannot be later than Date To."} return {"success": False, "message": "Date From cannot be later than Date To."}
if exceeds_one_year(date_from, date_to):
return {"success": False, "message": "Date range cannot exceed one year."}
date_from_str = df.strftime("%d-%m-%Y 00:00") date_from_str = df.strftime("%d-%m-%Y 00:00")
date_to_str = dt.strftime("%d-%m-%Y 23:59") date_to_str = dt.strftime("%d-%m-%Y 23:59")
@ -4750,6 +4752,8 @@ def start_items_loading_bg(date_from, date_to):
record_etaxes_activity() record_etaxes_activity()
if not date_from or not date_to: if not date_from or not date_to:
return {"success": False, "message": "Date range required."} return {"success": False, "message": "Date range required."}
if exceeds_one_year(date_from, date_to):
return {"success": False, "message": "Date range cannot exceed one year."}
job_id = background_tasks.new_job_id() job_id = background_tasks.new_job_id()
frappe.enqueue( frappe.enqueue(
_process_entity_loading, _process_entity_loading,
@ -4766,6 +4770,8 @@ def start_units_loading_bg(date_from, date_to):
record_etaxes_activity() record_etaxes_activity()
if not date_from or not date_to: if not date_from or not date_to:
return {"success": False, "message": "Date range required."} return {"success": False, "message": "Date range required."}
if exceeds_one_year(date_from, date_to):
return {"success": False, "message": "Date range cannot exceed one year."}
job_id = background_tasks.new_job_id() job_id = background_tasks.new_job_id()
frappe.enqueue( frappe.enqueue(
_process_entity_loading, _process_entity_loading,

View File

@ -689,6 +689,11 @@ function show_items_filter_dialog() {
return; return;
} }
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
frappe.msgprint(__('The date range cannot exceed one year.'));
return;
}
d.hide(); d.hide();
let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00'); let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00');

View File

@ -649,6 +649,11 @@ function show_units_filter_dialog() {
return; return;
} }
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
frappe.msgprint(__('The date range cannot exceed one year.'));
return;
}
d.hide(); d.hide();
let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00'); let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00');

View File

@ -925,6 +925,11 @@ VATETaxes.import = {
return; return;
} }
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
frappe.msgprint(__('The date range cannot exceed one year.'));
return;
}
if (fromDateMoment.isAfter(toDateMoment)) { if (fromDateMoment.isAfter(toDateMoment)) {
frappe.msgprint(__('From Date cannot be later than To Date')); frappe.msgprint(__('From Date cannot be later than To Date'));
return; return;

View File

@ -902,6 +902,11 @@ ETaxes.import = {
return; return;
} }
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
frappe.msgprint(__('The date range cannot exceed one year.'));
return;
}
if (!values.warehouse) { if (!values.warehouse) {
frappe.msgprint({ frappe.msgprint({
title: __('Warning'), title: __('Warning'),

View File

@ -921,6 +921,11 @@ ETaxes.import = {
return; return;
} }
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
frappe.msgprint(__('The date range cannot exceed one year.'));
return;
}
if (!values.warehouse) { if (!values.warehouse) {
frappe.msgprint({ frappe.msgprint({
title: __('Warning'), title: __('Warning'),

View File

@ -903,6 +903,11 @@ SalesETaxes.import = {
return; return;
} }
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
frappe.msgprint(__('The date range cannot exceed one year.'));
return;
}
if (!values.warehouse) { if (!values.warehouse) {
frappe.msgprint({ frappe.msgprint({
title: __('Warning'), title: __('Warning'),

View File

@ -954,6 +954,11 @@ function show_load_dialog_with_summary(frm, summary) {
return; return;
} }
if (moment(values.date_to).diff(moment(values.date_from), 'days') > 366) {
frappe.msgprint(__('The date range cannot exceed one year.'));
return;
}
// Show confirmation with selected types // Show confirmation with selected types
const selected_types = []; const selected_types = [];
if (values.load_items) selected_types.push(__('Items')); if (values.load_items) selected_types.push(__('Items'));

View File

@ -899,7 +899,7 @@ def import_bulk_sales_invoices(invoice_ids, token, warehouse=None):
user=user, user=user,
bg_job_id=job_id, bg_job_id=job_id,
queue="default", queue="default",
timeout=1200, timeout=7200,
) )
return {"success": True, "enqueued": True, "total": len(invoice_ids), "job_id": job_id} return {"success": True, "enqueued": True, "total": len(invoice_ids), "job_id": job_id}

View File

@ -1,6 +1,7 @@
# ======= SHARED HELPERS ======= # ======= SHARED HELPERS =======
import frappe import frappe
from datetime import datetime
def resolve_customer_group(preferred=None): def resolve_customer_group(preferred=None):
@ -27,3 +28,27 @@ def resolve_territory(preferred=None):
if preferred and frappe.db.exists("Territory", preferred): if preferred and frappe.db.exists("Territory", preferred):
return preferred return preferred
return None return None
def _parse_load_date(value):
"""Parse the date formats the e-taxes loaders use ('YYYY-MM-DD' or
'DD-MM-YYYY[ HH:MM]'). Returns a datetime.date or None."""
if not value:
return None
token = str(value).strip().split(" ")[0]
for fmt in ("%Y-%m-%d", "%d-%m-%Y"):
try:
return datetime.strptime(token, fmt).date()
except ValueError:
continue
return None
def exceeds_one_year(date_from, date_to):
"""True if [date_from, date_to] spans more than one year (366 days).
Used to forbid loading more than a year of data in a single run applies to
every e-taxes load (invoices, reference data, VAT) EXCEPT Company data."""
df = _parse_load_date(date_from)
dt = _parse_load_date(date_to)
return bool(df and dt and (dt - df).days > 366)

View File

@ -8,7 +8,7 @@ import json
from datetime import datetime from datetime import datetime
from frappe.utils import now_datetime, getdate from frappe.utils import now_datetime, getdate
from invoice_az.auth import record_etaxes_activity, get_default_asan_login from invoice_az.auth import record_etaxes_activity, get_default_asan_login
from invoice_az.utils import resolve_customer_group from invoice_az.utils import resolve_customer_group, exceeds_one_year
from invoice_az import background_tasks from invoice_az import background_tasks
# API Endpoint # API Endpoint
@ -97,6 +97,9 @@ def get_vat_operations(token, date_from, date_to, max_count=200, offset=0):
max_count = 200 max_count = 200
offset = 0 offset = 0
if offset == 0 and exceeds_one_year(date_from, date_to):
return {"error": "range_too_long", "message": "Date range cannot exceed one year."}
payload = { payload = {
"accounts": None, "accounts": None,
"creationDateFrom": date_from, "creationDateFrom": date_from,
@ -1147,7 +1150,7 @@ def import_bulk_vat_operations(operations_data, company=None, create_as_draft=0,
user=user, user=user,
bg_job_id=job_id, bg_job_id=job_id,
queue="default", queue="default",
timeout=1200, timeout=7200,
) )
return {"success": True, "enqueued": True, "total": len(operations_data), "job_id": job_id} return {"success": True, "enqueued": True, "total": len(operations_data), "job_id": job_id}