This commit is contained in:
Ali 2026-01-30 20:38:24 +04:00
parent 44bed2e82f
commit 061a71f93d
12 changed files with 121 additions and 186 deletions

View File

@ -305,8 +305,8 @@ bench --site site1 clear-cache
# 4. Restart bench (if needed) # 4. Restart bench (if needed)
bench restart bench restart
# 5. (Optional) Run data migration script # 5. (Optional) Run data migration script (if needed)
# bench --site site1 execute invoice_az.patches.migrate_tax_templates # Create and execute migration function directly
``` ```
--- ---

View File

@ -1405,7 +1405,9 @@ VATETaxes.import = {
// Journal Entry List // Journal Entry List
frappe.listview_settings['Journal Entry'] = { 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) { get_indicator: function(doc) {
if (doc.docstatus == 1) { if (doc.docstatus == 1) {
@ -1425,6 +1427,14 @@ frappe.listview_settings['Journal Entry'] = {
return '<span class="indicator-pill red">' + value + '</span>'; return '<span class="indicator-pill red">' + value + '</span>';
} }
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 '<span class="indicator-pill blue" title="' + __('VAT Operation from E-Taxes') + '">ƏDV</span>';
}
return '';
} }
}, },

View File

@ -75,10 +75,11 @@ def after_install():
def after_migrate(): def after_migrate():
"""Run migration tasks""" """Run migration tasks"""
from invoice_az.auth import setup_token_renewal 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() setup_token_renewal()
create_journal_entry_custom_fields() create_journal_entry_custom_fields()
create_supplier_custom_fields()
# Fixtures for master data # Fixtures for master data
# ------------------------ # ------------------------

85
invoice_az/install.py Normal file
View File

@ -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")

View File

@ -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

View File

@ -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.")

View File

@ -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")

View File

@ -934,13 +934,25 @@ def build_act_payload(doc, serial_number):
# Get supplier details # Get supplier details
supplier = frappe.get_doc("Supplier", doc.supplier) 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) # Build seller object (physical person)
# IMPORTANT: Only include required fields (fin, passportSerialNumber, fullName, address) # IMPORTANT: Only include required fields (fin, passportSerialNumber, fullName, address)
# Extra fields (dateOfBirth, firstname, lastname, phoneNumber) cause API rejection # Extra fields (dateOfBirth, firstname, lastname, phoneNumber) cause API rejection
seller = { seller = {
"fin": supplier.fin, "fin": supplier.fin,
"passportSerialNumber": supplier.passport_serial_number, "passportSerialNumber": supplier.passport_serial_number,
"fullName": supplier.supplier_name, "fullName": full_name,
"address": "" # Empty string instead of None "address": "" # Empty string instead of None
} }

View File

@ -211,16 +211,17 @@ def update_supplier_from_etaxes(supplier_name):
supplier.last_name_individual = data.get("lastname") supplier.last_name_individual = data.get("lastname")
updated_fields.append("Last Name") 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) # Phone number (optional)
if data.get("phone") and not supplier.phone_number_individual: if data.get("phone") and not supplier.phone_number_individual:
supplier.phone_number_individual = data.get("phone") supplier.phone_number_individual = data.get("phone")
updated_fields.append("Phone Number") updated_fields.append("Phone Number")
# Update supplier name if needed (use fullName from E-Taxes) # Note: Supplier Name is not updated to preserve user's custom name
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')}')")
# Save supplier # Save supplier
supplier.save() supplier.save()

View File

@ -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.user_remark = f"VAT Operation: {operation_data.get('explanation', '')}"
je.expense_income_type = expense_income_type je.expense_income_type = expense_income_type
je.customer_name_etaxes = operation_data.get('name', '') 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: Дебет # Строка 1: Дебет
debit_entry = { debit_entry = {