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 = {