feat(purchase): import comments + split serial, atomic rollback, serial in error log
- Import invoiceComment/invoiceComment2 and split serialNumber into series/number onto Purchase Order/Invoice and the E-Taxes Purchase tracking record - Roll back the whole import (delete PO/PI + tracking record) when submit fails, so a non-submittable document leaves nothing half-imported behind - Show the real serial number (not the internal e-taxes id) in the bulk-import error log; clean up "undefined"/"No date" placeholders Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8e3cc32501
commit
b3a549e80b
|
|
@ -16,6 +16,75 @@ from .utils import resolve_customer_group, exceeds_one_year
|
||||||
from .sales_api import get_sales_invoices
|
from .sales_api import get_sales_invoices
|
||||||
|
|
||||||
|
|
||||||
|
def split_serial_number(serial):
|
||||||
|
"""Split an e-taxes serialNumber into (series, number).
|
||||||
|
|
||||||
|
Two known formats, distinguished by the count of leading letters:
|
||||||
|
- 2 letters + 4 date digits (YYMM) + number, e.g. "MT240912345678"
|
||||||
|
-> series "MT2409", number "12345678"
|
||||||
|
- 4 letters + number, e.g. "EBEA057741"
|
||||||
|
-> series "EBEA", number "057741"
|
||||||
|
|
||||||
|
Anything else (empty, unexpected prefix) -> the whole string becomes the
|
||||||
|
series and the number is left empty (per agreed fallback).
|
||||||
|
"""
|
||||||
|
serial = (serial or "").strip()
|
||||||
|
if not serial:
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
match = re.match(r"^([A-Za-z]+)(\d+)$", serial)
|
||||||
|
if match:
|
||||||
|
letters, digits = match.group(1), match.group(2)
|
||||||
|
if len(letters) == 2 and len(digits) > 4:
|
||||||
|
# new format: letters + YYMM are the series, the rest is the number
|
||||||
|
return letters + digits[:4], digits[4:]
|
||||||
|
if len(letters) == 4:
|
||||||
|
# old format: letters are the series, digits are the number
|
||||||
|
return letters, digits
|
||||||
|
|
||||||
|
# Unknown / unexpected format: keep the whole value as the series.
|
||||||
|
return serial, ""
|
||||||
|
|
||||||
|
|
||||||
|
def _rollback_import(po_name=None, etaxes_purchase_name=None, pi_name=None):
|
||||||
|
"""Remove any partially-imported docs so a failed import leaves nothing behind.
|
||||||
|
|
||||||
|
Used when a Purchase Order or its Purchase Invoice cannot be submitted: we
|
||||||
|
must not keep a half-imported draft or an orphan tracking record. Deletes in
|
||||||
|
dependency order (PI -> PO -> E-Taxes Purchase). Deleting the PO also fires
|
||||||
|
the on_trash hook that removes the linked E-Taxes Purchase, but we delete it
|
||||||
|
explicitly too in case linking never happened.
|
||||||
|
"""
|
||||||
|
# Purchase Invoice first (it depends on the PO)
|
||||||
|
if pi_name and frappe.db.exists("Purchase Invoice", pi_name):
|
||||||
|
try:
|
||||||
|
pi = frappe.get_doc("Purchase Invoice", pi_name)
|
||||||
|
if pi.docstatus == 1:
|
||||||
|
pi.cancel()
|
||||||
|
frappe.delete_doc("Purchase Invoice", pi_name, force=True, ignore_permissions=True)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(f"Failed to delete PI {pi_name}: {e}\n{frappe.get_traceback()}", "Import Rollback Error")
|
||||||
|
|
||||||
|
# Purchase Order
|
||||||
|
if po_name and frappe.db.exists("Purchase Order", po_name):
|
||||||
|
try:
|
||||||
|
po = frappe.get_doc("Purchase Order", po_name)
|
||||||
|
if po.docstatus == 1:
|
||||||
|
po.cancel()
|
||||||
|
frappe.delete_doc("Purchase Order", po_name, force=True, ignore_permissions=True)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(f"Failed to delete PO {po_name}: {e}\n{frappe.get_traceback()}", "Import Rollback Error")
|
||||||
|
|
||||||
|
# E-Taxes Purchase tracking record (may already be gone via PO on_trash hook)
|
||||||
|
if etaxes_purchase_name and frappe.db.exists("E-Taxes Purchase", etaxes_purchase_name):
|
||||||
|
try:
|
||||||
|
frappe.delete_doc("E-Taxes Purchase", etaxes_purchase_name, force=True, ignore_permissions=True)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(f"Failed to delete E-Taxes Purchase {etaxes_purchase_name}: {e}\n{frappe.get_traceback()}", "Import Rollback Error")
|
||||||
|
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_invoices(token, filters=None):
|
def get_invoices(token, filters=None):
|
||||||
"""Getting list of invoices with filtering options"""
|
"""Getting list of invoices with filtering options"""
|
||||||
|
|
@ -907,6 +976,15 @@ def create_purchase_invoice_from_order(purchase_order_name):
|
||||||
# ДОБАВЛЕНО: Устанавливаем галочку is_taxes_doc для Purchase Invoice
|
# ДОБАВЛЕНО: Устанавливаем галочку is_taxes_doc для Purchase Invoice
|
||||||
pi_doc.is_taxes_doc = 1
|
pi_doc.is_taxes_doc = 1
|
||||||
|
|
||||||
|
# Переносим комментарии и серию/номер из Purchase Order.
|
||||||
|
# remarks (коммент 1) ставим только если он непустой — иначе пусть
|
||||||
|
# ERPNext сам подставит стандартное "Against Supplier Invoice ...".
|
||||||
|
if po_doc.get("remarks"):
|
||||||
|
pi_doc.remarks = po_doc.remarks
|
||||||
|
pi_doc.etaxes_invoice_comment_2 = po_doc.get("etaxes_invoice_comment_2")
|
||||||
|
pi_doc.etaxes_invoice_series = po_doc.get("etaxes_invoice_series")
|
||||||
|
pi_doc.etaxes_invoice_number = po_doc.get("etaxes_invoice_number")
|
||||||
|
|
||||||
# ДОБАВЛЕНО: Копируем налоги из Purchase Order
|
# ДОБАВЛЕНО: Копируем налоги из Purchase Order
|
||||||
if po_doc.taxes:
|
if po_doc.taxes:
|
||||||
pi_doc.taxes = []
|
pi_doc.taxes = []
|
||||||
|
|
@ -1106,6 +1184,14 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
|
||||||
po.title = f"Invoice {invoice_data.get('serialNumber', '')}"
|
po.title = f"Invoice {invoice_data.get('serialNumber', '')}"
|
||||||
po.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "")
|
po.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "")
|
||||||
|
|
||||||
|
# Comments from e-taxes: comment 1 -> general remarks (human-editable),
|
||||||
|
# comment 2 -> dedicated field. Series/number split from serialNumber.
|
||||||
|
po.remarks = invoice_data.get("invoiceComment") or ""
|
||||||
|
po.etaxes_invoice_comment_2 = invoice_data.get("invoiceComment2") or ""
|
||||||
|
etaxes_series, etaxes_number = split_serial_number(invoice_data.get("serialNumber"))
|
||||||
|
po.etaxes_invoice_series = etaxes_series
|
||||||
|
po.etaxes_invoice_number = etaxes_number
|
||||||
|
|
||||||
# Clear existing items if need to update them completely
|
# Clear existing items if need to update them completely
|
||||||
if purchase_order_name and invoice_data.get("items"):
|
if purchase_order_name and invoice_data.get("items"):
|
||||||
po.items = []
|
po.items = []
|
||||||
|
|
@ -1296,52 +1382,57 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
|
||||||
total = invoice_data.get('totalAmount', 0) or invoice_data.get('amount', 0)
|
total = invoice_data.get('totalAmount', 0) or invoice_data.get('amount', 0)
|
||||||
|
|
||||||
# Создаем E-Taxes Purchase
|
# Создаем E-Taxes Purchase
|
||||||
etaxes_purchase_result = create_etaxes_purchase(invoice_id, date_to_use, sender_name, total)
|
etaxes_purchase_result = create_etaxes_purchase(
|
||||||
|
invoice_id, date_to_use, sender_name, total,
|
||||||
|
series=etaxes_series,
|
||||||
|
number=etaxes_number,
|
||||||
|
comment=invoice_data.get("invoiceComment") or "",
|
||||||
|
comment2=invoice_data.get("invoiceComment2") or "",
|
||||||
|
)
|
||||||
|
etaxes_purchase_name = None
|
||||||
if etaxes_purchase_result and etaxes_purchase_result.get('success'):
|
if etaxes_purchase_result and etaxes_purchase_result.get('success'):
|
||||||
|
etaxes_purchase_name = etaxes_purchase_result.get('name')
|
||||||
# Устанавливаем поля E-Taxes ДО submit'а
|
# Устанавливаем поля E-Taxes ДО submit'а
|
||||||
po.is_taxes_doc = 1
|
po.is_taxes_doc = 1
|
||||||
po.taxes_doc = etaxes_purchase_result.get('name')
|
po.taxes_doc = etaxes_purchase_name
|
||||||
# Сохраняем изменения
|
# Сохраняем изменения
|
||||||
po.save()
|
po.save()
|
||||||
|
|
||||||
# ИЗМЕНЕНИЕ: Делаем Submit для Purchase Order
|
# Submit Purchase Order. Если документ не сабмитится — НЕ оставляем его:
|
||||||
|
# откатываем весь импорт (черновик PO + трекинг-запись) и сообщаем об ошибке.
|
||||||
try:
|
try:
|
||||||
po.submit()
|
po.submit()
|
||||||
frappe.log_error(f"Purchase Order {po.name} submitted successfully", "Import Invoice Success")
|
frappe.log_error(f"Purchase Order {po.name} submitted successfully", "Import Invoice Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.log_error(f"Error submitting Purchase Order {po.name}: {str(e)}", "Submit PO Error")
|
frappe.log_error(f"Error submitting Purchase Order {po.name}: {str(e)}", "Submit PO Error")
|
||||||
|
_rollback_import(po_name=po.name, etaxes_purchase_name=etaxes_purchase_name)
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'Failed to submit Purchase Order: {str(e)}'
|
'message': f'Failed to submit Purchase Order: {str(e)}'
|
||||||
}
|
}
|
||||||
|
|
||||||
# ИЗМЕНЕНИЕ: Создаем Purchase Invoice
|
# Создаём и сабмитим Purchase Invoice. Любая неудача на этом этапе —
|
||||||
|
# тоже полный откат (PO уже засабмичен, поэтому отменяем и удаляем его).
|
||||||
|
pi_name = None
|
||||||
try:
|
try:
|
||||||
pi_name = create_purchase_invoice_from_order(po.name)
|
pi_name = create_purchase_invoice_from_order(po.name)
|
||||||
if pi_name:
|
if not pi_name:
|
||||||
# Делаем Submit для Purchase Invoice
|
raise Exception("create_purchase_invoice_from_order returned no name")
|
||||||
pi = frappe.get_doc("Purchase Invoice", pi_name)
|
pi = frappe.get_doc("Purchase Invoice", pi_name)
|
||||||
pi.submit()
|
pi.submit()
|
||||||
frappe.log_error(f"Purchase Invoice {pi_name} created and submitted successfully", "Import Invoice Success")
|
frappe.log_error(f"Purchase Invoice {pi_name} created and submitted successfully", "Import Invoice Success")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': 'Invoice data imported successfully. Purchase Order and Purchase Invoice created.',
|
'message': 'Invoice data imported successfully. Purchase Order and Purchase Invoice created.',
|
||||||
'purchase_order': po.name,
|
'purchase_order': po.name,
|
||||||
'purchase_invoice': pi_name
|
'purchase_invoice': pi_name
|
||||||
}
|
}
|
||||||
else:
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.',
|
|
||||||
'purchase_order': po.name
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.log_error(f"Error creating or submitting Purchase Invoice: {str(e)}", "Submit PI Error")
|
frappe.log_error(f"Error creating or submitting Purchase Invoice: {str(e)}\n{frappe.get_traceback()}", "Submit PI Error")
|
||||||
|
_rollback_import(po_name=po.name, etaxes_purchase_name=etaxes_purchase_name, pi_name=pi_name)
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': False,
|
||||||
'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.',
|
'message': f'Failed to submit Purchase Invoice: {str(e)}'
|
||||||
'purchase_order': po.name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1373,7 +1464,8 @@ def get_etaxes_purchases():
|
||||||
}
|
}
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_etaxes_purchase(etaxes_id, date, party, total):
|
def create_etaxes_purchase(etaxes_id, date, party, total,
|
||||||
|
series=None, number=None, comment=None, comment2=None):
|
||||||
"""Creates E-Taxes Purchase record for tracking imported invoices"""
|
"""Creates E-Taxes Purchase record for tracking imported invoices"""
|
||||||
# Записываем активность
|
# Записываем активность
|
||||||
record_etaxes_activity()
|
record_etaxes_activity()
|
||||||
|
|
@ -1403,6 +1495,10 @@ def create_etaxes_purchase(etaxes_id, date, party, total):
|
||||||
etaxes_purchase.date = date
|
etaxes_purchase.date = date
|
||||||
etaxes_purchase.party = party
|
etaxes_purchase.party = party
|
||||||
etaxes_purchase.total = total
|
etaxes_purchase.total = total
|
||||||
|
etaxes_purchase.series = series or ""
|
||||||
|
etaxes_purchase.number = number or ""
|
||||||
|
etaxes_purchase.comment = comment or ""
|
||||||
|
etaxes_purchase.comment2 = comment2 or ""
|
||||||
|
|
||||||
etaxes_purchase.insert()
|
etaxes_purchase.insert()
|
||||||
frappe.db.commit() # Explicit commit
|
frappe.db.commit() # Explicit commit
|
||||||
|
|
@ -4471,7 +4567,11 @@ def _process_bulk_purchase_import(invoice_ids, token, warehouse, user, bg_job_id
|
||||||
imported_count += 1
|
imported_count += 1
|
||||||
else:
|
else:
|
||||||
error_msg = result.get("message", "Unknown import error") if result else "Empty result"
|
error_msg = result.get("message", "Unknown import error") if result else "Empty result"
|
||||||
error_entry = {"invoice_id": invoice_id, "error": error_msg}
|
error_entry = {
|
||||||
|
"invoice_id": invoice_id,
|
||||||
|
"serial_number": details.get("serialNumber") or "",
|
||||||
|
"error": error_msg,
|
||||||
|
}
|
||||||
if result:
|
if result:
|
||||||
for key in ("unmatched_items", "unmatched_parties", "unmatched_units"):
|
for key in ("unmatched_items", "unmatched_parties", "unmatched_units"):
|
||||||
if result.get(key):
|
if result.get(key):
|
||||||
|
|
|
||||||
|
|
@ -557,11 +557,12 @@ ETaxes.auth = {
|
||||||
ETaxes.errors = {
|
ETaxes.errors = {
|
||||||
// Добавить ошибку в лог
|
// Добавить ошибку в лог
|
||||||
add: function(invoiceData, errorType, errorMessage, additionalData = {}) {
|
add: function(invoiceData, errorType, errorMessage, additionalData = {}) {
|
||||||
|
const invoiceData_ = invoiceData || {};
|
||||||
const errorEntry = {
|
const errorEntry = {
|
||||||
invoice_id: invoiceData.id || 'Unknown',
|
invoice_id: invoiceData_.id || '',
|
||||||
serial_number: invoiceData.serialNumber || 'Unknown',
|
serial_number: invoiceData_.serialNumber || invoiceData_.id || '',
|
||||||
sender_name: invoiceData.sender ? invoiceData.sender.name : 'Unknown',
|
sender_name: (invoiceData_.sender && invoiceData_.sender.name) || invoiceData_.senderName || '',
|
||||||
creation_date: invoiceData.creationDate || invoiceData.createdAt || invoiceData.date || 'Unknown',
|
creation_date: invoiceData_.creationDate || invoiceData_.createdAt || invoiceData_.date || invoiceData_.operationDate || '',
|
||||||
error_type: errorType,
|
error_type: errorType,
|
||||||
error_message: errorMessage,
|
error_message: errorMessage,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
@ -596,12 +597,11 @@ ETaxes.errors = {
|
||||||
let errorsHtml = '<div class="error-log-container">';
|
let errorsHtml = '<div class="error-log-container">';
|
||||||
|
|
||||||
ETaxes.loadingErrors.forEach(function(error, index) {
|
ETaxes.loadingErrors.forEach(function(error, index) {
|
||||||
let documentDate = 'No date';
|
let documentDate = '';
|
||||||
if (error.creation_date && error.creation_date !== 'Unknown') {
|
if (error.creation_date && error.creation_date !== 'Unknown') {
|
||||||
try {
|
const m = moment(error.creation_date);
|
||||||
documentDate = moment(error.creation_date).format('DD.MM.YYYY');
|
if (m.isValid()) {
|
||||||
} catch (e) {
|
documentDate = m.format('DD.MM.YYYY');
|
||||||
documentDate = 'Invalid date';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -611,10 +611,12 @@ ETaxes.errors = {
|
||||||
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
||||||
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||||||
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
|
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
|
||||||
'<strong>' + (error.serial_number || error.invoice_id) + '</strong></h5>';
|
'<strong>' + (error.serial_number || error.invoice_id || __('Unknown')) + '</strong></h5>';
|
||||||
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
||||||
errorsHtml += '</div>';
|
errorsHtml += '</div>';
|
||||||
|
if (error.sender_name) {
|
||||||
errorsHtml += '<small style="color: var(--text-muted);">' + error.sender_name + '</small>';
|
errorsHtml += '<small style="color: var(--text-muted);">' + error.sender_name + '</small>';
|
||||||
|
}
|
||||||
errorsHtml += '</div>';
|
errorsHtml += '</div>';
|
||||||
|
|
||||||
// Сообщение об ошибке
|
// Сообщение об ошибке
|
||||||
|
|
@ -660,9 +662,11 @@ ETaxes.errors = {
|
||||||
errorsHtml += '</ul></div>';
|
errorsHtml += '</ul></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (documentDate) {
|
||||||
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
||||||
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
||||||
errorsHtml += '</div>';
|
errorsHtml += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
errorsHtml += '</div>';
|
errorsHtml += '</div>';
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -559,11 +559,12 @@ ETaxes.auth = {
|
||||||
ETaxes.errors = {
|
ETaxes.errors = {
|
||||||
// Добавить ошибку в лог
|
// Добавить ошибку в лог
|
||||||
add: function(invoiceData, errorType, errorMessage, additionalData = {}) {
|
add: function(invoiceData, errorType, errorMessage, additionalData = {}) {
|
||||||
|
const invoiceData_ = invoiceData || {};
|
||||||
const errorEntry = {
|
const errorEntry = {
|
||||||
invoice_id: invoiceData.id || 'Unknown',
|
invoice_id: invoiceData_.id || '',
|
||||||
serial_number: invoiceData.serialNumber || 'Unknown',
|
serial_number: invoiceData_.serialNumber || invoiceData_.id || '',
|
||||||
sender_name: invoiceData.sender ? invoiceData.sender.name : 'Unknown',
|
sender_name: (invoiceData_.sender && invoiceData_.sender.name) || invoiceData_.senderName || '',
|
||||||
creation_date: invoiceData.creationDate || invoiceData.createdAt || invoiceData.date || 'Unknown',
|
creation_date: invoiceData_.creationDate || invoiceData_.createdAt || invoiceData_.date || invoiceData_.operationDate || '',
|
||||||
error_type: errorType,
|
error_type: errorType,
|
||||||
error_message: errorMessage,
|
error_message: errorMessage,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
@ -598,12 +599,11 @@ ETaxes.errors = {
|
||||||
let errorsHtml = '<div class="error-log-container">';
|
let errorsHtml = '<div class="error-log-container">';
|
||||||
|
|
||||||
ETaxes.loadingErrors.forEach(function(error, index) {
|
ETaxes.loadingErrors.forEach(function(error, index) {
|
||||||
let documentDate = 'No date';
|
let documentDate = '';
|
||||||
if (error.creation_date && error.creation_date !== 'Unknown') {
|
if (error.creation_date && error.creation_date !== 'Unknown') {
|
||||||
try {
|
const m = moment(error.creation_date);
|
||||||
documentDate = moment(error.creation_date).format('DD.MM.YYYY');
|
if (m.isValid()) {
|
||||||
} catch (e) {
|
documentDate = m.format('DD.MM.YYYY');
|
||||||
documentDate = 'Invalid date';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -613,10 +613,12 @@ ETaxes.errors = {
|
||||||
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
||||||
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||||||
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
|
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
|
||||||
'<strong>' + (error.serial_number || error.invoice_id) + '</strong></h5>';
|
'<strong>' + (error.serial_number || error.invoice_id || __('Unknown')) + '</strong></h5>';
|
||||||
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
||||||
errorsHtml += '</div>';
|
errorsHtml += '</div>';
|
||||||
|
if (error.sender_name) {
|
||||||
errorsHtml += '<small style="color: var(--text-muted);">' + error.sender_name + '</small>';
|
errorsHtml += '<small style="color: var(--text-muted);">' + error.sender_name + '</small>';
|
||||||
|
}
|
||||||
errorsHtml += '</div>';
|
errorsHtml += '</div>';
|
||||||
|
|
||||||
// Сообщение об ошибке
|
// Сообщение об ошибке
|
||||||
|
|
@ -662,9 +664,11 @@ ETaxes.errors = {
|
||||||
errorsHtml += '</ul></div>';
|
errorsHtml += '</ul></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (documentDate) {
|
||||||
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
||||||
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
||||||
errorsHtml += '</div>';
|
errorsHtml += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
errorsHtml += '</div>';
|
errorsHtml += '</div>';
|
||||||
});
|
});
|
||||||
|
|
@ -743,8 +747,8 @@ ETaxes.errors = {
|
||||||
csvContent += "Invoice ID,Serial Number,Date,Supplier,Error Type,Error Message,Timestamp\n";
|
csvContent += "Invoice ID,Serial Number,Date,Supplier,Error Type,Error Message,Timestamp\n";
|
||||||
|
|
||||||
ETaxes.loadingErrors.forEach(function(error) {
|
ETaxes.loadingErrors.forEach(function(error) {
|
||||||
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
const cleanMessage = (error.error_message || '').replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
||||||
const cleanSender = error.sender_name.replace(/"/g, '""');
|
const cleanSender = (error.sender_name || '').replace(/"/g, '""');
|
||||||
|
|
||||||
const row = [
|
const row = [
|
||||||
error.invoice_id,
|
error.invoice_id,
|
||||||
|
|
@ -1110,8 +1114,14 @@ ETaxes.import = {
|
||||||
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('Amount') + '</th>' +
|
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('Amount') + '</th>' +
|
||||||
'</tr></thead><tbody>';
|
'</tr></thead><tbody>';
|
||||||
|
|
||||||
|
// Remember the serial from the selection list per invoice id. The detail
|
||||||
|
// endpoint sometimes returns it empty (e.g. old documents), so we fall
|
||||||
|
// back to this during import instead of showing the internal id.
|
||||||
|
ETaxes.import.serialByInvoiceId = {};
|
||||||
|
|
||||||
invoices.forEach(function(invoice) {
|
invoices.forEach(function(invoice) {
|
||||||
const serialNumber = invoice.serialNumber || '';
|
const serialNumber = invoice.serialNumber || '';
|
||||||
|
ETaxes.import.serialByInvoiceId[String(invoice.id)] = serialNumber;
|
||||||
|
|
||||||
let creationDate = '';
|
let creationDate = '';
|
||||||
if (invoice.createdAt) {
|
if (invoice.createdAt) {
|
||||||
|
|
@ -1281,7 +1291,11 @@ ETaxes.import = {
|
||||||
if (r.message && !r.message.error) {
|
if (r.message && !r.message.error) {
|
||||||
const creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
|
const creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
|
||||||
const scheduleDate = moment(creationDate).format('YYYY-MM-DD');
|
const scheduleDate = moment(creationDate).format('YYYY-MM-DD');
|
||||||
const serialNumber = r.message.serialNumber || '';
|
// Fall back to the serial captured from the selection list when the
|
||||||
|
// detail omits it, and write it back so the import payload (series/
|
||||||
|
// number, title), the success alert and the error log all use it.
|
||||||
|
const serialNumber = r.message.serialNumber || (ETaxes.import.serialByInvoiceId || {})[String(invoiceId)] || '';
|
||||||
|
r.message.serialNumber = serialNumber;
|
||||||
const senderName = r.message.sender ? r.message.sender.name : '';
|
const senderName = r.message.sender ? r.message.sender.name : '';
|
||||||
const total = r.message.totalAmount || r.message.amount || 0;
|
const total = r.message.totalAmount || r.message.amount || 0;
|
||||||
|
|
||||||
|
|
@ -1500,7 +1514,10 @@ ETaxes.import = {
|
||||||
|
|
||||||
(data.errors || []).forEach(function(err) {
|
(data.errors || []).forEach(function(err) {
|
||||||
ETaxes.loadingErrors.push({
|
ETaxes.loadingErrors.push({
|
||||||
invoice_id: err.invoice_id || 'Unknown',
|
invoice_id: err.invoice_id || '',
|
||||||
|
// Prefer the serial the backend resolved; otherwise fall back to the
|
||||||
|
// serial captured from the selection list so we never show the raw id.
|
||||||
|
serial_number: err.serial_number || (ETaxes.import.serialByInvoiceId || {})[String(err.invoice_id)] || '',
|
||||||
error_type: err.unmatched_items ? 'Unmapped Items'
|
error_type: err.unmatched_items ? 'Unmapped Items'
|
||||||
: err.unmatched_parties ? 'Unmapped Parties'
|
: err.unmatched_parties ? 'Unmapped Parties'
|
||||||
: err.unmatched_units ? 'Unmapped Units'
|
: err.unmatched_units ? 'Unmapped Units'
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,11 @@
|
||||||
"date",
|
"date",
|
||||||
"total",
|
"total",
|
||||||
"party",
|
"party",
|
||||||
"etaxes_id"
|
"etaxes_id",
|
||||||
|
"series",
|
||||||
|
"number",
|
||||||
|
"comment",
|
||||||
|
"comment2"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
|
@ -30,6 +34,26 @@
|
||||||
"fieldname": "etaxes_id",
|
"fieldname": "etaxes_id",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "etaxes_id"
|
"label": "etaxes_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "series",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Series"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "comment",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "comment2",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Comment 2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue