final look
This commit is contained in:
parent
192a449741
commit
39213fec27
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -204,7 +204,10 @@ def _apply_custom_filters(query, Item, custom_filters):
|
||||||
query с применёнными фильтрами
|
query с применёнными фильтрами
|
||||||
"""
|
"""
|
||||||
item_meta = frappe.get_meta('Item')
|
item_meta = frappe.get_meta('Item')
|
||||||
valid_fields = {f.fieldname for f in item_meta.fields}
|
valid_fields = {f.fieldname: f for f in item_meta.fields}
|
||||||
|
|
||||||
|
# Для этих типов значение в БД может содержать HTML-обёртку — точное совпадение бессмысленно
|
||||||
|
text_like_types = {'Small Text', 'Text', 'Long Text', 'Text Editor'}
|
||||||
|
|
||||||
for cf in custom_filters:
|
for cf in custom_filters:
|
||||||
field = cstr(cf.get('field', '')).strip()
|
field = cstr(cf.get('field', '')).strip()
|
||||||
|
|
@ -214,8 +217,26 @@ def _apply_custom_filters(query, Item, custom_filters):
|
||||||
if not field or field not in valid_fields:
|
if not field or field not in valid_fields:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
field_meta = valid_fields[field]
|
||||||
item_field = Item[field]
|
item_field = Item[field]
|
||||||
|
|
||||||
|
# Check: значение приходит как 'True'/'False', преобразуем в 1/0
|
||||||
|
if field_meta.fieldtype == 'Check':
|
||||||
|
if value == 'True':
|
||||||
|
value = 1
|
||||||
|
elif value == 'False':
|
||||||
|
value = 0
|
||||||
|
else:
|
||||||
|
continue # пустой выбор — пропускаем фильтр
|
||||||
|
|
||||||
|
# Для текстовых HTML-полей = и != заменяем на LIKE / NOT LIKE
|
||||||
|
if field_meta.fieldtype in text_like_types and operator in ('=', '!='):
|
||||||
|
if operator == '=':
|
||||||
|
query = query.where(item_field.like(f'%{value}%'))
|
||||||
|
else:
|
||||||
|
query = query.where(~item_field.like(f'%{value}%'))
|
||||||
|
continue
|
||||||
|
|
||||||
if operator == '=':
|
if operator == '=':
|
||||||
query = query.where(item_field == value)
|
query = query.where(item_field == value)
|
||||||
elif operator == '!=':
|
elif operator == '!=':
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
.selection-dialog-wrapper .modal-body {
|
.selection-dialog-wrapper .modal-body {
|
||||||
padding: 15px !important;
|
padding: 15px !important;
|
||||||
overflow: hidden !important;
|
overflow: clip !important;
|
||||||
flex: 1 !important;
|
flex: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
/* 3-колоночный layout */
|
/* 3-колоночный layout */
|
||||||
.selection-dialog-content {
|
.selection-dialog-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 0;
|
||||||
height: calc(100vh - 130px);
|
height: calc(100vh - 130px);
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
@ -44,8 +44,7 @@
|
||||||
/* Левая колонка: Фильтры */
|
/* Левая колонка: Фильтры */
|
||||||
.selection-filters {
|
.selection-filters {
|
||||||
flex: 0 0 300px;
|
flex: 0 0 300px;
|
||||||
border-right: 1px solid var(--border-color);
|
padding-right: 12px;
|
||||||
padding-right: 15px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -139,40 +138,85 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Тулбар поиска (заменяет панель сортировки) */
|
/* ── Поиск в левой панели ─────────────────────────────────────────────── */
|
||||||
.selection-sort-toolbar {
|
.selection-search-group {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Убираем margin-bottom у frappe-control внутри поиска */
|
||||||
|
.selection-search-group .frappe-control,
|
||||||
|
.selection-search-group .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка поиска для левой панели */
|
||||||
|
.selection-search-group .form-group {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-search-group .form-group::before {
|
||||||
|
content: '\f002';
|
||||||
|
font-family: FontAwesome;
|
||||||
|
position: absolute;
|
||||||
|
left: 9px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-muted);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-search-group .form-control {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Панель кастомных фильтров над таблицей ───────────────────────────── */
|
||||||
|
.selection-filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
padding: 6px 10px;
|
padding: 8px 10px;
|
||||||
background-color: var(--bg-light-gray);
|
background-color: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 200;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selection-search-icon {
|
/* Awesomplete dropdown должен отображаться поверх таблицы товаров */
|
||||||
color: var(--text-muted);
|
.selection-filter-bar .awesomplete > ul {
|
||||||
flex-shrink: 0;
|
z-index: 1060;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selection-search-input-toolbar {
|
.selection-filter-bar #custom-filters-list:empty {
|
||||||
flex: 1;
|
display: none;
|
||||||
font-size: 13px;
|
}
|
||||||
height: 30px;
|
|
||||||
padding: 4px 8px;
|
.selection-filter-bar .btn-add-custom-filter {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Убираем лишние отступы у frappe-control внутри панели фильтров */
|
||||||
|
.selection-filter-bar .frappe-control,
|
||||||
|
.selection-filter-bar .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ручки resize для панелей */
|
/* Ручки resize для панелей */
|
||||||
.resize-handle {
|
.resize-handle {
|
||||||
flex: 0 0 6px;
|
flex: 0 0 5px;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
background: var(--border-color);
|
background: var(--border-color);
|
||||||
border-radius: 3px;
|
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
margin: 0 2px;
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle:hover,
|
.resize-handle:hover,
|
||||||
|
|
@ -398,8 +442,7 @@ body.selection-resizing * {
|
||||||
/* Правая колонка: Корзина */
|
/* Правая колонка: Корзина */
|
||||||
.selection-cart {
|
.selection-cart {
|
||||||
flex: 0 0 290px;
|
flex: 0 0 290px;
|
||||||
border-left: 1px solid var(--border-color);
|
padding-left: 12px;
|
||||||
padding-left: 15px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
@ -549,66 +592,47 @@ body.selection-resizing * {
|
||||||
|
|
||||||
.custom-filter-row {
|
.custom-filter-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cf-field {
|
/* Только flex-размеры — внешний вид полностью от Frappe */
|
||||||
flex: 1.5;
|
.cf-field-wrap {
|
||||||
font-size: 12px;
|
flex: 2;
|
||||||
padding: 3px 6px;
|
|
||||||
height: auto;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cf-operator {
|
.cf-operator-wrap {
|
||||||
flex: 1;
|
flex: 0 0 100px;
|
||||||
font-size: 12px;
|
|
||||||
padding: 3px 6px;
|
|
||||||
height: auto;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cf-value {
|
.cf-value-wrap {
|
||||||
flex: 1.5;
|
flex: 2;
|
||||||
font-size: 12px;
|
|
||||||
padding: 3px 6px;
|
|
||||||
height: auto;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cf-field-wrap .frappe-control,
|
||||||
|
.cf-operator-wrap .frappe-control,
|
||||||
|
.cf-value-wrap .frappe-control,
|
||||||
|
.cf-field-wrap .form-group,
|
||||||
|
.cf-operator-wrap .form-group,
|
||||||
|
.cf-value-wrap .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.cf-remove {
|
.cf-remove {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-add-custom-filter {
|
.btn-add-custom-filter {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
margin-top: 4px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Link-контролы в строках кастомных фильтров */
|
/* Link-контролы для склада и прайса */
|
||||||
.cf-value-link-wrapper {
|
|
||||||
flex: 1.5;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cf-value-link-wrapper .frappe-control {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cf-value-link-wrapper .form-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cf-value-link-wrapper input.form-control {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 3px 6px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Link-контролы для склада и прайса в левой панели */
|
|
||||||
#selection-warehouse-link .frappe-control,
|
#selection-warehouse-link .frappe-control,
|
||||||
#selection-pricelist-link .frappe-control {
|
#selection-pricelist-link .frappe-control {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,11 @@ class SelectionDialog {
|
||||||
get_dialog_html() {
|
get_dialog_html() {
|
||||||
return `
|
return `
|
||||||
<div class="selection-dialog-content">
|
<div class="selection-dialog-content">
|
||||||
<!-- Left column: Filters -->
|
<!-- Left column: Search + Warehouse + Price List + In Stock + Item Group -->
|
||||||
<div class="selection-filters">
|
<div class="selection-filters">
|
||||||
|
<div class="filter-group selection-search-group">
|
||||||
|
<div id="selection-search-control"></div>
|
||||||
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>${__('Warehouse')}</label>
|
<label>${__('Warehouse')}</label>
|
||||||
<div id="selection-warehouse-link"></div>
|
<div id="selection-warehouse-link"></div>
|
||||||
|
|
@ -78,13 +81,6 @@ class SelectionDialog {
|
||||||
<!-- Tree will be loaded dynamically -->
|
<!-- Tree will be loaded dynamically -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group" id="custom-filters-section">
|
|
||||||
<label>${__('Additional Filters')}</label>
|
|
||||||
<div id="custom-filters-list"></div>
|
|
||||||
<button class="btn btn-xs btn-default btn-block btn-add-custom-filter" id="selection-add-custom-filter">
|
|
||||||
<i class="fa fa-plus"></i> ${__('Add Filter')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<button class="btn btn-sm btn-default btn-block" id="selection-clear-filters">
|
<button class="btn btn-sm btn-default btn-block" id="selection-clear-filters">
|
||||||
${__('Clear Filters')}
|
${__('Clear Filters')}
|
||||||
|
|
@ -94,13 +90,14 @@ class SelectionDialog {
|
||||||
|
|
||||||
<div class="resize-handle" id="resize-handle-left"></div>
|
<div class="resize-handle" id="resize-handle-left"></div>
|
||||||
|
|
||||||
<!-- Middle column: Items -->
|
<!-- Middle column: Custom filters bar + Items -->
|
||||||
<div class="selection-items-column">
|
<div class="selection-items-column">
|
||||||
<div class="selection-sort-toolbar">
|
<!-- Кастомные фильтры по полям товара — над таблицей -->
|
||||||
<i class="fa fa-search selection-search-icon"></i>
|
<div class="selection-filter-bar" id="custom-filters-section">
|
||||||
<input type="text" class="form-control selection-search-input-toolbar"
|
<div id="custom-filters-list"></div>
|
||||||
id="selection-search-input"
|
<button class="btn btn-xs btn-default btn-add-custom-filter" id="selection-add-custom-filter">
|
||||||
placeholder="${__('Search by code or name...')}">
|
<i class="fa fa-plus"></i> ${__('Add Filter')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="selection-items-grid" id="selection-items-grid">
|
<div class="selection-items-grid" id="selection-items-grid">
|
||||||
<div class="text-center text-muted" style="padding: 40px;">
|
<div class="text-center text-muted" style="padding: 40px;">
|
||||||
|
|
@ -204,10 +201,11 @@ class SelectionDialog {
|
||||||
frappeControl.style.cssText = 'width: 100% !important; max-width: none !important;';
|
frappeControl.style.cssText = 'width: 100% !important; max-width: none !important;';
|
||||||
}
|
}
|
||||||
|
|
||||||
me.setup_resizable_panels();
|
this.setup_resizable_panels();
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
this.setup_handlers();
|
this.setup_handlers();
|
||||||
|
this.setup_search_field();
|
||||||
this.setup_custom_filters();
|
this.setup_custom_filters();
|
||||||
this.setup_warehouse_field();
|
this.setup_warehouse_field();
|
||||||
this.setup_pricelist_field();
|
this.setup_pricelist_field();
|
||||||
|
|
@ -218,17 +216,7 @@ class SelectionDialog {
|
||||||
setup_handlers() {
|
setup_handlers() {
|
||||||
const me = this;
|
const me = this;
|
||||||
|
|
||||||
// Поиск с debounce
|
// Поиск — обработчик вешается в setup_search_field()
|
||||||
let search_timeout;
|
|
||||||
this.dialog.$wrapper.find('#selection-search-input').on('input', function() {
|
|
||||||
clearTimeout(search_timeout);
|
|
||||||
const search_term = $(this).val();
|
|
||||||
search_timeout = setTimeout(function() {
|
|
||||||
me.filters.search_term = search_term;
|
|
||||||
me.current_page = 0;
|
|
||||||
me.load_items();
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Фильтр "Только в наличии"
|
// Фильтр "Только в наличии"
|
||||||
this.dialog.$wrapper.find('#selection-only-in-stock').on('change', function() {
|
this.dialog.$wrapper.find('#selection-only-in-stock').on('change', function() {
|
||||||
|
|
@ -247,7 +235,7 @@ class SelectionDialog {
|
||||||
me.sort_by = 'item_name';
|
me.sort_by = 'item_name';
|
||||||
me.sort_order = 'asc';
|
me.sort_order = 'asc';
|
||||||
me.current_page = 0;
|
me.current_page = 0;
|
||||||
me.dialog.$wrapper.find('#selection-search-input').val('');
|
if (me.search_field) me.search_field.set_value('');
|
||||||
if (me.warehouse_field) me.warehouse_field.set_value('');
|
if (me.warehouse_field) me.warehouse_field.set_value('');
|
||||||
if (me.pricelist_field) me.pricelist_field.set_value('');
|
if (me.pricelist_field) me.pricelist_field.set_value('');
|
||||||
me.dialog.$wrapper.find('#selection-only-in-stock').prop('checked', false);
|
me.dialog.$wrapper.find('#selection-only-in-stock').prop('checked', false);
|
||||||
|
|
@ -307,34 +295,43 @@ class SelectionDialog {
|
||||||
|
|
||||||
setup_resizable_panels() {
|
setup_resizable_panels() {
|
||||||
const me = this;
|
const me = this;
|
||||||
const $filters = me.dialog.$wrapper.find('.selection-filters');
|
const filters_el = me.dialog.$wrapper.find('.selection-filters')[0];
|
||||||
const $cart = me.dialog.$wrapper.find('.selection-cart');
|
const cart_el = me.dialog.$wrapper.find('.selection-cart')[0];
|
||||||
|
|
||||||
function make_resizable(handle_id, $panel, direction) {
|
function make_resizable(handle_selector, panel_el, direction) {
|
||||||
me.dialog.$wrapper.find(handle_id).on('mousedown', function(e) {
|
const handle_el = me.dialog.$wrapper.find(handle_selector)[0];
|
||||||
|
if (!handle_el || !panel_el) return;
|
||||||
|
|
||||||
|
// pointerdown + setPointerCapture: все события указателя идут на handle_el
|
||||||
|
// даже когда мышь ушла за его пределы — стандартный drag API
|
||||||
|
handle_el.addEventListener('pointerdown', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const startX = e.clientX;
|
handle_el.setPointerCapture(e.pointerId);
|
||||||
const startWidth = $panel[0].getBoundingClientRect().width;
|
document.body.classList.add('selection-resizing');
|
||||||
|
|
||||||
$('body').addClass('selection-resizing');
|
let currentWidth = panel_el.getBoundingClientRect().width;
|
||||||
|
|
||||||
$(document).on('mousemove.sel-resize', function(e) {
|
function on_move(ev) {
|
||||||
const delta = direction === 'right'
|
// movementX — готовая дельта от предыдущего события, без ручного расчёта
|
||||||
? e.clientX - startX
|
const delta = direction === 'right' ? ev.movementX : -ev.movementX;
|
||||||
: startX - e.clientX;
|
currentWidth = Math.max(180, Math.min(500, currentWidth + delta));
|
||||||
const newWidth = Math.max(180, Math.min(500, startWidth + delta));
|
panel_el.style.flex = `0 0 ${currentWidth}px`;
|
||||||
$panel.css('flex', `0 0 ${newWidth}px`);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on('mouseup.sel-resize', function() {
|
function on_up() {
|
||||||
$(document).off('mousemove.sel-resize mouseup.sel-resize');
|
handle_el.removeEventListener('pointermove', on_move);
|
||||||
$('body').removeClass('selection-resizing');
|
handle_el.removeEventListener('pointerup', on_up);
|
||||||
});
|
document.body.classList.remove('selection-resizing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Слушаем на самом handle_el (после capture все события придут сюда)
|
||||||
|
handle_el.addEventListener('pointermove', on_move);
|
||||||
|
handle_el.addEventListener('pointerup', on_up);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
make_resizable('#resize-handle-left', $filters, 'right');
|
make_resizable('#resize-handle-left', filters_el, 'right');
|
||||||
make_resizable('#resize-handle-right', $cart, 'left');
|
make_resizable('#resize-handle-right', cart_el, 'left');
|
||||||
}
|
}
|
||||||
|
|
||||||
_update_stock_toggle_state() {
|
_update_stock_toggle_state() {
|
||||||
|
|
@ -406,6 +403,41 @@ class SelectionDialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setup_search_field() {
|
||||||
|
const me = this;
|
||||||
|
let search_timeout;
|
||||||
|
this.search_field = frappe.ui.form.make_control({
|
||||||
|
df: {
|
||||||
|
fieldtype: 'Data',
|
||||||
|
fieldname: 'selection_search',
|
||||||
|
placeholder: __('Search by code or name...'),
|
||||||
|
},
|
||||||
|
parent: me.dialog.$wrapper.find('#selection-search-control')[0],
|
||||||
|
render_input: true,
|
||||||
|
});
|
||||||
|
this.search_field.$input.on('input', function() {
|
||||||
|
clearTimeout(search_timeout);
|
||||||
|
const val = $(this).val();
|
||||||
|
search_timeout = setTimeout(() => {
|
||||||
|
me.filters.search_term = val;
|
||||||
|
me.current_page = 0;
|
||||||
|
me.load_items();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.search_field.$input.on('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
clearTimeout(search_timeout);
|
||||||
|
const val = $(this).val();
|
||||||
|
me.filters.search_term = val;
|
||||||
|
me.current_page = 0;
|
||||||
|
me.load_items();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setup_warehouse_field() {
|
setup_warehouse_field() {
|
||||||
const me = this;
|
const me = this;
|
||||||
this.warehouse_field = frappe.ui.form.make_control({
|
this.warehouse_field = frappe.ui.form.make_control({
|
||||||
|
|
@ -716,7 +748,11 @@ class SelectionDialog {
|
||||||
// Загружаем мету Item для списка полей
|
// Загружаем мету Item для списка полей
|
||||||
frappe.model.with_doctype('Item', function() {
|
frappe.model.with_doctype('Item', function() {
|
||||||
const meta = frappe.get_meta('Item');
|
const meta = frappe.get_meta('Item');
|
||||||
const excluded_types = ['Section Break', 'Column Break', 'HTML', 'Button', 'Tab Break'];
|
const excluded_types = [
|
||||||
|
'Section Break', 'Column Break', 'HTML', 'Button', 'Tab Break',
|
||||||
|
'Table', 'Table MultiSelect',
|
||||||
|
'Image', 'Attach', 'Attach Image'
|
||||||
|
];
|
||||||
me._item_fields = meta.fields
|
me._item_fields = meta.fields
|
||||||
.filter(f => !excluded_types.includes(f.fieldtype) && f.fieldname)
|
.filter(f => !excluded_types.includes(f.fieldtype) && f.fieldname)
|
||||||
.sort((a, b) => (a.label || a.fieldname).localeCompare(b.label || b.fieldname));
|
.sort((a, b) => (a.label || a.fieldname).localeCompare(b.label || b.fieldname));
|
||||||
|
|
@ -743,48 +779,56 @@ class SelectionDialog {
|
||||||
$list.empty();
|
$list.empty();
|
||||||
|
|
||||||
this.custom_filters.forEach(cf => {
|
this.custom_filters.forEach(cf => {
|
||||||
// Опции полей
|
|
||||||
let field_options = `<option value="">${__('Select field...')}</option>`;
|
|
||||||
me._item_fields.forEach(f => {
|
|
||||||
const selected = cf.field === f.fieldname ? 'selected' : '';
|
|
||||||
const label = f.label || f.fieldname;
|
|
||||||
field_options += `<option value="${f.fieldname}" ${selected}>${label}</option>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Определяем тип поля выбранного поля
|
|
||||||
const selected_meta = me._item_fields.find(f => f.fieldname === cf.field);
|
const selected_meta = me._item_fields.find(f => f.fieldname === cf.field);
|
||||||
const fieldtype = selected_meta ? selected_meta.fieldtype : 'Data';
|
const fieldtype = selected_meta ? selected_meta.fieldtype : 'Data';
|
||||||
|
|
||||||
// Опции операторов
|
|
||||||
const operators = me.get_operators_for_fieldtype(fieldtype);
|
|
||||||
let op_options = '';
|
|
||||||
operators.forEach(op => {
|
|
||||||
const selected = cf.operator === op.value ? 'selected' : '';
|
|
||||||
op_options += `<option value="${op.value}" ${selected}>${op.label}</option>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Инпут значения
|
|
||||||
const no_value_ops = ['is set', 'is not set'];
|
const no_value_ops = ['is set', 'is not set'];
|
||||||
const value_html = no_value_ops.includes(cf.operator)
|
|
||||||
? `<input class="form-control cf-value" disabled placeholder="" value="">`
|
|
||||||
: me.get_value_input_html(selected_meta, cf.value);
|
|
||||||
|
|
||||||
|
// Контейнеры для трёх контролов
|
||||||
const $row = $(`
|
const $row = $(`
|
||||||
<div class="custom-filter-row" data-id="${cf.id}">
|
<div class="custom-filter-row" data-id="${cf.id}">
|
||||||
<select class="cf-field form-control">${field_options}</select>
|
<div class="cf-field-wrap"></div>
|
||||||
<select class="cf-operator form-control">${op_options}</select>
|
<div class="cf-operator-wrap"></div>
|
||||||
${value_html}
|
<div class="cf-value-wrap"></div>
|
||||||
<button class="btn btn-xs btn-default cf-remove" title="${__('Remove')}">
|
<button class="btn btn-xs btn-default cf-remove" title="${__('Remove')}">
|
||||||
<i class="fa fa-times"></i>
|
<i class="fa fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
$list.append($row);
|
||||||
|
|
||||||
// Изменение поля → сбрасываем оператор и значение
|
// ── 1. Контрол выбора поля (Select через make_control) ──────────
|
||||||
$row.find('.cf-field').on('change', function() {
|
const field_options_str = [''].concat(
|
||||||
|
me._item_fields.map(f => f.label || f.fieldname)
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
// Маппинг label → fieldname для обратного поиска
|
||||||
|
const label_to_fieldname = {};
|
||||||
|
me._item_fields.forEach(f => {
|
||||||
|
label_to_fieldname[f.label || f.fieldname] = f.fieldname;
|
||||||
|
});
|
||||||
|
const fieldname_to_label = {};
|
||||||
|
me._item_fields.forEach(f => {
|
||||||
|
fieldname_to_label[f.fieldname] = f.label || f.fieldname;
|
||||||
|
});
|
||||||
|
|
||||||
|
const field_ctrl = frappe.ui.form.make_control({
|
||||||
|
df: {
|
||||||
|
fieldtype: 'Select',
|
||||||
|
fieldname: `cf_field_${cf.id}`,
|
||||||
|
options: field_options_str,
|
||||||
|
placeholder: __('Select field...'),
|
||||||
|
},
|
||||||
|
parent: $row.find('.cf-field-wrap')[0],
|
||||||
|
render_input: true,
|
||||||
|
});
|
||||||
|
field_ctrl.set_value(fieldname_to_label[cf.field] || '');
|
||||||
|
|
||||||
|
field_ctrl.$input.on('change', function() {
|
||||||
|
const label = $(this).val();
|
||||||
|
const fieldname = label_to_fieldname[label] || '';
|
||||||
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
me.custom_filters[idx].field = $(this).val();
|
me.custom_filters[idx].field = fieldname;
|
||||||
me.custom_filters[idx].operator = '=';
|
me.custom_filters[idx].operator = '=';
|
||||||
me.custom_filters[idx].value = '';
|
me.custom_filters[idx].value = '';
|
||||||
}
|
}
|
||||||
|
|
@ -793,70 +837,125 @@ class SelectionDialog {
|
||||||
me.load_items();
|
me.load_items();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Изменение оператора
|
// ── 2. Контрол оператора (Select) ───────────────────────────────
|
||||||
$row.find('.cf-operator').on('change', function() {
|
const operators = me.get_operators_for_fieldtype(fieldtype);
|
||||||
|
const op_options_str = operators.map(o => o.label).join('\n');
|
||||||
|
const op_label_to_value = {};
|
||||||
|
const op_value_to_label = {};
|
||||||
|
operators.forEach(o => {
|
||||||
|
op_label_to_value[o.label] = o.value;
|
||||||
|
op_value_to_label[o.value] = o.label;
|
||||||
|
});
|
||||||
|
|
||||||
|
const op_ctrl = frappe.ui.form.make_control({
|
||||||
|
df: {
|
||||||
|
fieldtype: 'Select',
|
||||||
|
fieldname: `cf_op_${cf.id}`,
|
||||||
|
options: op_options_str,
|
||||||
|
},
|
||||||
|
parent: $row.find('.cf-operator-wrap')[0],
|
||||||
|
render_input: true,
|
||||||
|
});
|
||||||
|
op_ctrl.set_value(op_value_to_label[cf.operator] || operators[0].label);
|
||||||
|
|
||||||
|
op_ctrl.$input.on('change', function() {
|
||||||
|
const op_value = op_label_to_value[$(this).val()] || '=';
|
||||||
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) me.custom_filters[idx].operator = op_value;
|
||||||
me.custom_filters[idx].operator = $(this).val();
|
|
||||||
}
|
|
||||||
me.render_custom_filter_rows();
|
me.render_custom_filter_rows();
|
||||||
me.current_page = 0;
|
me.current_page = 0;
|
||||||
me.load_items();
|
me.load_items();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Изменение значения (только для обычных инпутов — не Link-обёрток)
|
// ── 3. Контрол значения (make_control по типу поля) ─────────────
|
||||||
$row.find('.cf-value').on('change input', function() {
|
const $val_wrap = $row.find('.cf-value-wrap');
|
||||||
if ($(this).hasClass('cf-value-link-wrapper')) return;
|
|
||||||
|
if (no_value_ops.includes(cf.operator)) {
|
||||||
|
// Заглушка — поле отключено
|
||||||
|
const stub = frappe.ui.form.make_control({
|
||||||
|
df: { fieldtype: 'Data', fieldname: `cf_val_${cf.id}`, read_only: 1 },
|
||||||
|
parent: $val_wrap[0],
|
||||||
|
render_input: true,
|
||||||
|
});
|
||||||
|
stub.set_value('');
|
||||||
|
} else if (selected_meta) {
|
||||||
|
// Текстовые поля рендерим как Data — textarea/rich-редактор не нужны для фильтра
|
||||||
|
const text_like = ['Small Text', 'Text', 'Long Text', 'Text Editor'];
|
||||||
|
let filter_fieldtype = text_like.includes(selected_meta.fieldtype)
|
||||||
|
? 'Data'
|
||||||
|
: selected_meta.fieldtype;
|
||||||
|
let filter_options = selected_meta.options || '';
|
||||||
|
|
||||||
|
// Check рендерим как Select True/False — чтобы make_control создал
|
||||||
|
// идентичную DOM-структуру с другими контролами и не было смещения
|
||||||
|
let display_value = cf.value || '';
|
||||||
|
if (selected_meta.fieldtype === 'Check') {
|
||||||
|
filter_fieldtype = 'Select';
|
||||||
|
filter_options = '\nTrue\nFalse';
|
||||||
|
if (cf.value === '1') display_value = 'True';
|
||||||
|
else if (cf.value === '0') display_value = 'False';
|
||||||
|
else display_value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const val_df = {
|
||||||
|
fieldtype: filter_fieldtype,
|
||||||
|
fieldname: `cf_val_${cf.id}`,
|
||||||
|
options: filter_options,
|
||||||
|
onchange: function() {
|
||||||
|
let val = this.get_value();
|
||||||
|
// Обратное маппирование True/False → 1/0 для хранения
|
||||||
|
if (selected_meta.fieldtype === 'Check') {
|
||||||
|
if (val === 'True') val = '1';
|
||||||
|
else if (val === 'False') val = '0';
|
||||||
|
else val = '';
|
||||||
|
}
|
||||||
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
me.custom_filters[idx].value = $(this).val();
|
me.custom_filters[idx].value = (val != null) ? String(val) : '';
|
||||||
}
|
}
|
||||||
// debounce через timeout
|
|
||||||
clearTimeout(me._cf_value_timeout);
|
clearTimeout(me._cf_value_timeout);
|
||||||
me._cf_value_timeout = setTimeout(() => {
|
me._cf_value_timeout = setTimeout(() => {
|
||||||
me.current_page = 0;
|
me.current_page = 0;
|
||||||
me.load_items();
|
me.load_items();
|
||||||
}, 500);
|
}, 300);
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Удаление строки
|
const val_ctrl = frappe.ui.form.make_control({
|
||||||
|
df: val_df,
|
||||||
|
parent: $val_wrap[0],
|
||||||
|
render_input: true,
|
||||||
|
});
|
||||||
|
val_ctrl.set_value(display_value);
|
||||||
|
|
||||||
|
// Для Data/Text/Int/Float — дополнительный real-time слушатель
|
||||||
|
if (!['Link', 'Select', 'Check'].includes(selected_meta.fieldtype)) {
|
||||||
|
val_ctrl.$input.on('input', function() {
|
||||||
|
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
||||||
|
if (idx !== -1) me.custom_filters[idx].value = $(this).val() || '';
|
||||||
|
clearTimeout(me._cf_value_timeout);
|
||||||
|
me._cf_value_timeout = setTimeout(() => {
|
||||||
|
me.current_page = 0;
|
||||||
|
me.load_items();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Поле ещё не выбрано — пустой Data-контрол
|
||||||
|
frappe.ui.form.make_control({
|
||||||
|
df: { fieldtype: 'Data', fieldname: `cf_val_${cf.id}` },
|
||||||
|
parent: $val_wrap[0],
|
||||||
|
render_input: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Удаление строки ──────────────────────────────────────────────
|
||||||
$row.find('.cf-remove').on('click', function() {
|
$row.find('.cf-remove').on('click', function() {
|
||||||
me.custom_filters = me.custom_filters.filter(x => x.id !== cf.id);
|
me.custom_filters = me.custom_filters.filter(x => x.id !== cf.id);
|
||||||
me.render_custom_filter_rows();
|
me.render_custom_filter_rows();
|
||||||
me.current_page = 0;
|
me.current_page = 0;
|
||||||
me.load_items();
|
me.load_items();
|
||||||
});
|
});
|
||||||
|
|
||||||
$list.append($row);
|
|
||||||
|
|
||||||
// Если поле Link — создаём ControlLink в контейнере (после append в DOM)
|
|
||||||
if (selected_meta && selected_meta.fieldtype === 'Link' && !no_value_ops.includes(cf.operator)) {
|
|
||||||
const link_container = $row.find('.cf-value-link-wrapper')[0];
|
|
||||||
const link_ctrl = frappe.ui.form.make_control({
|
|
||||||
df: {
|
|
||||||
fieldtype: 'Link',
|
|
||||||
options: selected_meta.options,
|
|
||||||
fieldname: `cf_link_${cf.id}`,
|
|
||||||
only_select: false,
|
|
||||||
},
|
|
||||||
parent: link_container,
|
|
||||||
render_input: true,
|
|
||||||
});
|
|
||||||
link_ctrl.set_value(cf.value || '');
|
|
||||||
$row.data('link-ctrl', link_ctrl);
|
|
||||||
|
|
||||||
link_ctrl.$input.on('change', function() {
|
|
||||||
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
|
||||||
if (idx !== -1) {
|
|
||||||
me.custom_filters[idx].value = link_ctrl.get_value() || '';
|
|
||||||
}
|
|
||||||
clearTimeout(me._cf_value_timeout);
|
|
||||||
me._cf_value_timeout = setTimeout(() => {
|
|
||||||
me.current_page = 0;
|
|
||||||
me.load_items();
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -893,41 +992,6 @@ class SelectionDialog {
|
||||||
return text_ops;
|
return text_ops;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_value_input_html(field_meta, current_value) {
|
|
||||||
const val = current_value || '';
|
|
||||||
if (!field_meta) {
|
|
||||||
return `<input type="text" class="form-control cf-value" value="${frappe.utils.escape_html(val)}">`;
|
|
||||||
}
|
|
||||||
if (field_meta.fieldtype === 'Link') {
|
|
||||||
// Контейнер — ControlLink создаётся после append в DOM в render_custom_filter_rows
|
|
||||||
return `<div class="cf-value cf-value-link-wrapper"></div>`;
|
|
||||||
}
|
|
||||||
if (field_meta.fieldtype === 'Select') {
|
|
||||||
const options = (field_meta.options || '').split('\n');
|
|
||||||
let opts = `<option value=""></option>`;
|
|
||||||
options.forEach(opt => {
|
|
||||||
if (opt) {
|
|
||||||
const selected = val === opt ? 'selected' : '';
|
|
||||||
opts += `<option value="${frappe.utils.escape_html(opt)}" ${selected}>${frappe.utils.escape_html(opt)}</option>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return `<select class="form-control cf-value">${opts}</select>`;
|
|
||||||
}
|
|
||||||
if (field_meta.fieldtype === 'Check') {
|
|
||||||
return `
|
|
||||||
<select class="form-control cf-value">
|
|
||||||
<option value=""></option>
|
|
||||||
<option value="1" ${val == 1 ? 'selected' : ''}>${__('Yes')}</option>
|
|
||||||
<option value="0" ${val == 0 && val !== '' ? 'selected' : ''}>${__('No')}</option>
|
|
||||||
</select>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
if (field_meta.fieldtype === 'Date') {
|
|
||||||
return `<input type="date" class="form-control cf-value" value="${frappe.utils.escape_html(val)}">`;
|
|
||||||
}
|
|
||||||
return `<input type="text" class="form-control cf-value" value="${frappe.utils.escape_html(val)}">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Инфо-панель ─────────────────────────────────────────────────────────
|
// ── Инфо-панель ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
show_item_info(index) {
|
show_item_info(index) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue