diff --git a/invoice_az/api.py b/invoice_az/api.py index 3040a10..01be139 100644 --- a/invoice_az/api.py +++ b/invoice_az/api.py @@ -16,6 +16,75 @@ from .utils import resolve_customer_group, exceeds_one_year 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() def get_invoices(token, filters=None): """Getting list of invoices with filtering options""" @@ -906,6 +975,15 @@ def create_purchase_invoice_from_order(purchase_order_name): # ДОБАВЛЕНО: Устанавливаем галочку is_taxes_doc для Purchase Invoice 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 if po_doc.taxes: @@ -1105,6 +1183,14 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule # Add/update additional fields from invoice po.title = f"Invoice {invoice_data.get('serialNumber', '')}" 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 if purchase_order_name and invoice_data.get("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) # Создаем 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'): + etaxes_purchase_name = etaxes_purchase_result.get('name') # Устанавливаем поля E-Taxes ДО submit'а po.is_taxes_doc = 1 - po.taxes_doc = etaxes_purchase_result.get('name') + po.taxes_doc = etaxes_purchase_name # Сохраняем изменения po.save() - - # ИЗМЕНЕНИЕ: Делаем Submit для Purchase Order + + # Submit Purchase Order. Если документ не сабмитится — НЕ оставляем его: + # откатываем весь импорт (черновик PO + трекинг-запись) и сообщаем об ошибке. try: po.submit() frappe.log_error(f"Purchase Order {po.name} submitted successfully", "Import Invoice Success") except Exception as e: 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 { 'success': False, 'message': f'Failed to submit Purchase Order: {str(e)}' } - - # ИЗМЕНЕНИЕ: Создаем Purchase Invoice + + # Создаём и сабмитим Purchase Invoice. Любая неудача на этом этапе — + # тоже полный откат (PO уже засабмичен, поэтому отменяем и удаляем его). + pi_name = None try: pi_name = create_purchase_invoice_from_order(po.name) - if pi_name: - # Делаем Submit для Purchase Invoice - pi = frappe.get_doc("Purchase Invoice", pi_name) - pi.submit() - frappe.log_error(f"Purchase Invoice {pi_name} created and submitted successfully", "Import Invoice Success") - - return { - 'success': True, - 'message': 'Invoice data imported successfully. Purchase Order and Purchase Invoice created.', - 'purchase_order': po.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: - frappe.log_error(f"Error creating or submitting Purchase Invoice: {str(e)}", "Submit PI Error") + if not pi_name: + raise Exception("create_purchase_invoice_from_order returned no name") + pi = frappe.get_doc("Purchase Invoice", pi_name) + pi.submit() + frappe.log_error(f"Purchase Invoice {pi_name} created and submitted successfully", "Import Invoice Success") return { 'success': True, - 'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.', - 'purchase_order': po.name + 'message': 'Invoice data imported successfully. Purchase Order and Purchase Invoice created.', + 'purchase_order': po.name, + 'purchase_invoice': pi_name + } + except Exception as e: + 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 { + 'success': False, + 'message': f'Failed to submit Purchase Invoice: {str(e)}' } except Exception as e: @@ -1373,37 +1464,42 @@ def get_etaxes_purchases(): } @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""" # Записываем активность record_etaxes_activity() - + try: # Normalize ID for storage etaxes_id = str(etaxes_id).strip() if etaxes_id else "" - + # Debug logging frappe.logger().info(f"Creating E-Taxes Purchase: ID={etaxes_id}, date={date}, party={party}, total={total}") - + # Check if record with such etaxes_id already exists existing = frappe.db.get_value('E-Taxes Purchase', {'etaxes_id': etaxes_id}, 'name') if existing: # Debug logging frappe.logger().info(f"E-Taxes Purchase already exists: {existing}") - + return { 'success': True, 'message': 'Record already exists', 'name': existing } - + # Create new record etaxes_purchase = frappe.new_doc('E-Taxes Purchase') etaxes_purchase.etaxes_id = etaxes_id etaxes_purchase.date = date etaxes_purchase.party = party 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() 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 else: 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: for key in ("unmatched_items", "unmatched_parties", "unmatched_units"): if result.get(key): diff --git a/invoice_az/client/purchase_order.js b/invoice_az/client/purchase_order.js index fb96b11..dc7e689 100644 --- a/invoice_az/client/purchase_order.js +++ b/invoice_az/client/purchase_order.js @@ -557,11 +557,12 @@ ETaxes.auth = { ETaxes.errors = { // Добавить ошибку в лог add: function(invoiceData, errorType, errorMessage, additionalData = {}) { + const invoiceData_ = invoiceData || {}; const errorEntry = { - invoice_id: invoiceData.id || 'Unknown', - serial_number: invoiceData.serialNumber || 'Unknown', - sender_name: invoiceData.sender ? invoiceData.sender.name : 'Unknown', - creation_date: invoiceData.creationDate || invoiceData.createdAt || invoiceData.date || 'Unknown', + invoice_id: invoiceData_.id || '', + serial_number: invoiceData_.serialNumber || invoiceData_.id || '', + sender_name: (invoiceData_.sender && invoiceData_.sender.name) || invoiceData_.senderName || '', + creation_date: invoiceData_.creationDate || invoiceData_.createdAt || invoiceData_.date || invoiceData_.operationDate || '', error_type: errorType, error_message: errorMessage, timestamp: new Date().toISOString(), @@ -596,12 +597,11 @@ ETaxes.errors = { let errorsHtml = '