diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md
index 923e145..41a86b7 100644
--- a/IMPLEMENTATION_SUMMARY.md
+++ b/IMPLEMENTATION_SUMMARY.md
@@ -305,8 +305,8 @@ bench --site site1 clear-cache
# 4. Restart bench (if needed)
bench restart
-# 5. (Optional) Run data migration script
-# bench --site site1 execute invoice_az.patches.migrate_tax_templates
+# 5. (Optional) Run data migration script (if needed)
+# Create and execute migration function directly
```
---
diff --git a/invoice_az/client/journal_entry.js b/invoice_az/client/journal_entry.js
index 15f1d4e..9a72e73 100644
--- a/invoice_az/client/journal_entry.js
+++ b/invoice_az/client/journal_entry.js
@@ -1405,7 +1405,9 @@ VATETaxes.import = {
// Journal Entry List
frappe.listview_settings['Journal Entry'] = {
- add_fields: ['posting_date', 'voucher_type', 'total_debit', 'total_credit', 'docstatus', 'expense_income_type'],
+ add_fields: ['posting_date', 'voucher_type', 'total_debit', 'total_credit', 'docstatus', 'expense_income_type', 'etaxes_document_type', 'loaded_from_etaxes'],
+
+ hide_name_column: false,
get_indicator: function(doc) {
if (doc.docstatus == 1) {
@@ -1425,6 +1427,14 @@ frappe.listview_settings['Journal Entry'] = {
return '' + value + '';
}
return value || '';
+ },
+
+ etaxes_document_type: function(value, field, doc) {
+ // Show blue badge "ƏDV" only for documents loaded from e-taxes
+ if (value === 'ədv' && (doc.loaded_from_etaxes === 1 || doc.loaded_from_etaxes === '1')) {
+ return 'ƏDV';
+ }
+ return '';
}
},
diff --git a/invoice_az/hooks.py b/invoice_az/hooks.py
index 452f2c5..3e3b388 100644
--- a/invoice_az/hooks.py
+++ b/invoice_az/hooks.py
@@ -75,10 +75,11 @@ def after_install():
def after_migrate():
"""Run migration tasks"""
from invoice_az.auth import setup_token_renewal
- from invoice_az.install import create_journal_entry_custom_fields
+ from invoice_az.install import create_journal_entry_custom_fields, create_supplier_custom_fields
setup_token_renewal()
create_journal_entry_custom_fields()
+ create_supplier_custom_fields()
# Fixtures for master data
# ------------------------
diff --git a/invoice_az/install.py b/invoice_az/install.py
new file mode 100644
index 0000000..ac59021
--- /dev/null
+++ b/invoice_az/install.py
@@ -0,0 +1,85 @@
+"""Installation hooks for Invoice Az app"""
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def after_install():
+ """Run after app installation"""
+ create_journal_entry_custom_fields()
+ create_supplier_custom_fields()
+
+
+def create_journal_entry_custom_fields():
+ """Create custom fields for Journal Entry doctype"""
+
+ # Always define all fields - create_custom_fields will handle updates
+ custom_fields = {
+ 'Journal Entry': [
+ {
+ 'fieldname': 'expense_income_type',
+ 'label': 'Expense/Income Type',
+ 'fieldtype': 'Select',
+ 'options': '\nExpense\nIncome',
+ 'hidden': 1,
+ 'in_list_view': 0,
+ 'insert_after': 'user_remark'
+ },
+ {
+ 'fieldname': 'customer_name_etaxes',
+ 'label': 'Customer Name (E-Taxes)',
+ 'fieldtype': 'Data',
+ 'hidden': 1,
+ 'in_list_view': 0,
+ 'insert_after': 'expense_income_type'
+ },
+ {
+ 'fieldname': 'etaxes_document_type',
+ 'label': 'E-Taxes Document Type',
+ 'fieldtype': 'Select',
+ 'options': '\nədv',
+ 'hidden': 1,
+ 'in_list_view': 1,
+ 'insert_after': 'customer_name_etaxes'
+ },
+ {
+ 'fieldname': 'loaded_from_etaxes',
+ 'label': 'Loaded from E-Taxes',
+ 'fieldtype': 'Check',
+ 'default': '0',
+ 'hidden': 1,
+ 'in_list_view': 0,
+ 'insert_after': 'etaxes_document_type'
+ }
+ ]
+ }
+
+ frappe.logger().info("Creating/updating custom fields for Journal Entry")
+ create_custom_fields(custom_fields, update=True)
+
+ # Clear cache for Journal Entry
+ frappe.clear_cache(doctype='Journal Entry')
+ frappe.logger().info("Custom fields created/updated successfully for Journal Entry")
+
+
+def create_supplier_custom_fields():
+ """Create custom fields for Supplier doctype"""
+
+ custom_fields = {
+ 'Supplier': [
+ {
+ 'fieldname': 'patronimyc',
+ 'label': 'Patronymic (Father\'s Name)',
+ 'fieldtype': 'Data',
+ 'insert_after': 'first_name_individual',
+ 'depends_on': 'eval:doc.supplier_type=="Individual"'
+ }
+ ]
+ }
+
+ frappe.logger().info("Creating/updating custom fields for Supplier")
+ create_custom_fields(custom_fields, update=True)
+
+ # Clear cache for Supplier
+ frappe.clear_cache(doctype='Supplier')
+ frappe.logger().info("Custom fields created/updated successfully for Supplier")
diff --git a/invoice_az/patches.txt b/invoice_az/patches.txt
deleted file mode 100644
index 43aa329..0000000
--- a/invoice_az/patches.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-[pre_model_sync]
-# Patches added in this section will be executed before doctypes are migrated
-# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
-
-[post_model_sync]
-# Patches added in this section will be executed after doctypes are migrated
-invoice_az.patches.v1_0.rename_agricultural_to_purchase_type
-# TEMPORARY: cleanup_agricultural_fields is now executed via hooks.py after_migrate()
-# invoice_az.patches.cleanup_agricultural_fields
\ No newline at end of file
diff --git a/invoice_az/patches/__init__.py b/invoice_az/patches/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/invoice_az/patches/cleanup_agricultural_fields.py b/invoice_az/patches/cleanup_agricultural_fields.py
deleted file mode 100644
index 6897d2e..0000000
--- a/invoice_az/patches/cleanup_agricultural_fields.py
+++ /dev/null
@@ -1,98 +0,0 @@
-import frappe
-
-
-def execute():
- """Remove old agricultural fields and hide E-Taxes Purchase Act ID field"""
-
- # List of old fields to delete (already migrated to new fields)
- # These fields were migrated by rename_agricultural_to_purchase_type patch:
- # - agricultural_goods → purchase_type
- # - purchase_confirmation_doc_type → act_type
- # - agricultural_country was never used and should be removed
- old_fields_to_delete = [
- "agricultural_goods",
- "agricultural_country",
- "purchase_confirmation_doc_type"
- ]
-
- # Doctypes to clean up
- doctypes = ["Purchase Invoice", "Purchase Order"]
-
- # Delete old custom fields
- deleted_count = 0
- for doctype in doctypes:
- for fieldname in old_fields_to_delete:
- try:
- field_name = f"{doctype}-{fieldname}"
- if frappe.db.exists("Custom Field", field_name):
- frappe.delete_doc("Custom Field", field_name, force=True, ignore_permissions=True)
- deleted_count += 1
- print(f"Deleted custom field: {field_name}")
- except Exception as e:
- print(f"Error deleting {field_name}: {str(e)}")
-
- if deleted_count > 0:
- frappe.db.commit()
- print(f"Committed deletion of {deleted_count} fields")
-
- # Hide E-Taxes Purchase Act ID field in Purchase Invoice
- try:
- field_name = "Purchase Invoice-etaxes_purchase_id"
- if frappe.db.exists("Custom Field", field_name):
- field_doc = frappe.get_doc("Custom Field", field_name)
- if not field_doc.hidden:
- field_doc.hidden = 1
- field_doc.flags.ignore_validate = True
- field_doc.save(ignore_permissions=True)
- frappe.db.commit()
- print(f"Hidden field: {field_name}")
- else:
- print(f"Field already hidden: {field_name}")
- except Exception as e:
- print(f"Error hiding etaxes_purchase_id: {str(e)}")
-
- # Fix section placement to prevent standard ERPNext fields (is_return, apply_tds, amended_from)
- # from appearing in the E-Taxes section
- # Correct order:
- # amended_from (standard)
- # is_taxes_doc (custom)
- # purchase_type (custom)
- # act_type (custom)
- # etaxes_agricultural_section (Section Break)
- # act_kind, etaxes_purchase_id, etc.
-
- # Move is_taxes_doc to after amended_from
- try:
- is_taxes_field_name = "Purchase Invoice-is_taxes_doc"
- if frappe.db.exists("Custom Field", is_taxes_field_name):
- is_taxes_doc = frappe.get_doc("Custom Field", is_taxes_field_name)
- if is_taxes_doc.insert_after != "amended_from":
- is_taxes_doc.insert_after = "amended_from"
- is_taxes_doc.flags.ignore_validate = True
- is_taxes_doc.save(ignore_permissions=True)
- frappe.db.commit()
- print(f"Moved is_taxes_doc to after 'amended_from': {is_taxes_field_name}")
- else:
- print(f"is_taxes_doc already in correct position: {is_taxes_field_name}")
- except Exception as e:
- print(f"Error moving is_taxes_doc: {str(e)}")
-
- # Move etaxes_agricultural_section to after act_type (not amended_from!)
- try:
- section_field_name = "Purchase Invoice-etaxes_agricultural_section"
- if frappe.db.exists("Custom Field", section_field_name):
- section_doc = frappe.get_doc("Custom Field", section_field_name)
- if section_doc.insert_after != "act_type":
- section_doc.insert_after = "act_type"
- section_doc.flags.ignore_validate = True
- section_doc.save(ignore_permissions=True)
- frappe.db.commit()
- print(f"Moved section to after 'act_type': {section_field_name}")
- else:
- print(f"Section already in correct position: {section_field_name}")
- except Exception as e:
- print(f"Error moving section: {str(e)}")
-
- # Clear cache to apply changes
- frappe.clear_cache()
- print("Cache cleared. Old agricultural fields removed, E-Taxes Purchase Act ID hidden, and section placement fixed.")
diff --git a/invoice_az/patches/v1_0/__init__.py b/invoice_az/patches/v1_0/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/invoice_az/patches/v1_0/rename_agricultural_to_purchase_type.py b/invoice_az/patches/v1_0/rename_agricultural_to_purchase_type.py
deleted file mode 100644
index 3c4d3e9..0000000
--- a/invoice_az/patches/v1_0/rename_agricultural_to_purchase_type.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import frappe
-
-def execute():
- """
- Migrate agricultural fields to purchase_type fields
-
- This migration renames:
- - agricultural_goods → purchase_type (Purchase Order, Purchase Invoice)
- - purchase_confirmation_doc_type → act_type (Purchase Order, Purchase Invoice)
- - agricultural_tax_amount → purchase_tax_amount (Purchase Invoice Item)
- """
-
- frappe.logger().info("Starting migration: rename_agricultural_to_purchase_type")
-
- # Purchase Order - migrate agricultural_goods to purchase_type
- if frappe.db.has_column("Purchase Order", "agricultural_goods"):
- frappe.logger().info("Migrating Purchase Order: agricultural_goods → purchase_type")
- frappe.db.sql("""
- UPDATE `tabPurchase Order`
- SET purchase_type = agricultural_goods
- WHERE agricultural_goods IS NOT NULL
- """)
- frappe.logger().info("Purchase Order: agricultural_goods migration complete")
-
- # Purchase Order - migrate purchase_confirmation_doc_type to act_type
- if frappe.db.has_column("Purchase Order", "purchase_confirmation_doc_type"):
- frappe.logger().info("Migrating Purchase Order: purchase_confirmation_doc_type → act_type")
- frappe.db.sql("""
- UPDATE `tabPurchase Order`
- SET act_type = purchase_confirmation_doc_type
- WHERE purchase_confirmation_doc_type IS NOT NULL
- """)
- frappe.logger().info("Purchase Order: purchase_confirmation_doc_type migration complete")
-
- # Purchase Invoice - migrate agricultural_goods to purchase_type
- if frappe.db.has_column("Purchase Invoice", "agricultural_goods"):
- frappe.logger().info("Migrating Purchase Invoice: agricultural_goods → purchase_type")
- frappe.db.sql("""
- UPDATE `tabPurchase Invoice`
- SET purchase_type = agricultural_goods
- WHERE agricultural_goods IS NOT NULL
- """)
- frappe.logger().info("Purchase Invoice: agricultural_goods migration complete")
-
- # Purchase Invoice - migrate purchase_confirmation_doc_type to act_type
- if frappe.db.has_column("Purchase Invoice", "purchase_confirmation_doc_type"):
- frappe.logger().info("Migrating Purchase Invoice: purchase_confirmation_doc_type → act_type")
- frappe.db.sql("""
- UPDATE `tabPurchase Invoice`
- SET act_type = purchase_confirmation_doc_type
- WHERE purchase_confirmation_doc_type IS NOT NULL
- """)
- frappe.logger().info("Purchase Invoice: purchase_confirmation_doc_type migration complete")
-
- # Purchase Invoice Item - migrate agricultural_tax_amount to purchase_tax_amount
- if frappe.db.has_column("Purchase Invoice Item", "agricultural_tax_amount"):
- frappe.logger().info("Migrating Purchase Invoice Item: agricultural_tax_amount → purchase_tax_amount")
- frappe.db.sql("""
- UPDATE `tabPurchase Invoice Item`
- SET purchase_tax_amount = agricultural_tax_amount
- WHERE agricultural_tax_amount IS NOT NULL
- """)
- frappe.logger().info("Purchase Invoice Item: agricultural_tax_amount migration complete")
-
- # Commit changes
- frappe.db.commit()
-
- frappe.logger().info("Migration completed successfully: rename_agricultural_to_purchase_type")
- print("Migration completed: agricultural fields renamed to purchase_type")
diff --git a/invoice_az/send_purchase_api.py b/invoice_az/send_purchase_api.py
index ed41179..b97429c 100644
--- a/invoice_az/send_purchase_api.py
+++ b/invoice_az/send_purchase_api.py
@@ -934,13 +934,25 @@ def build_act_payload(doc, serial_number):
# Get supplier details
supplier = frappe.get_doc("Supplier", doc.supplier)
+ # Build full name from individual fields (first name + patronymic + last name)
+ # E-Taxes API requires exact match with their database
+ full_name_parts = []
+ if supplier.first_name_individual:
+ full_name_parts.append(supplier.first_name_individual)
+ if supplier.patronimyc:
+ full_name_parts.append(supplier.patronimyc)
+ if supplier.last_name_individual:
+ full_name_parts.append(supplier.last_name_individual)
+
+ full_name = " ".join(full_name_parts) if full_name_parts else supplier.supplier_name
+
# Build seller object (physical person)
# IMPORTANT: Only include required fields (fin, passportSerialNumber, fullName, address)
# Extra fields (dateOfBirth, firstname, lastname, phoneNumber) cause API rejection
seller = {
"fin": supplier.fin,
"passportSerialNumber": supplier.passport_serial_number,
- "fullName": supplier.supplier_name,
+ "fullName": full_name,
"address": "" # Empty string instead of None
}
diff --git a/invoice_az/supplier_api.py b/invoice_az/supplier_api.py
index 5b90360..58e1cc9 100644
--- a/invoice_az/supplier_api.py
+++ b/invoice_az/supplier_api.py
@@ -211,16 +211,17 @@ def update_supplier_from_etaxes(supplier_name):
supplier.last_name_individual = data.get("lastname")
updated_fields.append("Last Name")
+ # Patronymic (middle name / father's name)
+ if data.get("patronimyc"):
+ supplier.patronimyc = data.get("patronimyc")
+ updated_fields.append("Patronymic")
+
# Phone number (optional)
if data.get("phone") and not supplier.phone_number_individual:
supplier.phone_number_individual = data.get("phone")
updated_fields.append("Phone Number")
- # Update supplier name if needed (use fullName from E-Taxes)
- if data.get("fullName") and supplier.supplier_name != data.get("fullName"):
- old_name = supplier.supplier_name
- supplier.supplier_name = data.get("fullName")
- updated_fields.append(f"Supplier Name (from '{old_name}' to '{data.get('fullName')}')")
+ # Note: Supplier Name is not updated to preserve user's custom name
# Save supplier
supplier.save()
diff --git a/invoice_az/vat_api.py b/invoice_az/vat_api.py
index d059c71..d9fc6d5 100644
--- a/invoice_az/vat_api.py
+++ b/invoice_az/vat_api.py
@@ -749,6 +749,8 @@ def create_journal_entry_from_vat_operation(operation_data, company, create_as_d
je.user_remark = f"VAT Operation: {operation_data.get('explanation', '')}"
je.expense_income_type = expense_income_type
je.customer_name_etaxes = operation_data.get('name', '')
+ je.etaxes_document_type = 'ədv' # Always ədv for VAT operations
+ je.loaded_from_etaxes = 1 # Flag that this was loaded from e-taxes
# Строка 1: Дебет
debit_entry = {