From e68a1deb4c521702f1c788aa38a1a9638907450d Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Mon, 16 Feb 2026 17:21:09 +0400 Subject: [PATCH] Fix won/lost checkboxes in Sales Invoice for gambling companies - Rewrote won/lost checkbox logic with clean code approach - Removed complex HTML synchronization in favor of native Frappe checkboxes - Added CSS to display checkboxes inline (side by side) instead of stacked - Implemented automatic enforcement: one checkbox must always be selected - Added client-side validation that auto-corrects invalid states - Added server-side validation for gambling companies only - Removed old column break and section break fields - Fields now only validate for gambling company main activities Changes: - custom_fields.py: Simplified won/lost field definitions, removed HTML field - sales_invoice_vat.js: Clean logic + CSS for inline display + auto-selection - sales_invoice.py: Server validation with auto-correction - hooks.py: Added validate_won_lost_fields hook - patches: Added cleanup patches for old fields Co-Authored-By: Claude Sonnet 4.5 --- jey_erp/custom/sales_invoice.py | 30 +++ jey_erp/custom_fields.py | 35 +--- jey_erp/hooks.py | 4 + jey_erp/patches.txt | 4 +- jey_erp/patches/remove_old_won_lost_fields.py | 17 ++ jey_erp/patches/remove_won_lost_breaks.py | 22 ++ jey_erp/public/js/sales_invoice_vat.js | 194 ++++++++---------- 7 files changed, 174 insertions(+), 132 deletions(-) create mode 100644 jey_erp/patches/remove_old_won_lost_fields.py create mode 100644 jey_erp/patches/remove_won_lost_breaks.py diff --git a/jey_erp/custom/sales_invoice.py b/jey_erp/custom/sales_invoice.py index 3d656d9..f2ff4bd 100644 --- a/jey_erp/custom/sales_invoice.py +++ b/jey_erp/custom/sales_invoice.py @@ -3,6 +3,36 @@ 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 diff --git a/jey_erp/custom_fields.py b/jey_erp/custom_fields.py index a10c77b..6e00060 100644 --- a/jey_erp/custom_fields.py +++ b/jey_erp/custom_fields.py @@ -607,43 +607,26 @@ def create_custom_fields(): insert_after='act_type', depends_on='eval:doc.purchase_type && doc.act_type == "Import"' ), - # Won and Lost checkboxes after section_break_42 (только HTML для v15) - dict( - fieldname='won_lost_html', - fieldtype='HTML', - insert_after='section_break_42', - depends_on='eval:doc.company_main_activity && ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"].includes(doc.company_main_activity)', - options=""" -
- - -
- """ - ), - # Скрытые поля для хранения значений + # Won and Lost checkboxes - в одну строку dict( fieldname='won', label='Won', fieldtype='Check', - insert_after='won_lost_html', - hidden=1, + insert_after='section_break_42', + depends_on='eval:doc.company_main_activity && ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"].includes(doc.company_main_activity)', default=1, - allow_on_submit=1 + allow_on_submit=1, + mandatory_depends_on='eval:doc.company_main_activity && ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"].includes(doc.company_main_activity)' ), dict( fieldname='lost', - label='Lost', + label='Lost', fieldtype='Check', insert_after='won', - hidden=1, + depends_on='eval:doc.company_main_activity && ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"].includes(doc.company_main_activity)', default=0, - allow_on_submit=1 + allow_on_submit=1, + mandatory_depends_on='eval:doc.company_main_activity && ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"].includes(doc.company_main_activity)' ), # New comments section after time_sheet_list dict( diff --git a/jey_erp/hooks.py b/jey_erp/hooks.py index e3229fd..f6b3ce0 100644 --- a/jey_erp/hooks.py +++ b/jey_erp/hooks.py @@ -34,8 +34,12 @@ doc_events = { }, "Sales Invoice": { "validate": [ + "jey_erp.custom.sales_invoice.validate_won_lost_fields", "jey_erp.custom.sales_invoice.calculate_tax_free_amounts", ], + "before_save": [ + "jey_erp.custom.sales_invoice.validate_won_lost_fields", + ], "on_update": [ "jey_erp.custom.sales_invoice.calculate_tax_free_amounts", ] diff --git a/jey_erp/patches.txt b/jey_erp/patches.txt index 419bef0..0e2687e 100644 --- a/jey_erp/patches.txt +++ b/jey_erp/patches.txt @@ -4,4 +4,6 @@ [post_model_sync] # Patches added in this section will be executed after doctypes are migrated -jey_erp.patches.create_asset_categories_post_migration \ No newline at end of file +jey_erp.patches.create_asset_categories_post_migration +jey_erp.patches.remove_old_won_lost_fields +jey_erp.patches.remove_won_lost_breaks \ No newline at end of file diff --git a/jey_erp/patches/remove_old_won_lost_fields.py b/jey_erp/patches/remove_old_won_lost_fields.py new file mode 100644 index 0000000..2c4161d --- /dev/null +++ b/jey_erp/patches/remove_old_won_lost_fields.py @@ -0,0 +1,17 @@ +import frappe + +def execute(): + """Remove old won_lost_html field if it exists""" + + # Remove old HTML field + if frappe.db.exists("Custom Field", {"dt": "Sales Invoice", "fieldname": "won_lost_html"}): + try: + custom_field_name = frappe.db.get_value("Custom Field", + {"dt": "Sales Invoice", "fieldname": "won_lost_html"}, "name") + frappe.delete_doc("Custom Field", custom_field_name) + frappe.db.commit() + print("Deleted old won_lost_html field") + except Exception as e: + print(f"Failed to delete won_lost_html: {e}") + + print("Cleanup completed") diff --git a/jey_erp/patches/remove_won_lost_breaks.py b/jey_erp/patches/remove_won_lost_breaks.py new file mode 100644 index 0000000..4531ef4 --- /dev/null +++ b/jey_erp/patches/remove_won_lost_breaks.py @@ -0,0 +1,22 @@ +import frappe + +def execute(): + """Remove old column break and section break fields for won/lost""" + + fields_to_remove = [ + "won_lost_column_break", + "won_lost_section_break_end" + ] + + for fieldname in fields_to_remove: + if frappe.db.exists("Custom Field", {"dt": "Sales Invoice", "fieldname": fieldname}): + try: + custom_field_name = frappe.db.get_value("Custom Field", + {"dt": "Sales Invoice", "fieldname": fieldname}, "name") + frappe.delete_doc("Custom Field", custom_field_name) + print(f"Deleted field: {fieldname}") + except Exception as e: + print(f"Failed to delete {fieldname}: {e}") + + frappe.db.commit() + print("Cleanup completed") diff --git a/jey_erp/public/js/sales_invoice_vat.js b/jey_erp/public/js/sales_invoice_vat.js index d717797..f4b14f3 100644 --- a/jey_erp/public/js/sales_invoice_vat.js +++ b/jey_erp/public/js/sales_invoice_vat.js @@ -1,168 +1,152 @@ // Запускаем логику калькулятора НДС для Sales Invoice jey_erp.vat_calculator.init('Sales Invoice'); -// Логика для чекбоксов Won/Lost и Seller/Organizer +// Простая логика для чекбоксов Won/Lost и Seller/Organizer frappe.ui.form.on('Sales Invoice', { refresh: function(frm) { - // Убедимся что одна из галочек Won/Lost всегда выбрана при загрузке формы - if (!frm.doc.won && !frm.doc.lost) { - frm.set_value('won', 1); + // Убедимся что одна из галочек Won/Lost выбрана (для gambling компаний) + if (is_gambling_company(frm)) { + if (!frm.doc.won && !frm.doc.lost) { + frm.set_value('won', 1); + } } - + // Убедимся что одна из галочек Seller/Organizer выбрана (если поля видимы) if (should_show_seller_organizer(frm)) { if (!frm.doc.seller && !frm.doc.organizer) { frm.set_value('organizer', 1); } } - - // Настройка HTML чекбоксов после загрузки формы - setTimeout(function() { - setup_won_lost_html_checkboxes(frm); - }, 1000); + + // Применяем CSS для отображения Won и Lost в одну строку близко друг к другу + apply_inline_checkbox_style(frm); }, - - // Обработчики для Won/Lost + + onload: function(frm) { + // Применяем стиль при загрузке формы + apply_inline_checkbox_style(frm); + }, + + // Обработчики для Won/Lost - гарантируем что всегда одна галочка включена won: function(frm) { - // Синхронизируем HTML чекбоксы при изменении скрытых полей - update_html_checkboxes(frm); + if (!is_gambling_company(frm)) return; + if (frm.doc.won) { - frm.set_value('lost', 0); + // Включили won - выключаем lost + if (frm.doc.lost) { + frm.set_value('lost', 0); + } } else { + // Выключили won - автоматически включаем lost frm.set_value('lost', 1); } }, - + lost: function(frm) { - // Синхронизируем HTML чекбоксы при изменении скрытых полей - update_html_checkboxes(frm); + if (!is_gambling_company(frm)) return; + if (frm.doc.lost) { - frm.set_value('won', 0); + // Включили lost - выключаем won + if (frm.doc.won) { + frm.set_value('won', 0); + } } else { + // Выключили lost - автоматически включаем won frm.set_value('won', 1); } }, - + + before_save: function(frm) { + // Перед сохранением убеждаемся что одна галочка включена + if (is_gambling_company(frm)) { + if (!frm.doc.won && !frm.doc.lost) { + frm.set_value('won', 1); + } + } + }, + + validate: function(frm) { + // Валидация для gambling компаний - автоматически исправляем + if (is_gambling_company(frm)) { + if (!frm.doc.won && !frm.doc.lost) { + // Если обе выключены, включаем won + frm.set_value('won', 1); + } else if (frm.doc.won && frm.doc.lost) { + // Если обе включены, оставляем только won + frm.set_value('lost', 0); + } + } + }, + // Обработчики для Seller/Organizer seller: function(frm) { if (should_show_seller_organizer(frm)) { if (frm.doc.seller) { frm.set_value('organizer', 0); - } else { - // Если seller снят, обязательно ставим organizer + } else if (!frm.doc.organizer) { + // Если seller снят и organizer тоже не выбран, ставим organizer frm.set_value('organizer', 1); } } }, - + organizer: function(frm) { if (should_show_seller_organizer(frm)) { if (frm.doc.organizer) { frm.set_value('seller', 0); - } else { - // Если organizer снят, обязательно ставим seller + } else if (!frm.doc.seller) { + // Если organizer снят и seller тоже не выбран, ставим seller frm.set_value('seller', 1); } } }, - + // Обработчик изменения company_main_activity company_main_activity: function(frm) { // Если поля Seller/Organizer должны быть видимы, устанавливаем значения по умолчанию if (should_show_seller_organizer(frm)) { if (!frm.doc.seller && !frm.doc.organizer) { frm.set_value('organizer', 1); - frm.set_value('seller', 0); - } - } - }, - - // При валидации формы - validate: function(frm) { - // Проверяем Won/Lost - if (!frm.doc.won && !frm.doc.lost) { - frm.set_value('won', 1); - frm.set_value('lost', 0); - } - - // Проверяем Seller/Organizer (если поля должны быть видимы) - if (should_show_seller_organizer(frm)) { - if (!frm.doc.seller && !frm.doc.organizer) { - frm.set_value('organizer', 1); - frm.set_value('seller', 0); } } } }); -// Функция проверки, должны ли быть видимы поля Seller/Organizer -function should_show_seller_organizer(frm) { +// Функция проверки, является ли компания gambling компанией +function is_gambling_company(frm) { const allowed_activities = ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"]; - return frm.doc.company_main_activity && + return frm.doc.company_main_activity && allowed_activities.includes(frm.doc.company_main_activity); } -// Функция настройки HTML чекбоксов для Won/Lost -function setup_won_lost_html_checkboxes(frm) { - const wonCheckbox = document.querySelector('.won-checkbox'); - const lostCheckbox = document.querySelector('.lost-checkbox'); - - if (!wonCheckbox || !lostCheckbox) return; - - // Синхронизация с текущими значениями полей - wonCheckbox.checked = frm.doc.won ? true : false; - lostCheckbox.checked = frm.doc.lost ? true : false; - - // Обеспечиваем что всегда выбрана одна галочка - if (!wonCheckbox.checked && !lostCheckbox.checked) { - wonCheckbox.checked = true; - frm.set_value('won', 1); - } - - // Удаляем старые обработчики событий - wonCheckbox.removeEventListener('change', wonCheckbox._changeHandler); - lostCheckbox.removeEventListener('change', lostCheckbox._changeHandler); - - // Добавляем новые обработчики событий - wonCheckbox._changeHandler = function() { - if (this.checked) { - lostCheckbox.checked = false; - frm.set_value('lost', 0); - frm.set_value('won', 1); - } else { - lostCheckbox.checked = true; - frm.set_value('lost', 1); - frm.set_value('won', 0); - } - }; - - lostCheckbox._changeHandler = function() { - if (this.checked) { - wonCheckbox.checked = false; - frm.set_value('won', 0); - frm.set_value('lost', 1); - } else { - wonCheckbox.checked = true; - frm.set_value('won', 1); - frm.set_value('lost', 0); - } - }; - - wonCheckbox.addEventListener('change', wonCheckbox._changeHandler); - lostCheckbox.addEventListener('change', lostCheckbox._changeHandler); +// Функция проверки, должны ли быть видимы поля Seller/Organizer (то же самое условие) +function should_show_seller_organizer(frm) { + return is_gambling_company(frm); } -// Функция синхронизации HTML чекбоксов со скрытыми полями -function update_html_checkboxes(frm) { - setTimeout(function() { - const wonCheckbox = document.querySelector('.won-checkbox'); - const lostCheckbox = document.querySelector('.lost-checkbox'); +// Функция для применения CSS стилей чтобы чекбоксы были в одну строку +function apply_inline_checkbox_style(frm) { + // Добавляем CSS если еще не добавлен + if (!document.getElementById('won-lost-inline-style')) { + const style = document.createElement('style'); + style.id = 'won-lost-inline-style'; + style.textContent = ` + /* Делаем Won и Lost в одну строку */ + [data-fieldname="won"], + [data-fieldname="lost"] { + display: inline-block !important; + width: auto !important; + margin-right: 30px !important; + vertical-align: top !important; + } - if (wonCheckbox && lostCheckbox) { - wonCheckbox.checked = frm.doc.won ? true : false; - lostCheckbox.checked = frm.doc.lost ? true : false; - } - }, 100); + /* Убираем перенос строки после Won */ + [data-fieldname="won"] + [data-fieldname="lost"] { + margin-left: 0 !important; + } + `; + document.head.appendChild(style); + } } // Функция синхронизации фактических сумм НДС из items в taxes