added tax articles to sales invoice, and tax calculations
This commit is contained in:
parent
e3e6a3ba70
commit
b032b6fb85
|
|
@ -1,196 +1,102 @@
|
||||||
# Файл: jey_erp/custom/sales_invoice_vat.py
|
# Файл: jey_erp/custom/sales_invoice_vat.py
|
||||||
|
# Упрощенная версия без сложной логики
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import json
|
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
def set_vat_fields_permissions(doc, method=None):
|
def on_sales_invoice_validate(doc, method=None):
|
||||||
"""
|
"""
|
||||||
Set VAT fields permissions based on Item Tax Template
|
Hook для Sales Invoice validate
|
||||||
Called on Sales Invoice validate/before_save
|
Простая очистка неиспользуемых полей
|
||||||
"""
|
"""
|
||||||
if doc.doctype != "Sales Invoice":
|
if doc.doctype != "Sales Invoice":
|
||||||
return
|
return
|
||||||
|
|
||||||
for item in doc.get("items", []):
|
for item in doc.get("items", []):
|
||||||
set_item_vat_permissions(item)
|
clean_vat_fields_for_item(item)
|
||||||
|
|
||||||
def set_item_vat_permissions(item):
|
def clean_vat_fields_for_item(item):
|
||||||
"""
|
"""
|
||||||
Set VAT field permissions for individual item based on tax template
|
Простая очистка VAT полей на основе налогового шаблона
|
||||||
"""
|
"""
|
||||||
if not item.get("item_code"):
|
if not item.get("item_code"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get item tax template
|
tax_template = item.get("item_tax_template")
|
||||||
item_tax_template = item.get("item_tax_template")
|
|
||||||
|
|
||||||
# If no tax template in item row, get from Item master
|
# Очищаем все поля по умолчанию
|
||||||
if not item_tax_template:
|
vat_fields = {
|
||||||
try:
|
'vat_18_percent_with_amount': 0,
|
||||||
item_doc = frappe.get_doc("Item", item.item_code)
|
'vat_amount': 0,
|
||||||
item_tax_template = item_doc.get("item_tax_template")
|
'vat_0_percent_with_amount': 0,
|
||||||
except Exception:
|
'amount_without_vat': 0,
|
||||||
pass
|
'vat_free_amount': 0,
|
||||||
|
'tax_article': None
|
||||||
# Default state - all fields disabled
|
|
||||||
vat_permissions = {
|
|
||||||
'vat_18_percent_with_amount': False,
|
|
||||||
'vat_amount': False,
|
|
||||||
'vat_0_percent_with_amount': False,
|
|
||||||
'amount_without_vat': False,
|
|
||||||
'vat_free_amount': False
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if item_tax_template == "ƏDV 18%":
|
# Определяем какие поля оставить
|
||||||
# 18% VAT template
|
|
||||||
vat_permissions['vat_18_percent_with_amount'] = True
|
|
||||||
vat_permissions['vat_amount'] = True # Показывать VAT Amount только для 18%
|
|
||||||
elif item_tax_template == "ƏDV 0%":
|
|
||||||
# 0% VAT template
|
|
||||||
vat_permissions['vat_0_percent_with_amount'] = True
|
|
||||||
elif item_tax_template == "ƏDV-dən azadolma":
|
|
||||||
# VAT exempt template
|
|
||||||
vat_permissions['vat_free_amount'] = True
|
|
||||||
elif not item_tax_template:
|
|
||||||
# No tax template - not subject to VAT
|
|
||||||
vat_permissions['amount_without_vat'] = True
|
|
||||||
else:
|
|
||||||
# Any other template - default to amount_without_vat
|
|
||||||
vat_permissions['amount_without_vat'] = True
|
|
||||||
|
|
||||||
# Apply permissions to item
|
|
||||||
apply_vat_permissions_to_item(item, vat_permissions)
|
|
||||||
|
|
||||||
def apply_vat_permissions_to_item(item, permissions):
|
|
||||||
"""
|
|
||||||
Apply VAT field permissions to item and clear disabled fields
|
|
||||||
"""
|
|
||||||
# Clear values for disabled fields
|
|
||||||
for field, enabled in permissions.items():
|
|
||||||
if not enabled and hasattr(item, field):
|
|
||||||
setattr(item, field, 0.0)
|
|
||||||
|
|
||||||
# Calculate amounts for enabled fields
|
|
||||||
item_amount = flt(item.get("amount", 0))
|
item_amount = flt(item.get("amount", 0))
|
||||||
|
|
||||||
for field, enabled in permissions.items():
|
if tax_template == "ƏDV 18%":
|
||||||
if enabled and hasattr(item, field):
|
# Для 18% НДС
|
||||||
# Only auto-fill if field is empty (except vat_amount which is always calculated)
|
if not item.get('vat_18_percent_with_amount'):
|
||||||
current_value = getattr(item, field, 0)
|
vat_fields['vat_18_percent_with_amount'] = item_amount * 1.18
|
||||||
if not current_value or current_value == 0 or field == 'vat_amount':
|
|
||||||
if field == 'vat_18_percent_with_amount':
|
|
||||||
# For 18% VAT: Amount + 18%
|
|
||||||
vat_amount = item_amount * 1.18
|
|
||||||
setattr(item, field, vat_amount)
|
|
||||||
elif field == 'vat_amount':
|
|
||||||
# Calculate VAT Amount: vat_18_percent_with_amount - amount
|
|
||||||
vat_18_total = flt(item.get('vat_18_percent_with_amount', 0))
|
|
||||||
if vat_18_total == 0:
|
|
||||||
# If vat_18_percent_with_amount is not set, calculate it first
|
|
||||||
vat_18_total = item_amount * 1.18
|
|
||||||
setattr(item, 'vat_18_percent_with_amount', vat_18_total)
|
|
||||||
vat_only = vat_18_total - item_amount
|
|
||||||
setattr(item, field, vat_only)
|
|
||||||
elif field == 'vat_0_percent_with_amount':
|
|
||||||
# For 0% VAT: just the amount
|
|
||||||
setattr(item, field, item_amount)
|
|
||||||
elif field == 'amount_without_vat':
|
|
||||||
# For VAT exempt: auto-fill with item amount
|
|
||||||
setattr(item, field, item_amount)
|
|
||||||
elif field == 'vat_free_amount':
|
|
||||||
# For "not subject to VAT": auto-fill with item amount
|
|
||||||
setattr(item, field, item_amount)
|
|
||||||
|
|
||||||
# Store permissions in item for client-side use
|
|
||||||
item.vat_permissions = json.dumps(permissions)
|
|
||||||
|
|
||||||
def calculate_vat_amount(item):
|
|
||||||
"""
|
|
||||||
Calculate VAT Amount whenever vat_18_percent_with_amount or amount changes
|
|
||||||
This function can be called from hooks or client-side triggers
|
|
||||||
"""
|
|
||||||
if hasattr(item, 'vat_18_percent_with_amount') and hasattr(item, 'amount'):
|
|
||||||
vat_18_total = flt(item.get('vat_18_percent_with_amount', 0))
|
|
||||||
item_amount = flt(item.get('amount', 0))
|
|
||||||
|
|
||||||
if vat_18_total > 0 and item_amount > 0:
|
|
||||||
vat_only = vat_18_total - item_amount
|
|
||||||
setattr(item, 'vat_amount', vat_only)
|
|
||||||
else:
|
else:
|
||||||
setattr(item, 'vat_amount', 0.0)
|
vat_fields['vat_18_percent_with_amount'] = flt(item.get('vat_18_percent_with_amount'))
|
||||||
|
|
||||||
@frappe.whitelist()
|
vat_fields['vat_amount'] = vat_fields['vat_18_percent_with_amount'] - item_amount
|
||||||
def get_vat_permissions_for_item(item_code, item_tax_template=None):
|
vat_fields['tax_article'] = None # Очищаем
|
||||||
"""
|
|
||||||
API method to get VAT permissions for an item
|
|
||||||
Called from client side when item changes
|
|
||||||
"""
|
|
||||||
# Create temporary item object
|
|
||||||
temp_item = frappe._dict({
|
|
||||||
'item_code': item_code,
|
|
||||||
'item_tax_template': item_tax_template
|
|
||||||
})
|
|
||||||
|
|
||||||
# Calculate permissions
|
elif tax_template == "ƏDV 0%":
|
||||||
set_item_vat_permissions(temp_item)
|
# Для 0% НДС
|
||||||
|
vat_fields['vat_0_percent_with_amount'] = item_amount
|
||||||
|
# tax_article оставляем как есть
|
||||||
|
if hasattr(item, 'tax_article') and item.tax_article:
|
||||||
|
vat_fields['tax_article'] = item.tax_article
|
||||||
|
|
||||||
|
elif tax_template == "ƏDV-dən azadolma":
|
||||||
|
# Для освобожденных от НДС
|
||||||
|
vat_fields['vat_free_amount'] = item_amount
|
||||||
|
# tax_article оставляем как есть
|
||||||
|
if hasattr(item, 'tax_article') and item.tax_article:
|
||||||
|
vat_fields['tax_article'] = item.tax_article
|
||||||
|
|
||||||
# Return permissions
|
|
||||||
if hasattr(temp_item, 'vat_permissions'):
|
|
||||||
return json.loads(temp_item.vat_permissions)
|
|
||||||
else:
|
else:
|
||||||
# Default fallback
|
# Без НДС
|
||||||
return {
|
vat_fields['amount_without_vat'] = item_amount
|
||||||
'vat_18_percent_with_amount': False,
|
vat_fields['tax_article'] = None # Очищаем
|
||||||
'vat_amount': False,
|
|
||||||
'vat_0_percent_with_amount': False,
|
|
||||||
'amount_without_vat': True,
|
|
||||||
'vat_free_amount': False
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# Применяем значения
|
||||||
|
for field, value in vat_fields.items():
|
||||||
|
if hasattr(item, field):
|
||||||
|
setattr(item, field, value)
|
||||||
|
|
||||||
|
# Простой API метод если нужен (необязательно)
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def calculate_vat_amount_api(item_data):
|
def get_vat_template_info(item_tax_template):
|
||||||
"""
|
"""
|
||||||
API method to calculate VAT amount from client side
|
Простая информация о налоговом шаблоне
|
||||||
"""
|
"""
|
||||||
try:
|
template_info = {
|
||||||
if isinstance(item_data, str):
|
"ƏDV 18%": {
|
||||||
item_data = json.loads(item_data)
|
"name": "VAT 18%",
|
||||||
|
"rate": 18,
|
||||||
vat_18_total = flt(item_data.get('vat_18_percent_with_amount', 0))
|
"allows_tax_article": False
|
||||||
item_amount = flt(item_data.get('amount', 0))
|
},
|
||||||
|
"ƏDV 0%": {
|
||||||
if vat_18_total > 0 and item_amount > 0:
|
"name": "VAT 0%",
|
||||||
vat_only = vat_18_total - item_amount
|
"rate": 0,
|
||||||
return vat_only
|
"allows_tax_article": True
|
||||||
else:
|
},
|
||||||
return 0.0
|
"ƏDV-dən azadolma": {
|
||||||
except Exception as e:
|
"name": "VAT Exempt",
|
||||||
frappe.log_error(f"Error calculating VAT amount: {str(e)}")
|
"rate": 0,
|
||||||
return 0.0
|
"allows_tax_article": True
|
||||||
|
}
|
||||||
# Hook functions for automatic calculation
|
}
|
||||||
def on_sales_invoice_item_validate(doc, method=None):
|
|
||||||
"""
|
|
||||||
Hook to be called on Sales Invoice Item validate
|
|
||||||
Automatically calculates VAT amount when item is saved
|
|
||||||
"""
|
|
||||||
if hasattr(doc, 'vat_18_percent_with_amount') and hasattr(doc, 'amount'):
|
|
||||||
calculate_vat_amount(doc)
|
|
||||||
|
|
||||||
def on_sales_invoice_validate(doc, method=None):
|
|
||||||
"""
|
|
||||||
Hook to be called on Sales Invoice validate
|
|
||||||
Ensures all VAT calculations are correct
|
|
||||||
"""
|
|
||||||
if doc.doctype != "Sales Invoice":
|
|
||||||
return
|
|
||||||
|
|
||||||
for item in doc.get("items", []):
|
|
||||||
# Set permissions first
|
|
||||||
set_item_vat_permissions(item)
|
|
||||||
|
|
||||||
# Then calculate VAT amount if needed
|
|
||||||
if hasattr(item, 'vat_18_percent_with_amount') and hasattr(item, 'amount'):
|
|
||||||
calculate_vat_amount(item)
|
|
||||||
|
|
||||||
|
return template_info.get(item_tax_template, {
|
||||||
|
"name": "No VAT",
|
||||||
|
"rate": 0,
|
||||||
|
"allows_tax_article": False
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
# your_app/doctype/sales_invoice/sales_invoice.py
|
||||||
|
# Добавить в существующий класс Sales Invoice или создать custom app
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
|
||||||
|
|
||||||
|
class CustomSalesInvoice(SalesInvoice):
|
||||||
|
def on_update_after_submit(self):
|
||||||
|
super().on_update_after_submit()
|
||||||
|
|
||||||
|
def calculate_taxes_and_totals(self):
|
||||||
|
# Сначала стандартные расчеты ERPNext
|
||||||
|
super().calculate_taxes_and_totals()
|
||||||
|
# Потом наши VAT расчеты
|
||||||
|
self.calculate_vat_fields()
|
||||||
|
|
||||||
|
def calculate_vat_fields(self):
|
||||||
|
"""Рассчитывает VAT поля для всех строк"""
|
||||||
|
if not self.items:
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in self.items:
|
||||||
|
self.calculate_item_vat_fields(item)
|
||||||
|
|
||||||
|
def calculate_item_vat_fields(self, item):
|
||||||
|
"""Рассчитывает VAT поля для одной строки"""
|
||||||
|
if not item.item_code or flt(item.amount) <= 0:
|
||||||
|
self.clear_item_vat_fields(item)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Очищаем все VAT поля перед новым расчетом
|
||||||
|
self.clear_item_vat_fields(item)
|
||||||
|
|
||||||
|
# Блокируем tax_article если не разрешено
|
||||||
|
self.handle_tax_article_permission(item)
|
||||||
|
|
||||||
|
# Рассчитываем поля в зависимости от налогового шаблона
|
||||||
|
amount = flt(item.amount)
|
||||||
|
tax_template = item.item_tax_template
|
||||||
|
|
||||||
|
if tax_template == "ƏDV 18%":
|
||||||
|
self.calculate_18_percent_vat(item, amount)
|
||||||
|
elif tax_template == "ƏDV 0%":
|
||||||
|
self.calculate_0_percent_vat(item, amount)
|
||||||
|
elif tax_template == "ƏDV-dən azadolma":
|
||||||
|
self.calculate_vat_free(item, amount)
|
||||||
|
else:
|
||||||
|
self.calculate_without_vat(item, amount)
|
||||||
|
|
||||||
|
def clear_item_vat_fields(self, item):
|
||||||
|
"""Очищает все VAT поля строки"""
|
||||||
|
vat_fields = [
|
||||||
|
'vat_18_percent_with_amount',
|
||||||
|
'vat_amount',
|
||||||
|
'vat_0_percent_with_amount',
|
||||||
|
'amount_without_vat',
|
||||||
|
'vat_free_amount'
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in vat_fields:
|
||||||
|
if hasattr(item, field):
|
||||||
|
setattr(item, field, 0)
|
||||||
|
|
||||||
|
def handle_tax_article_permission(self, item):
|
||||||
|
"""Обрабатывает разрешения для поля tax_article"""
|
||||||
|
tax_template = item.item_tax_template
|
||||||
|
is_allowed = tax_template in ["ƏDV 0%", "ƏDV-dən azadolma"]
|
||||||
|
|
||||||
|
# Очищаем tax_article если оно не должно быть доступно
|
||||||
|
if not is_allowed and item.tax_article:
|
||||||
|
item.tax_article = None
|
||||||
|
|
||||||
|
def calculate_18_percent_vat(self, item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV 18%"""
|
||||||
|
vat_18_total = amount * 1.18
|
||||||
|
vat_amount = vat_18_total - amount
|
||||||
|
|
||||||
|
item.vat_18_percent_with_amount = vat_18_total
|
||||||
|
item.vat_amount = vat_amount
|
||||||
|
|
||||||
|
def calculate_0_percent_vat(self, item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV 0%"""
|
||||||
|
item.vat_0_percent_with_amount = amount
|
||||||
|
|
||||||
|
def calculate_vat_free(self, item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV-dən azadolma"""
|
||||||
|
item.vat_free_amount = amount
|
||||||
|
|
||||||
|
def calculate_without_vat(self, item, amount):
|
||||||
|
"""Рассчитывает поля для случаев без НДС"""
|
||||||
|
item.amount_without_vat = amount
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSalesOrder(SalesOrder):
|
||||||
|
def on_update_after_submit(self):
|
||||||
|
super().on_update_after_submit()
|
||||||
|
|
||||||
|
def calculate_taxes_and_totals(self):
|
||||||
|
# Сначала стандартные расчеты ERPNext
|
||||||
|
super().calculate_taxes_and_totals()
|
||||||
|
# Потом наши VAT расчеты
|
||||||
|
self.calculate_vat_fields()
|
||||||
|
|
||||||
|
def calculate_vat_fields(self):
|
||||||
|
"""Рассчитывает VAT поля для всех строк"""
|
||||||
|
if not self.items:
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in self.items:
|
||||||
|
self.calculate_item_vat_fields(item)
|
||||||
|
|
||||||
|
def calculate_item_vat_fields(self, item):
|
||||||
|
"""Рассчитывает VAT поля для одной строки"""
|
||||||
|
if not item.item_code or flt(item.amount) <= 0:
|
||||||
|
self.clear_item_vat_fields(item)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Очищаем все VAT поля перед новым расчетом
|
||||||
|
self.clear_item_vat_fields(item)
|
||||||
|
|
||||||
|
# Блокируем tax_article если не разрешено
|
||||||
|
self.handle_tax_article_permission(item)
|
||||||
|
|
||||||
|
# Рассчитываем поля в зависимости от налогового шаблона
|
||||||
|
amount = flt(item.amount)
|
||||||
|
tax_template = item.item_tax_template
|
||||||
|
|
||||||
|
if tax_template == "ƏDV 18%":
|
||||||
|
self.calculate_18_percent_vat(item, amount)
|
||||||
|
elif tax_template == "ƏDV 0%":
|
||||||
|
self.calculate_0_percent_vat(item, amount)
|
||||||
|
elif tax_template == "ƏDV-dən azadolma":
|
||||||
|
self.calculate_vat_free(item, amount)
|
||||||
|
else:
|
||||||
|
self.calculate_without_vat(item, amount)
|
||||||
|
|
||||||
|
def clear_item_vat_fields(self, item):
|
||||||
|
"""Очищает все VAT поля строки"""
|
||||||
|
vat_fields = [
|
||||||
|
'vat_18_percent_with_amount',
|
||||||
|
'vat_amount',
|
||||||
|
'vat_0_percent_with_amount',
|
||||||
|
'amount_without_vat',
|
||||||
|
'vat_free_amount'
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in vat_fields:
|
||||||
|
if hasattr(item, field):
|
||||||
|
setattr(item, field, 0)
|
||||||
|
|
||||||
|
def handle_tax_article_permission(self, item):
|
||||||
|
"""Обрабатывает разрешения для поля tax_article"""
|
||||||
|
tax_template = item.item_tax_template
|
||||||
|
is_allowed = tax_template in ["ƏDV 0%", "ƏDV-dən azadolma"]
|
||||||
|
|
||||||
|
# Очищаем tax_article если оно не должно быть доступно
|
||||||
|
if not is_allowed and item.tax_article:
|
||||||
|
item.tax_article = None
|
||||||
|
|
||||||
|
def calculate_18_percent_vat(self, item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV 18%"""
|
||||||
|
vat_18_total = amount * 1.18
|
||||||
|
vat_amount = vat_18_total - amount
|
||||||
|
|
||||||
|
item.vat_18_percent_with_amount = vat_18_total
|
||||||
|
item.vat_amount = vat_amount
|
||||||
|
|
||||||
|
def calculate_0_percent_vat(self, item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV 0%"""
|
||||||
|
item.vat_0_percent_with_amount = amount
|
||||||
|
|
||||||
|
def calculate_vat_free(self, item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV-dən azadolma"""
|
||||||
|
item.vat_free_amount = amount
|
||||||
|
|
||||||
|
def calculate_without_vat(self, item, amount):
|
||||||
|
"""Рассчитывает поля для случаев без НДС"""
|
||||||
|
item.amount_without_vat = amount
|
||||||
|
|
||||||
|
|
||||||
|
# Альтернативный способ через monkey patching, если не хотите наследовать классы
|
||||||
|
def patch_sales_documents():
|
||||||
|
"""Патчит существующие классы Sales Invoice и Sales Order"""
|
||||||
|
|
||||||
|
# Патчим Sales Invoice
|
||||||
|
original_calc_taxes_invoice = SalesInvoice.calculate_taxes_and_totals
|
||||||
|
|
||||||
|
def new_calc_taxes_invoice(self):
|
||||||
|
original_calc_taxes_invoice(self)
|
||||||
|
calculate_vat_fields_for_doc(self)
|
||||||
|
|
||||||
|
SalesInvoice.calculate_taxes_and_totals = new_calc_taxes_invoice
|
||||||
|
|
||||||
|
# Патчим Sales Order
|
||||||
|
original_calc_taxes_order = SalesOrder.calculate_taxes_and_totals
|
||||||
|
|
||||||
|
def new_calc_taxes_order(self):
|
||||||
|
original_calc_taxes_order(self)
|
||||||
|
calculate_vat_fields_for_doc(self)
|
||||||
|
|
||||||
|
SalesOrder.calculate_taxes_and_totals = new_calc_taxes_order
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_vat_fields_for_doc(doc):
|
||||||
|
"""Универсальная функция для расчета VAT полей"""
|
||||||
|
if not doc.items:
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in doc.items:
|
||||||
|
if not item.item_code or flt(item.amount) <= 0:
|
||||||
|
clear_vat_fields(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Очищаем все VAT поля перед новым расчетом
|
||||||
|
clear_vat_fields(item)
|
||||||
|
|
||||||
|
# Блокируем tax_article если не разрешено
|
||||||
|
handle_tax_article_permission(item)
|
||||||
|
|
||||||
|
# Рассчитываем поля в зависимости от налогового шаблона
|
||||||
|
amount = flt(item.amount)
|
||||||
|
tax_template = item.item_tax_template
|
||||||
|
|
||||||
|
if tax_template == "ƏDV 18%":
|
||||||
|
calculate_18_percent_vat(item, amount)
|
||||||
|
elif tax_template == "ƏDV 0%":
|
||||||
|
calculate_0_percent_vat(item, amount)
|
||||||
|
elif tax_template == "ƏDV-dən azadolma":
|
||||||
|
calculate_vat_free(item, amount)
|
||||||
|
else:
|
||||||
|
calculate_without_vat(item, amount)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_vat_fields(item):
|
||||||
|
"""Очищает все VAT поля"""
|
||||||
|
vat_fields = [
|
||||||
|
'vat_18_percent_with_amount',
|
||||||
|
'vat_amount',
|
||||||
|
'vat_0_percent_with_amount',
|
||||||
|
'amount_without_vat',
|
||||||
|
'vat_free_amount'
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in vat_fields:
|
||||||
|
if hasattr(item, field):
|
||||||
|
setattr(item, field, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_tax_article_permission(item):
|
||||||
|
"""Обрабатывает разрешения для поля tax_article"""
|
||||||
|
tax_template = item.item_tax_template
|
||||||
|
is_allowed = tax_template in ["ƏDV 0%", "ƏDV-dən azadolma"]
|
||||||
|
|
||||||
|
if not is_allowed and item.tax_article:
|
||||||
|
item.tax_article = None
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_18_percent_vat(item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV 18%"""
|
||||||
|
vat_18_total = amount * 1.18
|
||||||
|
vat_amount = vat_18_total - amount
|
||||||
|
|
||||||
|
item.vat_18_percent_with_amount = vat_18_total
|
||||||
|
item.vat_amount = vat_amount
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_0_percent_vat(item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV 0%"""
|
||||||
|
item.vat_0_percent_with_amount = amount
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_vat_free(item, amount):
|
||||||
|
"""Рассчитывает поля для ƏDV-dən azadolma"""
|
||||||
|
item.vat_free_amount = amount
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_without_vat(item, amount):
|
||||||
|
"""Рассчитывает поля для случаев без НДС"""
|
||||||
|
item.amount_without_vat = amount
|
||||||
|
|
@ -61,6 +61,7 @@ def create_custom_fields():
|
||||||
fieldtype='Currency',
|
fieldtype='Currency',
|
||||||
insert_after='amount',
|
insert_after='amount',
|
||||||
in_list_view=1,
|
in_list_view=1,
|
||||||
|
read_only=1,
|
||||||
columns=2,
|
columns=2,
|
||||||
precision=2
|
precision=2
|
||||||
),
|
),
|
||||||
|
|
@ -81,6 +82,7 @@ def create_custom_fields():
|
||||||
fieldtype='Currency',
|
fieldtype='Currency',
|
||||||
insert_after='vat_amount',
|
insert_after='vat_amount',
|
||||||
in_list_view=1,
|
in_list_view=1,
|
||||||
|
read_only=1,
|
||||||
columns=2,
|
columns=2,
|
||||||
precision=2
|
precision=2
|
||||||
),
|
),
|
||||||
|
|
@ -90,6 +92,7 @@ def create_custom_fields():
|
||||||
fieldtype='Currency',
|
fieldtype='Currency',
|
||||||
insert_after='vat_0_percent_with_amount',
|
insert_after='vat_0_percent_with_amount',
|
||||||
in_list_view=1,
|
in_list_view=1,
|
||||||
|
read_only=1,
|
||||||
columns=2,
|
columns=2,
|
||||||
precision=2
|
precision=2
|
||||||
),
|
),
|
||||||
|
|
@ -99,8 +102,19 @@ def create_custom_fields():
|
||||||
fieldtype='Currency',
|
fieldtype='Currency',
|
||||||
insert_after='amount_without_vat',
|
insert_after='amount_without_vat',
|
||||||
in_list_view=1,
|
in_list_view=1,
|
||||||
|
read_only=1,
|
||||||
columns=2,
|
columns=2,
|
||||||
precision=2
|
precision=2
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
fieldname='tax_article',
|
||||||
|
label='Tax Article',
|
||||||
|
fieldtype='Link',
|
||||||
|
options='Tax Article',
|
||||||
|
insert_after='vat_free_amount',
|
||||||
|
in_list_view=1,
|
||||||
|
columns=2,
|
||||||
|
description='Tax Article field for VAT purposes'
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
"Sales Invoice": [
|
"Sales Invoice": [
|
||||||
|
|
@ -147,6 +161,7 @@ def create_custom_fields():
|
||||||
fieldtype='Currency',
|
fieldtype='Currency',
|
||||||
insert_after='amount',
|
insert_after='amount',
|
||||||
in_list_view=1,
|
in_list_view=1,
|
||||||
|
read_only=1,
|
||||||
columns=2,
|
columns=2,
|
||||||
precision=2
|
precision=2
|
||||||
),
|
),
|
||||||
|
|
@ -167,6 +182,7 @@ def create_custom_fields():
|
||||||
fieldtype='Currency',
|
fieldtype='Currency',
|
||||||
insert_after='vat_amount',
|
insert_after='vat_amount',
|
||||||
in_list_view=1,
|
in_list_view=1,
|
||||||
|
read_only=1,
|
||||||
columns=2,
|
columns=2,
|
||||||
precision=2
|
precision=2
|
||||||
),
|
),
|
||||||
|
|
@ -176,6 +192,7 @@ def create_custom_fields():
|
||||||
fieldtype='Currency',
|
fieldtype='Currency',
|
||||||
insert_after='vat_0_percent_with_amount',
|
insert_after='vat_0_percent_with_amount',
|
||||||
in_list_view=1,
|
in_list_view=1,
|
||||||
|
read_only=1,
|
||||||
columns=2,
|
columns=2,
|
||||||
precision=2
|
precision=2
|
||||||
),
|
),
|
||||||
|
|
@ -185,8 +202,19 @@ def create_custom_fields():
|
||||||
fieldtype='Currency',
|
fieldtype='Currency',
|
||||||
insert_after='amount_without_vat',
|
insert_after='amount_without_vat',
|
||||||
in_list_view=1,
|
in_list_view=1,
|
||||||
|
read_only=1,
|
||||||
columns=2,
|
columns=2,
|
||||||
precision=2
|
precision=2
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
fieldname='tax_article',
|
||||||
|
label='Tax Article',
|
||||||
|
fieldtype='Link',
|
||||||
|
options='Tax Article',
|
||||||
|
insert_after='vat_free_amount',
|
||||||
|
in_list_view=1,
|
||||||
|
columns=2,
|
||||||
|
description='Tax Article field for VAT purposes'
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
"Sales Taxes and Charges": [
|
"Sales Taxes and Charges": [
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ app_license = "unlicense"
|
||||||
boot_session = "jey_erp.extend_party_types.add_company_to_party_account_types"
|
boot_session = "jey_erp.extend_party_types.add_company_to_party_account_types"
|
||||||
|
|
||||||
app_include_js = [
|
app_include_js = [
|
||||||
|
"/assets/jey_erp/js/vat_calculator.js",
|
||||||
"/assets/jey_erp/js/account_tree_custom.js",
|
"/assets/jey_erp/js/account_tree_custom.js",
|
||||||
"/assets/jey_erp/js/sales_invoice.js",
|
"/assets/jey_erp/js/sales_invoice.js",
|
||||||
"/assets/jey_erp/js/sales_invoice_vat.js",
|
"/assets/jey_erp/js/sales_invoice_vat.js",
|
||||||
|
|
@ -32,13 +33,10 @@ doc_events = {
|
||||||
"Sales Invoice": {
|
"Sales Invoice": {
|
||||||
"validate": [
|
"validate": [
|
||||||
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
||||||
"jey_erp.custom.sales_invoice_vat.set_vat_fields_permissions"
|
|
||||||
],
|
],
|
||||||
"on_update": [
|
"on_update": [
|
||||||
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
||||||
"jey_erp.custom.sales_invoice_vat.set_vat_fields_permissions"
|
]
|
||||||
],
|
|
||||||
"before_save": "jey_erp.custom.sales_invoice_vat.set_vat_fields_permissions"
|
|
||||||
},
|
},
|
||||||
"Sales Order": {
|
"Sales Order": {
|
||||||
"validate": [
|
"validate": [
|
||||||
|
|
@ -59,6 +57,8 @@ def after_migrate_combined():
|
||||||
create_custom_fields()
|
create_custom_fields()
|
||||||
from jey_erp.custom.show_item_tax_template_in_sales_invoice import show_item_tax_template_in_sales_invoice
|
from jey_erp.custom.show_item_tax_template_in_sales_invoice import show_item_tax_template_in_sales_invoice
|
||||||
show_item_tax_template_in_sales_invoice()
|
show_item_tax_template_in_sales_invoice()
|
||||||
|
from jey_erp.custom.vat_calculations import patch_sales_documents
|
||||||
|
patch_sales_documents()
|
||||||
|
|
||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
|
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
// Sales Invoice customizations for Tax Free Amount calculation
|
|
||||||
|
|
||||||
frappe.ui.form.on('Sales Invoice', {
|
|
||||||
onload: function(frm) {
|
|
||||||
// Add custom calculate_tax_free_amounts method to frm
|
|
||||||
frm.calculate_tax_free_amounts = function() {
|
|
||||||
calculate_tax_free_amounts(frm);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: function(frm) {
|
|
||||||
// Calculate tax free amounts on refresh
|
|
||||||
if (frm.doc.docstatus === 0) {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
frappe.ui.form.on('Sales Invoice Item', {
|
|
||||||
item_code: function(frm, cdt, cdn) {
|
|
||||||
// Try immediate calculation, then with delay as fallback
|
|
||||||
setTimeout(function() {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
item_tax_template: function(frm, cdt, cdn) {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
},
|
|
||||||
|
|
||||||
item_tax_rate: function(frm, cdt, cdn) {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
},
|
|
||||||
|
|
||||||
qty: function(frm, cdt, cdn) {
|
|
||||||
setTimeout(function() {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
rate: function(frm, cdt, cdn) {
|
|
||||||
setTimeout(function() {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
net_amount: function(frm, cdt, cdn) {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
},
|
|
||||||
|
|
||||||
items_remove: function(frm, cdt, cdn) {
|
|
||||||
frm.calculate_tax_free_amounts();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function calculate_tax_free_amounts(frm) {
|
|
||||||
if (frm.doc.docstatus !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing tax_free_amount values
|
|
||||||
$.each(frm.doc.taxes || [], function(i, tax) {
|
|
||||||
tax.tax_free_amount = 0.0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Direct calculation without waiting
|
|
||||||
calculate_tax_free_amounts_direct(frm);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculate_tax_free_amounts_direct(frm) {
|
|
||||||
// Calculate tax free amounts for each item
|
|
||||||
$.each(frm.doc.items || [], function(i, item) {
|
|
||||||
if (!item.item_code) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tax_template = item.item_tax_template;
|
|
||||||
var tax_rate = item.item_tax_rate;
|
|
||||||
|
|
||||||
if (!tax_template && !tax_rate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process item_tax_rate (JSON format) - this is the most common case
|
|
||||||
if (tax_rate) {
|
|
||||||
try {
|
|
||||||
var tax_rates = typeof tax_rate === 'string' ? JSON.parse(tax_rate) : tax_rate;
|
|
||||||
|
|
||||||
// Check each tax account in the item_tax_rate
|
|
||||||
$.each(tax_rates, function(account_head, rate) {
|
|
||||||
if (flt(rate) === 0.0) {
|
|
||||||
// Find corresponding tax in sales invoice
|
|
||||||
$.each(frm.doc.taxes || [], function(k, doc_tax) {
|
|
||||||
if (doc_tax.account_head === account_head) {
|
|
||||||
// Add item amount to tax_free_amount
|
|
||||||
doc_tax.tax_free_amount = flt(doc_tax.tax_free_amount) + flt(item.net_amount);
|
|
||||||
return false; // break
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error parsing item_tax_rate:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process item_tax_template if no item_tax_rate
|
|
||||||
else if (tax_template) {
|
|
||||||
frappe.call({
|
|
||||||
method: 'frappe.client.get',
|
|
||||||
args: {
|
|
||||||
doctype: 'Item Tax Template',
|
|
||||||
name: tax_template
|
|
||||||
},
|
|
||||||
async: false,
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
var tax_template_doc = r.message;
|
|
||||||
|
|
||||||
// Check each tax in template
|
|
||||||
$.each(tax_template_doc.taxes || [], function(j, template_tax) {
|
|
||||||
if (flt(template_tax.tax_rate) === 0.0) {
|
|
||||||
// Find corresponding tax in sales invoice
|
|
||||||
$.each(frm.doc.taxes || [], function(k, doc_tax) {
|
|
||||||
if (doc_tax.account_head === template_tax.tax_type) {
|
|
||||||
// Add item amount to tax_free_amount
|
|
||||||
doc_tax.tax_free_amount = flt(doc_tax.tax_free_amount) + flt(item.net_amount);
|
|
||||||
return false; // break
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refresh taxes grid
|
|
||||||
frm.refresh_field('taxes');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override methods in the taxes calculation chain for immediate updates
|
|
||||||
let original_calculate_taxes_and_totals = erpnext.taxes_and_totals.prototype.calculate_taxes_and_totals;
|
|
||||||
let original_calculate_taxes = erpnext.taxes_and_totals.prototype.calculate_taxes;
|
|
||||||
|
|
||||||
erpnext.taxes_and_totals.prototype.calculate_taxes_and_totals = function(update_paid_amount) {
|
|
||||||
// Call original method
|
|
||||||
let result = original_calculate_taxes_and_totals.call(this, update_paid_amount);
|
|
||||||
|
|
||||||
// Calculate tax free amounts immediately after tax calculation
|
|
||||||
if (this.frm.doc.doctype === 'Sales Invoice') {
|
|
||||||
calculate_tax_free_amounts(this.frm);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Also override calculate_taxes to catch earlier in the process
|
|
||||||
erpnext.taxes_and_totals.prototype.calculate_taxes = function() {
|
|
||||||
// Call original method
|
|
||||||
let result = original_calculate_taxes.call(this);
|
|
||||||
|
|
||||||
// Calculate tax free amounts immediately after taxes calculation
|
|
||||||
if (this.frm.doc.doctype === 'Sales Invoice') {
|
|
||||||
calculate_tax_free_amounts(this.frm);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Override the item details loading to trigger calculation
|
|
||||||
if (erpnext.selling && erpnext.selling.SellingController) {
|
|
||||||
let original_item_code = erpnext.selling.SellingController.prototype.item_code;
|
|
||||||
|
|
||||||
erpnext.selling.SellingController.prototype.item_code = function(doc, cdt, cdn) {
|
|
||||||
// Call original method
|
|
||||||
let result = original_item_code.call(this, doc, cdt, cdn);
|
|
||||||
|
|
||||||
// Calculate tax free amounts after item details are loaded
|
|
||||||
if (this.frm.doc.doctype === 'Sales Invoice') {
|
|
||||||
setTimeout(() => {
|
|
||||||
calculate_tax_free_amounts(this.frm);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,480 +1,2 @@
|
||||||
// Файл: jey_erp/public/js/sales_invoice_vat.js
|
// Запускаем логику калькулятора НДС для Sales Invoice
|
||||||
|
jey_erp.vat_calculator.init('Sales Invoice');
|
||||||
// Sales Invoice VAT fields logic
|
|
||||||
frappe.ui.form.on('Sales Invoice', {
|
|
||||||
onload: function(frm) {
|
|
||||||
setup_vat_fields_logic(frm);
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: function(frm) {
|
|
||||||
if (frm.doc.docstatus === 0) {
|
|
||||||
wait_for_grid_ready(frm, function() {
|
|
||||||
update_all_vat_permissions(frm);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
frappe.ui.form.on('Sales Invoice Item', {
|
|
||||||
items_add: function(frm, cdt, cdn) {
|
|
||||||
wait_for_grid_ready(frm, function() {
|
|
||||||
update_item_vat_permissions(frm, cdt, cdn);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
item_code: function(frm, cdt, cdn) {
|
|
||||||
update_item_vat_permissions(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
item_tax_template: function(frm, cdt, cdn) {
|
|
||||||
update_item_vat_permissions(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Пересчет при изменении rate
|
|
||||||
rate: function(frm, cdt, cdn) {
|
|
||||||
// Небольшая задержка, чтобы amount успел пересчитаться
|
|
||||||
setTimeout(() => {
|
|
||||||
recalculate_all_vat_fields(frm, cdt, cdn);
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Пересчет при изменении qty
|
|
||||||
qty: function(frm, cdt, cdn) {
|
|
||||||
// Небольшая задержка, чтобы amount успел пересчитаться
|
|
||||||
setTimeout(() => {
|
|
||||||
recalculate_all_vat_fields(frm, cdt, cdn);
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
amount: function(frm, cdt, cdn) {
|
|
||||||
// Пересчитать все VAT поля при изменении основной суммы
|
|
||||||
recalculate_all_vat_fields(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
vat_18_percent_with_amount: function(frm, cdt, cdn) {
|
|
||||||
clear_other_vat_fields(frm, cdt, cdn, 'vat_18_percent_with_amount');
|
|
||||||
// Пересчитать VAT Amount при изменении суммы с НДС
|
|
||||||
calculate_vat_amount(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
vat_0_percent_with_amount: function(frm, cdt, cdn) {
|
|
||||||
clear_other_vat_fields(frm, cdt, cdn, 'vat_0_percent_with_amount');
|
|
||||||
},
|
|
||||||
|
|
||||||
amount_without_vat: function(frm, cdt, cdn) {
|
|
||||||
clear_other_vat_fields(frm, cdt, cdn, 'amount_without_vat');
|
|
||||||
},
|
|
||||||
|
|
||||||
vat_free_amount: function(frm, cdt, cdn) {
|
|
||||||
clear_other_vat_fields(frm, cdt, cdn, 'vat_free_amount');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function setup_vat_fields_logic(frm) {
|
|
||||||
frm.vat_field_names = [
|
|
||||||
'vat_18_percent_with_amount',
|
|
||||||
'vat_amount',
|
|
||||||
'vat_0_percent_with_amount',
|
|
||||||
'amount_without_vat',
|
|
||||||
'vat_free_amount'
|
|
||||||
];
|
|
||||||
|
|
||||||
frm.vat_permissions_cache = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function wait_for_grid_ready(frm, callback, max_attempts = 20, attempt = 0) {
|
|
||||||
if (attempt >= max_attempts) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let grid = frm.fields_dict.items.grid;
|
|
||||||
if (grid && grid.grid_rows && grid.grid_rows.length >= frm.doc.items.length) {
|
|
||||||
let all_ready = true;
|
|
||||||
for (let i = 0; i < frm.doc.items.length; i++) {
|
|
||||||
if (!grid.grid_rows[i] || !grid.grid_rows[i].docfields) {
|
|
||||||
all_ready = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (all_ready) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
wait_for_grid_ready(frm, callback, max_attempts, attempt + 1);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_all_vat_permissions(frm) {
|
|
||||||
if (frm.doc.items) {
|
|
||||||
frm.doc.items.forEach(function(item, idx) {
|
|
||||||
if (item.item_code) {
|
|
||||||
update_item_vat_permissions(frm, frm.fields_dict.items.grid.doctype, item.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_item_vat_permissions(frm, cdt, cdn) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
if (!item.item_code) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_key = item.item_code + '|' + (item.item_tax_template || '');
|
|
||||||
|
|
||||||
if (frm.vat_permissions_cache && frm.vat_permissions_cache[cache_key]) {
|
|
||||||
apply_vat_permissions(frm, cdt, cdn, frm.vat_permissions_cache[cache_key]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let row_index = frm.doc.items.findIndex(i => i.name === cdn);
|
|
||||||
if (row_index >= 0) {
|
|
||||||
try {
|
|
||||||
frm.fields_dict.items.grid.grid_rows[row_index].toggle_editable_row(false);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: 'jey_erp.custom.sales_invoice_vat.get_vat_permissions_for_item',
|
|
||||||
args: {
|
|
||||||
item_code: item.item_code,
|
|
||||||
item_tax_template: item.item_tax_template
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
if (!frm.vat_permissions_cache) {
|
|
||||||
frm.vat_permissions_cache = {};
|
|
||||||
}
|
|
||||||
frm.vat_permissions_cache[cache_key] = r.message;
|
|
||||||
|
|
||||||
apply_vat_permissions(frm, cdt, cdn, r.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
always: function() {
|
|
||||||
if (row_index >= 0) {
|
|
||||||
try {
|
|
||||||
frm.fields_dict.items.grid.grid_rows[row_index].toggle_editable_row(true);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_vat_permissions(frm, cdt, cdn, permissions) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
let row_index = frm.doc.items.findIndex(doc_item => doc_item.name === cdn);
|
|
||||||
|
|
||||||
if (row_index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_row_ready(frm, row_index, function() {
|
|
||||||
frm.vat_field_names.forEach(function(fieldname) {
|
|
||||||
try {
|
|
||||||
apply_field_permission(frm, cdt, cdn, row_index, fieldname, permissions[fieldname], item);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.fields_dict.items.grid.refresh();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function wait_for_row_ready(frm, row_index, callback, max_attempts = 10, attempt = 0) {
|
|
||||||
if (attempt >= max_attempts) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let grid = frm.fields_dict.items.grid;
|
|
||||||
if (grid && grid.grid_rows && grid.grid_rows[row_index] && grid.grid_rows[row_index].docfields) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
wait_for_row_ready(frm, row_index, callback, max_attempts, attempt + 1);
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_field_permission(frm, cdt, cdn, row_index, fieldname, is_enabled, item) {
|
|
||||||
let grid_row = frm.fields_dict.items.grid.grid_rows[row_index];
|
|
||||||
if (grid_row && grid_row.docfields) {
|
|
||||||
let docfield = grid_row.docfields.find(df => df.fieldname === fieldname);
|
|
||||||
if (docfield) {
|
|
||||||
// VAT Amount всегда только для чтения
|
|
||||||
if (fieldname === 'vat_amount') {
|
|
||||||
docfield.read_only = 1;
|
|
||||||
} else {
|
|
||||||
docfield.read_only = is_enabled ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let meta = frappe.get_meta("Sales Invoice Item");
|
|
||||||
let field_meta = meta.fields.find(f => f.fieldname === fieldname);
|
|
||||||
if (field_meta) {
|
|
||||||
if (fieldname === 'vat_amount') {
|
|
||||||
field_meta.read_only = 1;
|
|
||||||
} else {
|
|
||||||
field_meta.read_only = is_enabled ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (meta_error) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_enabled) {
|
|
||||||
auto_fill_vat_field(cdt, cdn, fieldname, item);
|
|
||||||
} else {
|
|
||||||
if (item[fieldname] && item[fieldname] != 0) {
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function recalculate_all_vat_fields(frm, cdt, cdn) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
if (!item || !item.item_code) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем текущие права доступа из кеша
|
|
||||||
let cache_key = item.item_code + '|' + (item.item_tax_template || '');
|
|
||||||
let permissions = frm.vat_permissions_cache ? frm.vat_permissions_cache[cache_key] : null;
|
|
||||||
|
|
||||||
if (!permissions) {
|
|
||||||
// Если нет в кеше, получаем из API
|
|
||||||
frappe.call({
|
|
||||||
method: 'jey_erp.custom.sales_invoice_vat.get_vat_permissions_for_item',
|
|
||||||
args: {
|
|
||||||
item_code: item.item_code,
|
|
||||||
item_tax_template: item.item_tax_template
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
if (!frm.vat_permissions_cache) {
|
|
||||||
frm.vat_permissions_cache = {};
|
|
||||||
}
|
|
||||||
frm.vat_permissions_cache[cache_key] = r.message;
|
|
||||||
recalculate_vat_fields_with_permissions(frm, cdt, cdn, r.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
recalculate_vat_fields_with_permissions(frm, cdt, cdn, permissions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function recalculate_vat_fields_with_permissions(frm, cdt, cdn, permissions) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
let item_amount = flt(item.amount || 0);
|
|
||||||
|
|
||||||
if (item_amount <= 0) {
|
|
||||||
// Если сумма 0 или меньше, очищаем все VAT поля
|
|
||||||
frm.vat_field_names.forEach(function(fieldname) {
|
|
||||||
if (item[fieldname] && item[fieldname] != 0) {
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, 0.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пересчитываем только активные поля
|
|
||||||
for (let fieldname in permissions) {
|
|
||||||
if (permissions[fieldname]) {
|
|
||||||
auto_fill_vat_field(cdt, cdn, fieldname, item, true); // true = force recalculate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculate_vat_amount(frm, cdt, cdn) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
// Вычислить VAT Amount только если есть vat_18_percent_with_amount и amount
|
|
||||||
let vat_18_total = flt(item.vat_18_percent_with_amount || 0);
|
|
||||||
let item_amount = flt(item.amount || 0);
|
|
||||||
|
|
||||||
if (vat_18_total > 0 && item_amount > 0) {
|
|
||||||
let vat_only = vat_18_total - item_amount;
|
|
||||||
frappe.model.set_value(cdt, cdn, 'vat_amount', vat_only);
|
|
||||||
} else if (vat_18_total == 0 && item_amount > 0) {
|
|
||||||
// Если vat_18_percent_with_amount пустое, но есть amount, очистить vat_amount
|
|
||||||
frappe.model.set_value(cdt, cdn, 'vat_amount', 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function auto_fill_vat_field(cdt, cdn, fieldname, item, force_recalculate = false) {
|
|
||||||
// Всегда пересчитываем vat_amount, для остальных полей проверяем есть ли значение
|
|
||||||
if (!item[fieldname] || item[fieldname] == 0 || fieldname === 'vat_amount' || force_recalculate) {
|
|
||||||
let item_amount = flt(item.amount || 0);
|
|
||||||
|
|
||||||
if (item_amount > 0) {
|
|
||||||
let new_value = 0;
|
|
||||||
|
|
||||||
if (fieldname === 'vat_18_percent_with_amount') {
|
|
||||||
new_value = item_amount * 1.18;
|
|
||||||
} else if (fieldname === 'vat_amount') {
|
|
||||||
// Вычислить VAT Amount
|
|
||||||
let vat_18_total = flt(item.vat_18_percent_with_amount || 0);
|
|
||||||
if (vat_18_total == 0) {
|
|
||||||
vat_18_total = item_amount * 1.18;
|
|
||||||
frappe.model.set_value(cdt, cdn, 'vat_18_percent_with_amount', vat_18_total);
|
|
||||||
}
|
|
||||||
new_value = vat_18_total - item_amount;
|
|
||||||
} else if (fieldname === 'vat_0_percent_with_amount') {
|
|
||||||
new_value = item_amount;
|
|
||||||
} else if (fieldname === 'amount_without_vat') {
|
|
||||||
new_value = item_amount;
|
|
||||||
} else if (fieldname === 'vat_free_amount') {
|
|
||||||
new_value = item_amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new_value > 0 || fieldname === 'vat_amount') {
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, new_value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Если amount = 0, очищаем поле
|
|
||||||
if (item[fieldname] && item[fieldname] != 0) {
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear_other_vat_fields(frm, cdt, cdn, current_field) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
if (item[current_field] && item[current_field] > 0) {
|
|
||||||
frm.vat_field_names.forEach(function(fieldname) {
|
|
||||||
if (fieldname !== current_field && fieldname !== 'vat_amount') {
|
|
||||||
// Не очищать vat_amount - оно вычисляется автоматически
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, 0.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Если текущее поле не vat_18_percent_with_amount, очистить vat_amount
|
|
||||||
if (current_field !== 'vat_18_percent_with_amount') {
|
|
||||||
frappe.model.set_value(cdt, cdn, 'vat_amount', 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Дополнительная функция для ручного пересчета VAT Amount
|
|
||||||
function recalculate_vat_amount_for_all_items(frm) {
|
|
||||||
if (frm.doc.items) {
|
|
||||||
frm.doc.items.forEach(function(item) {
|
|
||||||
if (item.vat_18_percent_with_amount && item.amount) {
|
|
||||||
let vat_only = flt(item.vat_18_percent_with_amount) - flt(item.amount);
|
|
||||||
frappe.model.set_value("Sales Invoice Item", item.name, 'vat_amount', vat_only);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для добавления кнопки пересчета в форму (опционально)
|
|
||||||
function add_recalculate_button(frm) {
|
|
||||||
if (frm.doc.docstatus === 0) {
|
|
||||||
frm.add_custom_button(__('Recalculate VAT Fields'), function() {
|
|
||||||
if (frm.doc.items) {
|
|
||||||
frm.doc.items.forEach(function(item) {
|
|
||||||
recalculate_all_vat_fields(frm, "Sales Invoice Item", item.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
frappe.show_alert({
|
|
||||||
message: __('VAT fields recalculated'),
|
|
||||||
indicator: 'green'
|
|
||||||
});
|
|
||||||
}, __('VAT'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Интеграция с ERPNext SellingController
|
|
||||||
$(document).ready(function() {
|
|
||||||
function setup_erpnext_override() {
|
|
||||||
if (erpnext && erpnext.selling && erpnext.selling.SellingController) {
|
|
||||||
if (!erpnext.selling.SellingController.prototype._original_item_code) {
|
|
||||||
let original_item_code = erpnext.selling.SellingController.prototype.item_code;
|
|
||||||
erpnext.selling.SellingController.prototype._original_item_code = original_item_code;
|
|
||||||
|
|
||||||
erpnext.selling.SellingController.prototype.item_code = function(doc, cdt, cdn) {
|
|
||||||
let result = this._original_item_code.call(this, doc, cdt, cdn);
|
|
||||||
|
|
||||||
if (this.frm.doc.doctype === 'Sales Invoice') {
|
|
||||||
let check_item_loaded = () => {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
if (item && item.item_code && (item.rate || item.price_list_rate)) {
|
|
||||||
update_item_vat_permissions(this.frm, cdt, cdn);
|
|
||||||
} else {
|
|
||||||
setTimeout(check_item_loaded, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(check_item_loaded, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перехватываем calculate_taxes_and_totals для пересчета VAT полей
|
|
||||||
if (!erpnext.selling.SellingController.prototype._original_calculate_taxes_and_totals) {
|
|
||||||
let original_calculate = erpnext.selling.SellingController.prototype.calculate_taxes_and_totals;
|
|
||||||
erpnext.selling.SellingController.prototype._original_calculate_taxes_and_totals = original_calculate;
|
|
||||||
|
|
||||||
erpnext.selling.SellingController.prototype.calculate_taxes_and_totals = function() {
|
|
||||||
let result = this._original_calculate_taxes_and_totals.call(this);
|
|
||||||
|
|
||||||
if (this.frm.doc.doctype === 'Sales Invoice') {
|
|
||||||
// Пересчитать все VAT поля для всех товаров после расчета налогов
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.frm.doc.items) {
|
|
||||||
this.frm.doc.items.forEach((item) => {
|
|
||||||
recalculate_all_vat_fields(this.frm, "Sales Invoice Item", item.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перехватываем price_list_rate для пересчета при изменении цены
|
|
||||||
if (!erpnext.selling.SellingController.prototype._original_price_list_rate) {
|
|
||||||
let original_price_list_rate = erpnext.selling.SellingController.prototype.price_list_rate;
|
|
||||||
erpnext.selling.SellingController.prototype._original_price_list_rate = original_price_list_rate;
|
|
||||||
|
|
||||||
erpnext.selling.SellingController.prototype.price_list_rate = function(doc, cdt, cdn) {
|
|
||||||
let result = this._original_price_list_rate.call(this, doc, cdt, cdn);
|
|
||||||
|
|
||||||
if (this.frm.doc.doctype === 'Sales Invoice') {
|
|
||||||
setTimeout(() => {
|
|
||||||
recalculate_all_vat_fields(this.frm, cdt, cdn);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setTimeout(setup_erpnext_override, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_erpnext_override();
|
|
||||||
});
|
|
||||||
|
|
@ -1,480 +1,2 @@
|
||||||
// Файл: jey_erp/public/js/sales_order_vat.js
|
// Запускаем логику калькулятора НДС для Sales Order
|
||||||
|
jey_erp.vat_calculator.init('Sales Order');
|
||||||
// Sales Order VAT fields logic
|
|
||||||
frappe.ui.form.on('Sales Order', {
|
|
||||||
onload: function(frm) {
|
|
||||||
setup_vat_fields_logic(frm);
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: function(frm) {
|
|
||||||
if (frm.doc.docstatus === 0) {
|
|
||||||
wait_for_grid_ready(frm, function() {
|
|
||||||
update_all_vat_permissions(frm);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
frappe.ui.form.on('Sales Order Item', {
|
|
||||||
items_add: function(frm, cdt, cdn) {
|
|
||||||
wait_for_grid_ready(frm, function() {
|
|
||||||
update_item_vat_permissions(frm, cdt, cdn);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
item_code: function(frm, cdt, cdn) {
|
|
||||||
update_item_vat_permissions(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
item_tax_template: function(frm, cdt, cdn) {
|
|
||||||
update_item_vat_permissions(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Пересчет при изменении rate
|
|
||||||
rate: function(frm, cdt, cdn) {
|
|
||||||
// Небольшая задержка, чтобы amount успел пересчитаться
|
|
||||||
setTimeout(() => {
|
|
||||||
recalculate_all_vat_fields(frm, cdt, cdn);
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Пересчет при изменении qty
|
|
||||||
qty: function(frm, cdt, cdn) {
|
|
||||||
// Небольшая задержка, чтобы amount успел пересчитаться
|
|
||||||
setTimeout(() => {
|
|
||||||
recalculate_all_vat_fields(frm, cdt, cdn);
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
amount: function(frm, cdt, cdn) {
|
|
||||||
// Пересчитать все VAT поля при изменении основной суммы
|
|
||||||
recalculate_all_vat_fields(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
vat_18_percent_with_amount: function(frm, cdt, cdn) {
|
|
||||||
clear_other_vat_fields(frm, cdt, cdn, 'vat_18_percent_with_amount');
|
|
||||||
// Пересчитать VAT Amount при изменении суммы с НДС
|
|
||||||
calculate_vat_amount(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
vat_0_percent_with_amount: function(frm, cdt, cdn) {
|
|
||||||
clear_other_vat_fields(frm, cdt, cdn, 'vat_0_percent_with_amount');
|
|
||||||
},
|
|
||||||
|
|
||||||
amount_without_vat: function(frm, cdt, cdn) {
|
|
||||||
clear_other_vat_fields(frm, cdt, cdn, 'amount_without_vat');
|
|
||||||
},
|
|
||||||
|
|
||||||
vat_free_amount: function(frm, cdt, cdn) {
|
|
||||||
clear_other_vat_fields(frm, cdt, cdn, 'vat_free_amount');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function setup_vat_fields_logic(frm) {
|
|
||||||
frm.vat_field_names = [
|
|
||||||
'vat_18_percent_with_amount',
|
|
||||||
'vat_amount',
|
|
||||||
'vat_0_percent_with_amount',
|
|
||||||
'amount_without_vat',
|
|
||||||
'vat_free_amount'
|
|
||||||
];
|
|
||||||
|
|
||||||
frm.vat_permissions_cache = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function wait_for_grid_ready(frm, callback, max_attempts = 20, attempt = 0) {
|
|
||||||
if (attempt >= max_attempts) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let grid = frm.fields_dict.items.grid;
|
|
||||||
if (grid && grid.grid_rows && grid.grid_rows.length >= frm.doc.items.length) {
|
|
||||||
let all_ready = true;
|
|
||||||
for (let i = 0; i < frm.doc.items.length; i++) {
|
|
||||||
if (!grid.grid_rows[i] || !grid.grid_rows[i].docfields) {
|
|
||||||
all_ready = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (all_ready) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
wait_for_grid_ready(frm, callback, max_attempts, attempt + 1);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_all_vat_permissions(frm) {
|
|
||||||
if (frm.doc.items) {
|
|
||||||
frm.doc.items.forEach(function(item, idx) {
|
|
||||||
if (item.item_code) {
|
|
||||||
update_item_vat_permissions(frm, frm.fields_dict.items.grid.doctype, item.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_item_vat_permissions(frm, cdt, cdn) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
if (!item.item_code) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_key = item.item_code + '|' + (item.item_tax_template || '');
|
|
||||||
|
|
||||||
if (frm.vat_permissions_cache && frm.vat_permissions_cache[cache_key]) {
|
|
||||||
apply_vat_permissions(frm, cdt, cdn, frm.vat_permissions_cache[cache_key]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let row_index = frm.doc.items.findIndex(i => i.name === cdn);
|
|
||||||
if (row_index >= 0) {
|
|
||||||
try {
|
|
||||||
frm.fields_dict.items.grid.grid_rows[row_index].toggle_editable_row(false);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: 'jey_erp.custom.sales_order_vat.get_vat_permissions_for_item',
|
|
||||||
args: {
|
|
||||||
item_code: item.item_code,
|
|
||||||
item_tax_template: item.item_tax_template
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
if (!frm.vat_permissions_cache) {
|
|
||||||
frm.vat_permissions_cache = {};
|
|
||||||
}
|
|
||||||
frm.vat_permissions_cache[cache_key] = r.message;
|
|
||||||
|
|
||||||
apply_vat_permissions(frm, cdt, cdn, r.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
always: function() {
|
|
||||||
if (row_index >= 0) {
|
|
||||||
try {
|
|
||||||
frm.fields_dict.items.grid.grid_rows[row_index].toggle_editable_row(true);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_vat_permissions(frm, cdt, cdn, permissions) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
let row_index = frm.doc.items.findIndex(doc_item => doc_item.name === cdn);
|
|
||||||
|
|
||||||
if (row_index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_row_ready(frm, row_index, function() {
|
|
||||||
frm.vat_field_names.forEach(function(fieldname) {
|
|
||||||
try {
|
|
||||||
apply_field_permission(frm, cdt, cdn, row_index, fieldname, permissions[fieldname], item);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.fields_dict.items.grid.refresh();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function wait_for_row_ready(frm, row_index, callback, max_attempts = 10, attempt = 0) {
|
|
||||||
if (attempt >= max_attempts) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let grid = frm.fields_dict.items.grid;
|
|
||||||
if (grid && grid.grid_rows && grid.grid_rows[row_index] && grid.grid_rows[row_index].docfields) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
wait_for_row_ready(frm, row_index, callback, max_attempts, attempt + 1);
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_field_permission(frm, cdt, cdn, row_index, fieldname, is_enabled, item) {
|
|
||||||
let grid_row = frm.fields_dict.items.grid.grid_rows[row_index];
|
|
||||||
if (grid_row && grid_row.docfields) {
|
|
||||||
let docfield = grid_row.docfields.find(df => df.fieldname === fieldname);
|
|
||||||
if (docfield) {
|
|
||||||
// VAT Amount всегда только для чтения
|
|
||||||
if (fieldname === 'vat_amount') {
|
|
||||||
docfield.read_only = 1;
|
|
||||||
} else {
|
|
||||||
docfield.read_only = is_enabled ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let meta = frappe.get_meta("Sales Order Item");
|
|
||||||
let field_meta = meta.fields.find(f => f.fieldname === fieldname);
|
|
||||||
if (field_meta) {
|
|
||||||
if (fieldname === 'vat_amount') {
|
|
||||||
field_meta.read_only = 1;
|
|
||||||
} else {
|
|
||||||
field_meta.read_only = is_enabled ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (meta_error) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_enabled) {
|
|
||||||
auto_fill_vat_field(cdt, cdn, fieldname, item);
|
|
||||||
} else {
|
|
||||||
if (item[fieldname] && item[fieldname] != 0) {
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function recalculate_all_vat_fields(frm, cdt, cdn) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
if (!item || !item.item_code) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем текущие права доступа из кеша
|
|
||||||
let cache_key = item.item_code + '|' + (item.item_tax_template || '');
|
|
||||||
let permissions = frm.vat_permissions_cache ? frm.vat_permissions_cache[cache_key] : null;
|
|
||||||
|
|
||||||
if (!permissions) {
|
|
||||||
// Если нет в кеше, получаем из API
|
|
||||||
frappe.call({
|
|
||||||
method: 'jey_erp.custom.sales_order_vat.get_vat_permissions_for_item',
|
|
||||||
args: {
|
|
||||||
item_code: item.item_code,
|
|
||||||
item_tax_template: item.item_tax_template
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
if (!frm.vat_permissions_cache) {
|
|
||||||
frm.vat_permissions_cache = {};
|
|
||||||
}
|
|
||||||
frm.vat_permissions_cache[cache_key] = r.message;
|
|
||||||
recalculate_vat_fields_with_permissions(frm, cdt, cdn, r.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
recalculate_vat_fields_with_permissions(frm, cdt, cdn, permissions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function recalculate_vat_fields_with_permissions(frm, cdt, cdn, permissions) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
let item_amount = flt(item.amount || 0);
|
|
||||||
|
|
||||||
if (item_amount <= 0) {
|
|
||||||
// Если сумма 0 или меньше, очищаем все VAT поля
|
|
||||||
frm.vat_field_names.forEach(function(fieldname) {
|
|
||||||
if (item[fieldname] && item[fieldname] != 0) {
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, 0.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пересчитываем только активные поля
|
|
||||||
for (let fieldname in permissions) {
|
|
||||||
if (permissions[fieldname]) {
|
|
||||||
auto_fill_vat_field(cdt, cdn, fieldname, item, true); // true = force recalculate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculate_vat_amount(frm, cdt, cdn) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
// Вычислить VAT Amount только если есть vat_18_percent_with_amount и amount
|
|
||||||
let vat_18_total = flt(item.vat_18_percent_with_amount || 0);
|
|
||||||
let item_amount = flt(item.amount || 0);
|
|
||||||
|
|
||||||
if (vat_18_total > 0 && item_amount > 0) {
|
|
||||||
let vat_only = vat_18_total - item_amount;
|
|
||||||
frappe.model.set_value(cdt, cdn, 'vat_amount', vat_only);
|
|
||||||
} else if (vat_18_total == 0 && item_amount > 0) {
|
|
||||||
// Если vat_18_percent_with_amount пустое, но есть amount, очистить vat_amount
|
|
||||||
frappe.model.set_value(cdt, cdn, 'vat_amount', 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function auto_fill_vat_field(cdt, cdn, fieldname, item, force_recalculate = false) {
|
|
||||||
// Всегда пересчитываем vat_amount, для остальных полей проверяем есть ли значение
|
|
||||||
if (!item[fieldname] || item[fieldname] == 0 || fieldname === 'vat_amount' || force_recalculate) {
|
|
||||||
let item_amount = flt(item.amount || 0);
|
|
||||||
|
|
||||||
if (item_amount > 0) {
|
|
||||||
let new_value = 0;
|
|
||||||
|
|
||||||
if (fieldname === 'vat_18_percent_with_amount') {
|
|
||||||
new_value = item_amount * 1.18;
|
|
||||||
} else if (fieldname === 'vat_amount') {
|
|
||||||
// Вычислить VAT Amount
|
|
||||||
let vat_18_total = flt(item.vat_18_percent_with_amount || 0);
|
|
||||||
if (vat_18_total == 0) {
|
|
||||||
vat_18_total = item_amount * 1.18;
|
|
||||||
frappe.model.set_value(cdt, cdn, 'vat_18_percent_with_amount', vat_18_total);
|
|
||||||
}
|
|
||||||
new_value = vat_18_total - item_amount;
|
|
||||||
} else if (fieldname === 'vat_0_percent_with_amount') {
|
|
||||||
new_value = item_amount;
|
|
||||||
} else if (fieldname === 'amount_without_vat') {
|
|
||||||
new_value = item_amount;
|
|
||||||
} else if (fieldname === 'vat_free_amount') {
|
|
||||||
new_value = item_amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new_value > 0 || fieldname === 'vat_amount') {
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, new_value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Если amount = 0, очищаем поле
|
|
||||||
if (item[fieldname] && item[fieldname] != 0) {
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear_other_vat_fields(frm, cdt, cdn, current_field) {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
|
|
||||||
if (item[current_field] && item[current_field] > 0) {
|
|
||||||
frm.vat_field_names.forEach(function(fieldname) {
|
|
||||||
if (fieldname !== current_field && fieldname !== 'vat_amount') {
|
|
||||||
// Не очищать vat_amount - оно вычисляется автоматически
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldname, 0.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Если текущее поле не vat_18_percent_with_amount, очистить vat_amount
|
|
||||||
if (current_field !== 'vat_18_percent_with_amount') {
|
|
||||||
frappe.model.set_value(cdt, cdn, 'vat_amount', 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Дополнительная функция для ручного пересчета VAT Amount
|
|
||||||
function recalculate_vat_amount_for_all_items(frm) {
|
|
||||||
if (frm.doc.items) {
|
|
||||||
frm.doc.items.forEach(function(item) {
|
|
||||||
if (item.vat_18_percent_with_amount && item.amount) {
|
|
||||||
let vat_only = flt(item.vat_18_percent_with_amount) - flt(item.amount);
|
|
||||||
frappe.model.set_value("Sales Order Item", item.name, 'vat_amount', vat_only);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для добавления кнопки пересчета в форму (опционально)
|
|
||||||
function add_recalculate_button(frm) {
|
|
||||||
if (frm.doc.docstatus === 0) {
|
|
||||||
frm.add_custom_button(__('Recalculate VAT Fields'), function() {
|
|
||||||
if (frm.doc.items) {
|
|
||||||
frm.doc.items.forEach(function(item) {
|
|
||||||
recalculate_all_vat_fields(frm, "Sales Order Item", item.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
frappe.show_alert({
|
|
||||||
message: __('VAT fields recalculated'),
|
|
||||||
indicator: 'green'
|
|
||||||
});
|
|
||||||
}, __('VAT'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Интеграция с ERPNext SellingController
|
|
||||||
$(document).ready(function() {
|
|
||||||
function setup_erpnext_override() {
|
|
||||||
if (erpnext && erpnext.selling && erpnext.selling.SellingController) {
|
|
||||||
if (!erpnext.selling.SellingController.prototype._original_item_code_so) {
|
|
||||||
let original_item_code = erpnext.selling.SellingController.prototype.item_code;
|
|
||||||
erpnext.selling.SellingController.prototype._original_item_code_so = original_item_code;
|
|
||||||
|
|
||||||
erpnext.selling.SellingController.prototype.item_code = function(doc, cdt, cdn) {
|
|
||||||
let result = this._original_item_code_so.call(this, doc, cdt, cdn);
|
|
||||||
|
|
||||||
if (this.frm.doc.doctype === 'Sales Order') {
|
|
||||||
let check_item_loaded = () => {
|
|
||||||
let item = locals[cdt][cdn];
|
|
||||||
if (item && item.item_code && (item.rate || item.price_list_rate)) {
|
|
||||||
update_item_vat_permissions(this.frm, cdt, cdn);
|
|
||||||
} else {
|
|
||||||
setTimeout(check_item_loaded, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(check_item_loaded, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перехватываем calculate_taxes_and_totals для пересчета VAT полей
|
|
||||||
if (!erpnext.selling.SellingController.prototype._original_calculate_taxes_and_totals_so) {
|
|
||||||
let original_calculate = erpnext.selling.SellingController.prototype.calculate_taxes_and_totals;
|
|
||||||
erpnext.selling.SellingController.prototype._original_calculate_taxes_and_totals_so = original_calculate;
|
|
||||||
|
|
||||||
erpnext.selling.SellingController.prototype.calculate_taxes_and_totals = function() {
|
|
||||||
let result = this._original_calculate_taxes_and_totals_so.call(this);
|
|
||||||
|
|
||||||
if (this.frm.doc.doctype === 'Sales Order') {
|
|
||||||
// Пересчитать все VAT поля для всех товаров после расчета налогов
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.frm.doc.items) {
|
|
||||||
this.frm.doc.items.forEach((item) => {
|
|
||||||
recalculate_all_vat_fields(this.frm, "Sales Order Item", item.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перехватываем price_list_rate для пересчета при изменении цены
|
|
||||||
if (!erpnext.selling.SellingController.prototype._original_price_list_rate_so) {
|
|
||||||
let original_price_list_rate = erpnext.selling.SellingController.prototype.price_list_rate;
|
|
||||||
erpnext.selling.SellingController.prototype._original_price_list_rate_so = original_price_list_rate;
|
|
||||||
|
|
||||||
erpnext.selling.SellingController.prototype.price_list_rate = function(doc, cdt, cdn) {
|
|
||||||
let result = this._original_price_list_rate_so.call(this, doc, cdt, cdn);
|
|
||||||
|
|
||||||
if (this.frm.doc.doctype === 'Sales Order') {
|
|
||||||
setTimeout(() => {
|
|
||||||
recalculate_all_vat_fields(this.frm, cdt, cdn);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setTimeout(setup_erpnext_override, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_erpnext_override();
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,343 @@
|
||||||
|
// Файл: jey_erp/public/js/vat_calculator.js
|
||||||
|
// Версия с двумя типами НДС 18%: сверху и в том числе
|
||||||
|
// + индивидуальные фильтры для tax_article
|
||||||
|
|
||||||
|
if (!window.jey_erp) {
|
||||||
|
window.jey_erp = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
jey_erp.vat_calculator = {
|
||||||
|
init: function(doctype) {
|
||||||
|
const child_doctype = doctype === 'Sales Order' ? 'Sales Order Item' : 'Sales Invoice Item';
|
||||||
|
|
||||||
|
// 1. Стандартные триггеры Frappe
|
||||||
|
frappe.ui.form.on(doctype, {
|
||||||
|
refresh: (frm) => {
|
||||||
|
if (frm.doc.docstatus === 0) {
|
||||||
|
this.setup_vat_system(frm, child_doctype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on(child_doctype, {
|
||||||
|
item_code: (frm, cdt, cdn) => {
|
||||||
|
// Даем время фреймворку загрузить данные товара
|
||||||
|
setTimeout(() => {
|
||||||
|
this.run_calculations(frm, cdt, cdn);
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ОСНОВНЫЕ ТРИГГЕРЫ - используем те, которые срабатывают после обновления значений
|
||||||
|
qty: (frm, cdt, cdn) => {
|
||||||
|
// Добавляем небольшую задержку для гарантии обновления
|
||||||
|
setTimeout(() => {
|
||||||
|
this.run_calculations(frm, cdt, cdn);
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
|
||||||
|
rate: (frm, cdt, cdn) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.run_calculations(frm, cdt, cdn);
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Триггер amount как дополнительная подстраховка
|
||||||
|
amount: (frm, cdt, cdn) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.run_calculations(frm, cdt, cdn);
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
|
||||||
|
item_tax_template: (frm, cdt, cdn) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.run_calculations(frm, cdt, cdn);
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
form_render: (frm, cdt, cdn) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.update_tax_article_readonly(frm, cdt, cdn);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. jQuery-обработчики для гарантированного обновления readonly и фильтров
|
||||||
|
$(document).ready(() => {
|
||||||
|
// Обработчик фокуса на элементах внутри строк таблицы
|
||||||
|
$(document).on('focus', '.grid-row input, .grid-row select', (e) => {
|
||||||
|
const frm = cur_frm;
|
||||||
|
if (frm && frm.doctype === doctype) {
|
||||||
|
const $row = $(e.target).closest('.grid-row');
|
||||||
|
const row_index = $row.index();
|
||||||
|
const item = frm.doc.items?.[row_index];
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.update_tax_article_readonly(frm, child_doctype, item.name);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Дополнительный обработчик клика по строке
|
||||||
|
$(document).on('click', '.grid-row', (e) => {
|
||||||
|
const frm = cur_frm;
|
||||||
|
if (frm && frm.doctype === doctype) {
|
||||||
|
const $row = $(e.currentTarget);
|
||||||
|
const row_index = $row.index();
|
||||||
|
const item = frm.doc.items?.[row_index];
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.update_tax_article_readonly(frm, child_doctype, item.name);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
run_calculations: function(frm, cdt, cdn) {
|
||||||
|
this.calculate_vat_fields(cdt, cdn, frm);
|
||||||
|
this.update_tax_article_readonly(frm, cdt, cdn);
|
||||||
|
},
|
||||||
|
|
||||||
|
setup_vat_system: function(frm, child_doctype) {
|
||||||
|
if (!frm.doc.items?.length) return;
|
||||||
|
frm.doc.items.forEach(item => {
|
||||||
|
if (item.item_code) {
|
||||||
|
this.calculate_vat_fields(child_doctype, item.name, frm);
|
||||||
|
this.update_tax_article_readonly(frm, child_doctype, item.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. ОБНОВЛЕННАЯ функция с поддержкой индивидуальных фильтров для tax_article
|
||||||
|
update_tax_article_readonly: function(frm, cdt, cdn) {
|
||||||
|
const item = locals[cdt][cdn];
|
||||||
|
if (!item?.item_code) return;
|
||||||
|
|
||||||
|
const is_editable = this.is_tax_article_allowed(item.item_tax_template);
|
||||||
|
const readonly_value = is_editable ? 0 : 1;
|
||||||
|
|
||||||
|
// Метод 1: Обновление через grid (для всех строк)
|
||||||
|
const grid = frm.fields_dict.items.grid;
|
||||||
|
if (grid) {
|
||||||
|
try {
|
||||||
|
grid.update_docfield_property("tax_article", "read_only", readonly_value);
|
||||||
|
|
||||||
|
// Устанавливаем фильтр для tax_article в зависимости от налогового шаблона
|
||||||
|
if (is_editable) {
|
||||||
|
const filter = this.get_tax_article_filter(item.item_tax_template);
|
||||||
|
if (filter) {
|
||||||
|
grid.update_docfield_property("tax_article", "get_query", function() {
|
||||||
|
return {
|
||||||
|
filters: filter
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Игнорируем ошибки
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод 2: Обновление конкретной строки (более надежно)
|
||||||
|
const row_index = frm.doc.items.findIndex(i => i.name === cdn);
|
||||||
|
if (row_index !== -1) {
|
||||||
|
const grid_row = grid?.grid_rows?.[row_index];
|
||||||
|
if (grid_row) {
|
||||||
|
try {
|
||||||
|
grid_row.toggle_editable("tax_article", is_editable);
|
||||||
|
|
||||||
|
// Устанавливаем фильтр для конкретной строки
|
||||||
|
if (is_editable) {
|
||||||
|
const filter = this.get_tax_article_filter(item.item_tax_template);
|
||||||
|
if (filter) {
|
||||||
|
const tax_article_field = grid_row.get_field('tax_article');
|
||||||
|
if (tax_article_field) {
|
||||||
|
tax_article_field.get_query = function() {
|
||||||
|
return {
|
||||||
|
filters: filter
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Игнорируем ошибки
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем tax_article если поле должно быть заблокировано или изменился тип НДС
|
||||||
|
if (!is_editable && item.tax_article) {
|
||||||
|
frappe.model.set_value(cdt, cdn, 'tax_article', '');
|
||||||
|
} else if (is_editable && item.tax_article) {
|
||||||
|
// Проверяем, соответствует ли текущий tax_article новому фильтру
|
||||||
|
this.validate_tax_article_against_filter(frm, cdt, cdn, item.item_tax_template);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ОБНОВЛЕНО: tax_article разрешен только для ƏDV 0% и ƏDV-dən azadolma
|
||||||
|
// Для обоих типов НДС 18% поле заблокировано
|
||||||
|
is_tax_article_allowed: function(tax_template) {
|
||||||
|
return tax_template === "ƏDV 0%" || tax_template === "ƏDV-dən azadolma";
|
||||||
|
},
|
||||||
|
|
||||||
|
// НОВАЯ функция: возвращает фильтр для tax_article в зависимости от налогового шаблона
|
||||||
|
get_tax_article_filter: function(tax_template) {
|
||||||
|
switch (tax_template) {
|
||||||
|
case "ƏDV 0%":
|
||||||
|
return {
|
||||||
|
"parent_tax_article": "ƏDV-yə 0% dərəcə ilə tutulan əməliyyatlar"
|
||||||
|
};
|
||||||
|
case "ƏDV-dən azadolma":
|
||||||
|
return {
|
||||||
|
"parent_tax_article": "ƏDV-dən azad olunan əməliyyatlar"
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// НОВАЯ функция: проверяет соответствие текущего tax_article новому фильтру
|
||||||
|
validate_tax_article_against_filter: function(frm, cdt, cdn, tax_template) {
|
||||||
|
const item = locals[cdt][cdn];
|
||||||
|
if (!item.tax_article) return;
|
||||||
|
|
||||||
|
const filter = this.get_tax_article_filter(tax_template);
|
||||||
|
if (!filter) return;
|
||||||
|
|
||||||
|
// Проверяем через API, подходит ли текущий tax_article под новый фильтр
|
||||||
|
frappe.call({
|
||||||
|
method: "frappe.client.get_value",
|
||||||
|
args: {
|
||||||
|
doctype: "Tax Article", // Предполагаемое название doctype
|
||||||
|
filters: {
|
||||||
|
name: item.tax_article,
|
||||||
|
...filter
|
||||||
|
},
|
||||||
|
fieldname: "name"
|
||||||
|
},
|
||||||
|
callback: (response) => {
|
||||||
|
if (!response.message) {
|
||||||
|
// Текущий tax_article не подходит под новый фильтр, очищаем
|
||||||
|
frappe.model.set_value(cdt, cdn, 'tax_article', '');
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __('Tax Article cleared due to template change'),
|
||||||
|
indicator: 'orange'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Альтернативный метод получения актуальных значений из DOM
|
||||||
|
get_current_values: function(frm, cdt, cdn) {
|
||||||
|
const item = locals[cdt][cdn];
|
||||||
|
const row_index = frm.doc.items.findIndex(i => i.name === cdn);
|
||||||
|
|
||||||
|
if (row_index === -1) {
|
||||||
|
return {
|
||||||
|
qty: flt(item.qty || 0),
|
||||||
|
rate: flt(item.rate || 0),
|
||||||
|
amount: flt(item.qty || 0) * flt(item.rate || 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся получить значения из DOM полей
|
||||||
|
const grid_row = frm.fields_dict.items.grid?.grid_rows?.[row_index];
|
||||||
|
if (grid_row) {
|
||||||
|
const qty_field = grid_row.get_field('qty');
|
||||||
|
const rate_field = grid_row.get_field('rate');
|
||||||
|
|
||||||
|
const qty = qty_field ? flt(qty_field.get_value()) : flt(item.qty || 0);
|
||||||
|
const rate = rate_field ? flt(rate_field.get_value()) : flt(item.rate || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
qty: qty,
|
||||||
|
rate: rate,
|
||||||
|
amount: qty * rate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback к обычному способу
|
||||||
|
return {
|
||||||
|
qty: flt(item.qty || 0),
|
||||||
|
rate: flt(item.rate || 0),
|
||||||
|
amount: flt(item.qty || 0) * flt(item.rate || 0)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate_vat_fields: function(cdt, cdn, frm = null) {
|
||||||
|
const item = locals[cdt][cdn];
|
||||||
|
|
||||||
|
// Получаем актуальные значения
|
||||||
|
let amount;
|
||||||
|
if (frm) {
|
||||||
|
const values = this.get_current_values(frm, cdt, cdn);
|
||||||
|
amount = values.amount;
|
||||||
|
} else {
|
||||||
|
// Fallback: вычисляем из значений в locals
|
||||||
|
const qty = flt(item.qty || 0);
|
||||||
|
const rate = flt(item.rate || 0);
|
||||||
|
amount = qty * rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем все поля НДС перед расчетом
|
||||||
|
this.clear_all_vat_fields(cdt, cdn);
|
||||||
|
|
||||||
|
if (amount <= 0) return;
|
||||||
|
|
||||||
|
const tax_template = item.item_tax_template;
|
||||||
|
|
||||||
|
switch (tax_template) {
|
||||||
|
case "ƏDV 18%":
|
||||||
|
// НДС 18% СВЕРХУ: к amount добавляется 18%
|
||||||
|
// amount = 100 -> vat_amount = 18, total = 118
|
||||||
|
const vat_amount_top = amount * 0.18;
|
||||||
|
const total_with_vat_top = amount + vat_amount_top;
|
||||||
|
|
||||||
|
frappe.model.set_value(cdt, cdn, 'vat_18_percent_with_amount', total_with_vat_top);
|
||||||
|
frappe.model.set_value(cdt, cdn, 'vat_amount', vat_amount_top);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ƏDV daxil 18%":
|
||||||
|
// НДС 18% В ТОМ ЧИСЛЕ: из amount выделяется НДС
|
||||||
|
// amount = 100 -> vat_amount = 15.25, amount без НДС = 84.75, total = 100
|
||||||
|
const total_with_vat_included = amount;
|
||||||
|
const amount_without_vat = total_with_vat_included / 1.18;
|
||||||
|
const vat_amount_included = total_with_vat_included - amount_without_vat;
|
||||||
|
|
||||||
|
frappe.model.set_value(cdt, cdn, 'vat_18_percent_with_amount', total_with_vat_included);
|
||||||
|
frappe.model.set_value(cdt, cdn, 'vat_amount', vat_amount_included);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ƏDV 0%":
|
||||||
|
frappe.model.set_value(cdt, cdn, 'vat_0_percent_with_amount', amount);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ƏDV-dən azadolma":
|
||||||
|
frappe.model.set_value(cdt, cdn, 'vat_free_amount', amount);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
frappe.model.set_value(cdt, cdn, 'amount_without_vat', amount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear_all_vat_fields: function(cdt, cdn) {
|
||||||
|
const vat_fields = [
|
||||||
|
'vat_18_percent_with_amount',
|
||||||
|
'vat_amount',
|
||||||
|
'vat_0_percent_with_amount',
|
||||||
|
'amount_without_vat',
|
||||||
|
'vat_free_amount'
|
||||||
|
];
|
||||||
|
vat_fields.forEach(fieldname => {
|
||||||
|
frappe.model.set_value(cdt, cdn, fieldname, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue