238 lines
8.9 KiB
Python
238 lines
8.9 KiB
Python
import frappe
|
||
import json
|
||
from frappe.utils import flt
|
||
|
||
|
||
def validate_won_lost_fields(doc, method=None):
|
||
"""
|
||
Ensure that exactly one of won/lost checkboxes is always selected for gambling companies
|
||
Automatically fixes the issue instead of throwing an error
|
||
"""
|
||
if doc.doctype != "Sales Invoice":
|
||
return
|
||
|
||
# Check if this is a gambling company
|
||
allowed_activities = ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"]
|
||
company_main_activity = doc.get('company_main_activity')
|
||
|
||
if not company_main_activity or company_main_activity not in allowed_activities:
|
||
# Not a gambling company - skip validation
|
||
return
|
||
|
||
# Check if both fields exist
|
||
if not hasattr(doc, 'won') or not hasattr(doc, 'lost'):
|
||
return
|
||
|
||
# Для gambling компаний - автоматически исправляем ситуацию
|
||
if not doc.won and not doc.lost:
|
||
# Если обе выключены - включаем won по умолчанию
|
||
doc.won = 1
|
||
doc.lost = 0
|
||
elif doc.won and doc.lost:
|
||
# Если обе включены - оставляем только won
|
||
doc.lost = 0
|
||
|
||
|
||
def calculate_tax_free_amounts(doc, method=None):
|
||
"""
|
||
Calculate tax free amounts for Sales Invoice
|
||
Called after calculate_taxes_and_totals
|
||
"""
|
||
if doc.doctype != "Sales Invoice":
|
||
return
|
||
|
||
# Clear existing tax_free_amount values
|
||
for tax in doc.get("taxes", []):
|
||
if hasattr(tax, 'tax_free_amount'):
|
||
tax.tax_free_amount = 0.0
|
||
|
||
# Calculate tax free amounts
|
||
for item in doc.get("items", []):
|
||
if not item.get("item_code"):
|
||
continue
|
||
|
||
# Try to get tax template from item row first
|
||
item_tax_template = item.get("item_tax_template")
|
||
item_tax_rate = item.get("item_tax_rate")
|
||
|
||
# If no tax info in item row, get from Item master
|
||
if not item_tax_template and not item_tax_rate:
|
||
try:
|
||
item_doc = frappe.get_doc("Item", item.item_code)
|
||
item_tax_template = item_doc.get("item_tax_template")
|
||
except Exception:
|
||
continue
|
||
|
||
# Process item_tax_rate (JSON format)
|
||
if item_tax_rate:
|
||
try:
|
||
if isinstance(item_tax_rate, str):
|
||
tax_rates = json.loads(item_tax_rate)
|
||
else:
|
||
tax_rates = item_tax_rate
|
||
|
||
for account_head, rate in tax_rates.items():
|
||
if flt(rate) == 0.0:
|
||
# Find corresponding tax in sales invoice
|
||
for doc_tax in doc.get("taxes", []):
|
||
if doc_tax.account_head == account_head:
|
||
if hasattr(doc_tax, 'tax_free_amount'):
|
||
doc_tax.tax_free_amount = flt(doc_tax.tax_free_amount) + flt(item.net_amount)
|
||
break
|
||
except Exception:
|
||
pass
|
||
|
||
# Process item_tax_template
|
||
elif item_tax_template:
|
||
try:
|
||
tax_template = frappe.get_doc("Item Tax Template", item_tax_template)
|
||
|
||
for template_tax in tax_template.get("taxes", []):
|
||
if flt(template_tax.tax_rate) == 0.0:
|
||
# Find corresponding tax in sales invoice
|
||
for doc_tax in doc.get("taxes", []):
|
||
if doc_tax.account_head == template_tax.tax_type:
|
||
if hasattr(doc_tax, 'tax_free_amount'):
|
||
doc_tax.tax_free_amount = flt(doc_tax.tax_free_amount) + flt(item.net_amount)
|
||
break
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
@frappe.whitelist()
|
||
def recalculate_tax_free_amounts(doc):
|
||
"""
|
||
API method to recalculate tax free amounts
|
||
Called from client side when items change
|
||
"""
|
||
if isinstance(doc, str):
|
||
doc = frappe.parse_json(doc)
|
||
|
||
# Convert to document object if it's a dict
|
||
if isinstance(doc, dict):
|
||
sales_invoice = frappe.get_doc(doc)
|
||
else:
|
||
sales_invoice = doc
|
||
|
||
# Calculate tax free amounts
|
||
calculate_tax_free_amounts(sales_invoice)
|
||
|
||
# Return updated taxes
|
||
taxes_data = []
|
||
for tax in sales_invoice.get("taxes", []):
|
||
tax_dict = tax.as_dict() if hasattr(tax, 'as_dict') else dict(tax)
|
||
taxes_data.append(tax_dict)
|
||
|
||
return taxes_data
|
||
|
||
|
||
@frappe.whitelist()
|
||
def sync_vat_to_taxes(doc):
|
||
"""
|
||
Синхронизирует фактические VAT amounts из items в taxes таблицу
|
||
Вызывается из клиентского JavaScript
|
||
"""
|
||
if isinstance(doc, str):
|
||
doc = frappe.parse_json(doc)
|
||
|
||
# Словарь для агрегации VAT amounts по account_head
|
||
vat_aggregates = {}
|
||
|
||
# Агрегируем vat_amount из items
|
||
for item in doc.get("items", []):
|
||
if not item.get("item_code"):
|
||
continue
|
||
|
||
# Округляем до 2 знаков при чтении
|
||
vat_amount = flt(item.get("vat_amount", 0), 2)
|
||
if vat_amount <= 0:
|
||
continue
|
||
|
||
item_tax_template = item.get("item_tax_template")
|
||
if not item_tax_template:
|
||
continue
|
||
|
||
# Получить account_head из налогового шаблона
|
||
try:
|
||
tax_template = frappe.get_doc("Item Tax Template", item_tax_template)
|
||
if tax_template.taxes and len(tax_template.taxes) > 0:
|
||
account_head = tax_template.taxes[0].tax_type
|
||
|
||
if account_head not in vat_aggregates:
|
||
vat_aggregates[account_head] = 0.0
|
||
vat_aggregates[account_head] += vat_amount
|
||
except Exception:
|
||
continue
|
||
|
||
# Обновляем taxes таблицу
|
||
updated_taxes = []
|
||
for tax in doc.get("taxes", []):
|
||
if tax.get("account_head") in vat_aggregates:
|
||
# Округляем итоговую сумму до 2 знаков
|
||
tax["tax_amount"] = flt(vat_aggregates[tax["account_head"]], 2)
|
||
tax["base_tax_amount"] = flt(vat_aggregates[tax["account_head"]], 2)
|
||
updated_taxes.append(tax)
|
||
|
||
return updated_taxes
|
||
|
||
|
||
def sync_actual_vat_amounts(doc, method=None):
|
||
"""
|
||
Перезаписывает tax_amount в taxes таблице фактическими суммами НДС из items.
|
||
Вызывается после стандартного calculate_taxes_and_totals.
|
||
"""
|
||
if doc.doctype not in ["Sales Invoice", "Sales Order"]:
|
||
return
|
||
|
||
# Словарь для агрегации сумм НДС по account_head
|
||
vat_aggregates = {}
|
||
|
||
# Агрегировать фактические суммы НДС из items
|
||
for item in doc.get("items", []):
|
||
if not item.get("item_code"):
|
||
continue
|
||
|
||
# Получить фактическую сумму НДС из строки товара
|
||
vat_amount = flt(item.get("vat_amount", 0), 2)
|
||
|
||
if vat_amount <= 0:
|
||
continue
|
||
|
||
# Получить налоговый шаблон товара
|
||
item_tax_template = item.get("item_tax_template")
|
||
|
||
if not item_tax_template:
|
||
# Если нет шаблона в строке, проверить Item master
|
||
try:
|
||
item_doc = frappe.get_doc("Item", item.item_code)
|
||
item_tax_template = item_doc.get("item_tax_template")
|
||
except Exception:
|
||
continue
|
||
|
||
if not item_tax_template:
|
||
continue
|
||
|
||
# Получить account_head из налогового шаблона
|
||
try:
|
||
tax_template = frappe.get_doc("Item Tax Template", item_tax_template)
|
||
|
||
# Взять первый tax_type (account_head) из шаблона
|
||
if tax_template.taxes and len(tax_template.taxes) > 0:
|
||
account_head = tax_template.taxes[0].tax_type
|
||
|
||
# Агрегировать сумму НДС с округлением
|
||
if account_head not in vat_aggregates:
|
||
vat_aggregates[account_head] = 0.0
|
||
vat_aggregates[account_head] += vat_amount
|
||
except Exception:
|
||
continue
|
||
|
||
# ПЕРЕЗАПИСАТЬ tax_amount в taxes таблице агрегированными суммами
|
||
for tax in doc.get("taxes", []):
|
||
account_head = tax.account_head
|
||
if account_head in vat_aggregates:
|
||
# Заменить стандартный расчет ERPNext нашей фактической суммой
|
||
# Округляем до 2 знаков
|
||
tax.tax_amount = flt(vat_aggregates[account_head], 2)
|
||
# Также обновить base_tax_amount для мультивалютных документов
|
||
tax.base_tax_amount = flt(vat_aggregates[account_head], 2) |