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 <noreply@anthropic.com>
This commit is contained in:
parent
4d1739d352
commit
e68a1deb4c
|
|
@ -3,6 +3,36 @@ import json
|
||||||
from frappe.utils import flt
|
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):
|
def calculate_tax_free_amounts(doc, method=None):
|
||||||
"""
|
"""
|
||||||
Calculate tax free amounts for Sales Invoice
|
Calculate tax free amounts for Sales Invoice
|
||||||
|
|
|
||||||
|
|
@ -607,43 +607,26 @@ def create_custom_fields():
|
||||||
insert_after='act_type',
|
insert_after='act_type',
|
||||||
depends_on='eval:doc.purchase_type && doc.act_type == "Import"'
|
depends_on='eval:doc.purchase_type && doc.act_type == "Import"'
|
||||||
),
|
),
|
||||||
# Won and Lost checkboxes after section_break_42 (только HTML для v15)
|
# Won and Lost checkboxes - в одну строку
|
||||||
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="""
|
|
||||||
<div style="display: flex; gap: 50px; margin: 8px 0;">
|
|
||||||
<label style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<input type="checkbox" class="won-checkbox" checked style="margin: 0;">
|
|
||||||
<span style="font-weight: 500;">Won</span>
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<input type="checkbox" class="lost-checkbox" style="margin: 0;">
|
|
||||||
<span style="font-weight: 500;">Lost</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
# Скрытые поля для хранения значений
|
|
||||||
dict(
|
dict(
|
||||||
fieldname='won',
|
fieldname='won',
|
||||||
label='Won',
|
label='Won',
|
||||||
fieldtype='Check',
|
fieldtype='Check',
|
||||||
insert_after='won_lost_html',
|
insert_after='section_break_42',
|
||||||
hidden=1,
|
depends_on='eval:doc.company_main_activity && ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"].includes(doc.company_main_activity)',
|
||||||
default=1,
|
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(
|
dict(
|
||||||
fieldname='lost',
|
fieldname='lost',
|
||||||
label='Lost',
|
label='Lost',
|
||||||
fieldtype='Check',
|
fieldtype='Check',
|
||||||
insert_after='won',
|
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,
|
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
|
# New comments section after time_sheet_list
|
||||||
dict(
|
dict(
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,12 @@ doc_events = {
|
||||||
},
|
},
|
||||||
"Sales Invoice": {
|
"Sales Invoice": {
|
||||||
"validate": [
|
"validate": [
|
||||||
|
"jey_erp.custom.sales_invoice.validate_won_lost_fields",
|
||||||
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
||||||
],
|
],
|
||||||
|
"before_save": [
|
||||||
|
"jey_erp.custom.sales_invoice.validate_won_lost_fields",
|
||||||
|
],
|
||||||
"on_update": [
|
"on_update": [
|
||||||
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,6 @@
|
||||||
|
|
||||||
[post_model_sync]
|
[post_model_sync]
|
||||||
# Patches added in this section will be executed after doctypes are migrated
|
# Patches added in this section will be executed after doctypes are migrated
|
||||||
jey_erp.patches.create_asset_categories_post_migration
|
jey_erp.patches.create_asset_categories_post_migration
|
||||||
|
jey_erp.patches.remove_old_won_lost_fields
|
||||||
|
jey_erp.patches.remove_won_lost_breaks
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -1,168 +1,152 @@
|
||||||
// Запускаем логику калькулятора НДС для Sales Invoice
|
// Запускаем логику калькулятора НДС для Sales Invoice
|
||||||
jey_erp.vat_calculator.init('Sales Invoice');
|
jey_erp.vat_calculator.init('Sales Invoice');
|
||||||
|
|
||||||
// Логика для чекбоксов Won/Lost и Seller/Organizer
|
// Простая логика для чекбоксов Won/Lost и Seller/Organizer
|
||||||
frappe.ui.form.on('Sales Invoice', {
|
frappe.ui.form.on('Sales Invoice', {
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
// Убедимся что одна из галочек Won/Lost всегда выбрана при загрузке формы
|
// Убедимся что одна из галочек Won/Lost выбрана (для gambling компаний)
|
||||||
if (!frm.doc.won && !frm.doc.lost) {
|
if (is_gambling_company(frm)) {
|
||||||
frm.set_value('won', 1);
|
if (!frm.doc.won && !frm.doc.lost) {
|
||||||
|
frm.set_value('won', 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Убедимся что одна из галочек Seller/Organizer выбрана (если поля видимы)
|
// Убедимся что одна из галочек Seller/Organizer выбрана (если поля видимы)
|
||||||
if (should_show_seller_organizer(frm)) {
|
if (should_show_seller_organizer(frm)) {
|
||||||
if (!frm.doc.seller && !frm.doc.organizer) {
|
if (!frm.doc.seller && !frm.doc.organizer) {
|
||||||
frm.set_value('organizer', 1);
|
frm.set_value('organizer', 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Настройка HTML чекбоксов после загрузки формы
|
// Применяем CSS для отображения Won и Lost в одну строку близко друг к другу
|
||||||
setTimeout(function() {
|
apply_inline_checkbox_style(frm);
|
||||||
setup_won_lost_html_checkboxes(frm);
|
|
||||||
}, 1000);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Обработчики для Won/Lost
|
onload: function(frm) {
|
||||||
|
// Применяем стиль при загрузке формы
|
||||||
|
apply_inline_checkbox_style(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Обработчики для Won/Lost - гарантируем что всегда одна галочка включена
|
||||||
won: function(frm) {
|
won: function(frm) {
|
||||||
// Синхронизируем HTML чекбоксы при изменении скрытых полей
|
if (!is_gambling_company(frm)) return;
|
||||||
update_html_checkboxes(frm);
|
|
||||||
if (frm.doc.won) {
|
if (frm.doc.won) {
|
||||||
frm.set_value('lost', 0);
|
// Включили won - выключаем lost
|
||||||
|
if (frm.doc.lost) {
|
||||||
|
frm.set_value('lost', 0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Выключили won - автоматически включаем lost
|
||||||
frm.set_value('lost', 1);
|
frm.set_value('lost', 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
lost: function(frm) {
|
lost: function(frm) {
|
||||||
// Синхронизируем HTML чекбоксы при изменении скрытых полей
|
if (!is_gambling_company(frm)) return;
|
||||||
update_html_checkboxes(frm);
|
|
||||||
if (frm.doc.lost) {
|
if (frm.doc.lost) {
|
||||||
frm.set_value('won', 0);
|
// Включили lost - выключаем won
|
||||||
|
if (frm.doc.won) {
|
||||||
|
frm.set_value('won', 0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Выключили lost - автоматически включаем won
|
||||||
frm.set_value('won', 1);
|
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/Organizer
|
||||||
seller: function(frm) {
|
seller: function(frm) {
|
||||||
if (should_show_seller_organizer(frm)) {
|
if (should_show_seller_organizer(frm)) {
|
||||||
if (frm.doc.seller) {
|
if (frm.doc.seller) {
|
||||||
frm.set_value('organizer', 0);
|
frm.set_value('organizer', 0);
|
||||||
} else {
|
} else if (!frm.doc.organizer) {
|
||||||
// Если seller снят, обязательно ставим organizer
|
// Если seller снят и organizer тоже не выбран, ставим organizer
|
||||||
frm.set_value('organizer', 1);
|
frm.set_value('organizer', 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
organizer: function(frm) {
|
organizer: function(frm) {
|
||||||
if (should_show_seller_organizer(frm)) {
|
if (should_show_seller_organizer(frm)) {
|
||||||
if (frm.doc.organizer) {
|
if (frm.doc.organizer) {
|
||||||
frm.set_value('seller', 0);
|
frm.set_value('seller', 0);
|
||||||
} else {
|
} else if (!frm.doc.seller) {
|
||||||
// Если organizer снят, обязательно ставим seller
|
// Если organizer снят и seller тоже не выбран, ставим seller
|
||||||
frm.set_value('seller', 1);
|
frm.set_value('seller', 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Обработчик изменения company_main_activity
|
// Обработчик изменения company_main_activity
|
||||||
company_main_activity: function(frm) {
|
company_main_activity: function(frm) {
|
||||||
// Если поля Seller/Organizer должны быть видимы, устанавливаем значения по умолчанию
|
// Если поля Seller/Organizer должны быть видимы, устанавливаем значения по умолчанию
|
||||||
if (should_show_seller_organizer(frm)) {
|
if (should_show_seller_organizer(frm)) {
|
||||||
if (!frm.doc.seller && !frm.doc.organizer) {
|
if (!frm.doc.seller && !frm.doc.organizer) {
|
||||||
frm.set_value('organizer', 1);
|
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
|
// Функция проверки, является ли компания gambling компанией
|
||||||
function should_show_seller_organizer(frm) {
|
function is_gambling_company(frm) {
|
||||||
const allowed_activities = ["92000", "9200003", "9200004", "9200001", "9200002", "9200005"];
|
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);
|
allowed_activities.includes(frm.doc.company_main_activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция настройки HTML чекбоксов для Won/Lost
|
// Функция проверки, должны ли быть видимы поля Seller/Organizer (то же самое условие)
|
||||||
function setup_won_lost_html_checkboxes(frm) {
|
function should_show_seller_organizer(frm) {
|
||||||
const wonCheckbox = document.querySelector('.won-checkbox');
|
return is_gambling_company(frm);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция синхронизации HTML чекбоксов со скрытыми полями
|
// Функция для применения CSS стилей чтобы чекбоксы были в одну строку
|
||||||
function update_html_checkboxes(frm) {
|
function apply_inline_checkbox_style(frm) {
|
||||||
setTimeout(function() {
|
// Добавляем CSS если еще не добавлен
|
||||||
const wonCheckbox = document.querySelector('.won-checkbox');
|
if (!document.getElementById('won-lost-inline-style')) {
|
||||||
const lostCheckbox = document.querySelector('.lost-checkbox');
|
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) {
|
/* Убираем перенос строки после Won */
|
||||||
wonCheckbox.checked = frm.doc.won ? true : false;
|
[data-fieldname="won"] + [data-fieldname="lost"] {
|
||||||
lostCheckbox.checked = frm.doc.lost ? true : false;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
}, 100);
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция синхронизации фактических сумм НДС из items в taxes
|
// Функция синхронизации фактических сумм НДС из items в taxes
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue