minor fixes and filter added
This commit is contained in:
parent
410216992e
commit
192a449741
|
|
@ -11,7 +11,7 @@ from erpnext.stock.get_item_details import get_item_details
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_items_for_selection(filters=None, start=0, page_length=50):
|
def get_items_for_selection(filters=None, start=0, page_length=50, custom_filters=None):
|
||||||
"""
|
"""
|
||||||
Получить список товаров с остатками и ценами для формы подбора
|
Получить список товаров с остатками и ценами для формы подбора
|
||||||
|
|
||||||
|
|
@ -19,6 +19,7 @@ def get_items_for_selection(filters=None, start=0, page_length=50):
|
||||||
filters (dict): Фильтры для поиска товаров
|
filters (dict): Фильтры для поиска товаров
|
||||||
start (int): Начальная позиция для пагинации
|
start (int): Начальная позиция для пагинации
|
||||||
page_length (int): Количество товаров на странице
|
page_length (int): Количество товаров на странице
|
||||||
|
custom_filters (list): Произвольные фильтры по полям DocType Item
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: {
|
dict: {
|
||||||
|
|
@ -29,6 +30,9 @@ def get_items_for_selection(filters=None, start=0, page_length=50):
|
||||||
if isinstance(filters, str):
|
if isinstance(filters, str):
|
||||||
filters = frappe.parse_json(filters)
|
filters = frappe.parse_json(filters)
|
||||||
|
|
||||||
|
if isinstance(custom_filters, str):
|
||||||
|
custom_filters = frappe.parse_json(custom_filters)
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
|
|
@ -102,6 +106,10 @@ def get_items_for_selection(filters=None, start=0, page_length=50):
|
||||||
if only_in_stock:
|
if only_in_stock:
|
||||||
query = query.where(avail_expr > 0)
|
query = query.where(avail_expr > 0)
|
||||||
|
|
||||||
|
# Применяем произвольные фильтры
|
||||||
|
if custom_filters:
|
||||||
|
query = _apply_custom_filters(query, Item, custom_filters)
|
||||||
|
|
||||||
# Подсчет общего количества
|
# Подсчет общего количества
|
||||||
count_query = query.select(Count('*').as_('count'))
|
count_query = query.select(Count('*').as_('count'))
|
||||||
count_result = count_query.run(as_dict=True)
|
count_result = count_query.run(as_dict=True)
|
||||||
|
|
@ -183,6 +191,55 @@ def get_items_for_selection(filters=None, start=0, page_length=50):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_custom_filters(query, Item, custom_filters):
|
||||||
|
"""
|
||||||
|
Применить произвольные фильтры по полям DocType Item
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Query Builder запрос
|
||||||
|
Item: DocType Item
|
||||||
|
custom_filters (list): Список фильтров [{field, operator, value}]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
query с применёнными фильтрами
|
||||||
|
"""
|
||||||
|
item_meta = frappe.get_meta('Item')
|
||||||
|
valid_fields = {f.fieldname for f in item_meta.fields}
|
||||||
|
|
||||||
|
for cf in custom_filters:
|
||||||
|
field = cstr(cf.get('field', '')).strip()
|
||||||
|
operator = cstr(cf.get('operator', '=')).strip()
|
||||||
|
value = cf.get('value', '')
|
||||||
|
|
||||||
|
if not field or field not in valid_fields:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item_field = Item[field]
|
||||||
|
|
||||||
|
if operator == '=':
|
||||||
|
query = query.where(item_field == value)
|
||||||
|
elif operator == '!=':
|
||||||
|
query = query.where(item_field != value)
|
||||||
|
elif operator == 'like':
|
||||||
|
query = query.where(item_field.like(f'%{value}%'))
|
||||||
|
elif operator == 'not like':
|
||||||
|
query = query.where(~item_field.like(f'%{value}%'))
|
||||||
|
elif operator == '>':
|
||||||
|
query = query.where(item_field > flt(value))
|
||||||
|
elif operator == '<':
|
||||||
|
query = query.where(item_field < flt(value))
|
||||||
|
elif operator == '>=':
|
||||||
|
query = query.where(item_field >= flt(value))
|
||||||
|
elif operator == '<=':
|
||||||
|
query = query.where(item_field <= flt(value))
|
||||||
|
elif operator == 'is set':
|
||||||
|
query = query.where(item_field.isnotnull() & (item_field != ''))
|
||||||
|
elif operator == 'is not set':
|
||||||
|
query = query.where(item_field.isnull() | (item_field == ''))
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_across_warehouses(item_code):
|
def get_stock_across_warehouses(item_code):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
/* Левая колонка: Фильтры */
|
/* Левая колонка: Фильтры */
|
||||||
.selection-filters {
|
.selection-filters {
|
||||||
flex: 0 0 250px;
|
flex: 0 0 300px;
|
||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Панель сортировки */
|
/* Тулбар поиска (заменяет панель сортировки) */
|
||||||
.selection-sort-toolbar {
|
.selection-sort-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -152,42 +152,42 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-label {
|
.selection-search-icon {
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
white-space: nowrap;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-buttons {
|
.selection-search-input-toolbar {
|
||||||
display: flex;
|
flex: 1;
|
||||||
gap: 4px;
|
font-size: 13px;
|
||||||
flex-wrap: wrap;
|
height: 30px;
|
||||||
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-btn {
|
/* Ручки resize для панелей */
|
||||||
font-size: 11px;
|
.resize-handle {
|
||||||
padding: 2px 8px;
|
flex: 0 0 6px;
|
||||||
border: 1px solid var(--border-color);
|
cursor: col-resize;
|
||||||
background-color: var(--bg-color);
|
background: var(--border-color);
|
||||||
color: var(--text-color);
|
border-radius: 3px;
|
||||||
border-radius: var(--border-radius);
|
transition: background 0.15s;
|
||||||
cursor: pointer;
|
align-self: stretch;
|
||||||
transition: all 0.15s;
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-btn:hover {
|
.resize-handle:hover,
|
||||||
background-color: var(--bg-light-gray);
|
.selection-resizing .resize-handle {
|
||||||
border-color: var(--primary-color);
|
background: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-btn.active {
|
/* Блокировать выделение текста во время drag */
|
||||||
background-color: var(--primary-color);
|
body.selection-resizing {
|
||||||
color: white;
|
user-select: none;
|
||||||
border-color: var(--primary-color);
|
cursor: col-resize !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-btn i {
|
body.selection-resizing * {
|
||||||
margin-left: 3px;
|
cursor: col-resize !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selection-items-grid {
|
.selection-items-grid {
|
||||||
|
|
@ -545,10 +545,199 @@
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Произвольные фильтры ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.custom-filter-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-field {
|
||||||
|
flex: 1.5;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
height: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-operator {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
height: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-value {
|
||||||
|
flex: 1.5;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
height: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-remove {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-custom-filter {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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-pricelist-link .frappe-control {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#selection-warehouse-link .form-group,
|
||||||
|
#selection-pricelist-link .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Инфо-панель ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.selection-info-panel {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background-color: var(--bg-light-gray);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-header-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-header-text strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-header-text small {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-body {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-image {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-image img {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-no-image {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-light-gray);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-description {
|
||||||
|
font-size: 13px;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-stock-table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-stock-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-stock-table td {
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-stock-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Адаптация для разных размеров экрана */
|
/* Адаптация для разных размеров экрана */
|
||||||
@media (max-width: 1400px) {
|
@media (max-width: 1400px) {
|
||||||
.selection-filters {
|
.selection-filters {
|
||||||
flex: 0 0 210px;
|
flex: 0 0 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selection-cart {
|
.selection-cart {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ class SelectionDialog {
|
||||||
this.sort_by = 'item_name';
|
this.sort_by = 'item_name';
|
||||||
this.sort_order = 'asc';
|
this.sort_order = 'asc';
|
||||||
this.focused_row_index = -1;
|
this.focused_row_index = -1;
|
||||||
|
this.custom_filters = []; // [{id, field, operator, value}]
|
||||||
|
this._item_fields = []; // кэш полей Item из meta
|
||||||
|
this.info_panel_item_code = null; // текущий товар в инфо-панели
|
||||||
|
this.warehouse_field = null;
|
||||||
|
this.pricelist_field = null;
|
||||||
|
|
||||||
this.make_dialog();
|
this.make_dialog();
|
||||||
}
|
}
|
||||||
|
|
@ -48,23 +53,13 @@ class SelectionDialog {
|
||||||
<div class="selection-dialog-content">
|
<div class="selection-dialog-content">
|
||||||
<!-- Left column: Filters -->
|
<!-- Left column: Filters -->
|
||||||
<div class="selection-filters">
|
<div class="selection-filters">
|
||||||
<div class="filter-group">
|
|
||||||
<label>${__('Search')}</label>
|
|
||||||
<input type="text" class="form-control" id="selection-search-input" placeholder="${__('Item code or name...')}">
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>${__('Warehouse')}</label>
|
<label>${__('Warehouse')}</label>
|
||||||
<select class="form-control" id="selection-warehouse-select">
|
<div id="selection-warehouse-link"></div>
|
||||||
<option value="" disabled>${__('Select Warehouse...')}</option>
|
|
||||||
</select>
|
|
||||||
<small class="text-muted">${__('For stock levels')}</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>${__('Price List')}</label>
|
<label>${__('Price List')}</label>
|
||||||
<select class="form-control" id="selection-pricelist-select">
|
<div id="selection-pricelist-link"></div>
|
||||||
<option value="" disabled>${__('Select Price List...')}</option>
|
|
||||||
</select>
|
|
||||||
<small class="text-muted">${__('For item prices')}</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<div class="selection-stock-toggle">
|
<div class="selection-stock-toggle">
|
||||||
|
|
@ -83,6 +78,13 @@ 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')}
|
||||||
|
|
@ -90,21 +92,15 @@ class SelectionDialog {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="resize-handle" id="resize-handle-left"></div>
|
||||||
|
|
||||||
<!-- Middle column: Items -->
|
<!-- Middle column: Items -->
|
||||||
<div class="selection-items-column">
|
<div class="selection-items-column">
|
||||||
<div class="selection-sort-toolbar">
|
<div class="selection-sort-toolbar">
|
||||||
<span class="sort-label">${__('Sort by')}:</span>
|
<i class="fa fa-search selection-search-icon"></i>
|
||||||
<div class="sort-buttons">
|
<input type="text" class="form-control selection-search-input-toolbar"
|
||||||
<button class="btn btn-xs sort-btn active" data-sort="item_name" data-order="asc">
|
id="selection-search-input"
|
||||||
${__('Name')} <i class="fa fa-sort-alpha-asc"></i>
|
placeholder="${__('Search by code or name...')}">
|
||||||
</button>
|
|
||||||
<button class="btn btn-xs sort-btn" data-sort="available_qty" data-order="asc">
|
|
||||||
${__('Stock')} <i class="fa fa-sort"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-xs sort-btn" data-sort="rate" data-order="asc">
|
|
||||||
${__('Price')} <i class="fa fa-sort"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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;">
|
||||||
|
|
@ -120,7 +116,32 @@ class SelectionDialog {
|
||||||
${__('Next')} <i class="fa fa-chevron-right"></i>
|
${__('Next')} <i class="fa fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="selection-info-panel" id="selection-info-panel" style="display:none;">
|
||||||
|
<div class="info-panel-header">
|
||||||
|
<div class="info-panel-header-text">
|
||||||
|
<strong id="info-panel-title"></strong>
|
||||||
|
<small class="text-muted" id="info-panel-subtitle"></small>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-xs btn-default info-panel-close" id="info-panel-close">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="info-panel-body">
|
||||||
|
<div class="info-panel-image" id="info-panel-image-wrap">
|
||||||
|
<img id="info-panel-img" src="" alt="" style="display:none;">
|
||||||
|
<div class="info-panel-no-image" id="info-panel-no-img">
|
||||||
|
<i class="fa fa-cube"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-panel-content">
|
||||||
|
<div class="info-panel-description" id="info-panel-description"></div>
|
||||||
|
<div class="info-panel-stock" id="info-panel-stock"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resize-handle" id="resize-handle-right"></div>
|
||||||
|
|
||||||
<!-- Right column: Cart -->
|
<!-- Right column: Cart -->
|
||||||
<div class="selection-cart">
|
<div class="selection-cart">
|
||||||
|
|
@ -182,11 +203,14 @@ class SelectionDialog {
|
||||||
if (frappeControl) {
|
if (frappeControl) {
|
||||||
frappeControl.style.cssText = 'width: 100% !important; max-width: none !important;';
|
frappeControl.style.cssText = 'width: 100% !important; max-width: none !important;';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
me.setup_resizable_panels();
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
this.setup_handlers();
|
this.setup_handlers();
|
||||||
this.load_warehouses();
|
this.setup_custom_filters();
|
||||||
this.load_price_lists();
|
this.setup_warehouse_field();
|
||||||
|
this.setup_pricelist_field();
|
||||||
this.load_item_groups();
|
this.load_item_groups();
|
||||||
this.load_items();
|
this.load_items();
|
||||||
}
|
}
|
||||||
|
|
@ -206,21 +230,6 @@ class SelectionDialog {
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Выбор склада
|
|
||||||
this.dialog.$wrapper.find('#selection-warehouse-select').on('change', function() {
|
|
||||||
me.filters.warehouse = $(this).val();
|
|
||||||
me.current_page = 0;
|
|
||||||
me._update_stock_toggle_state();
|
|
||||||
me.load_items();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Выбор прайс-листа
|
|
||||||
this.dialog.$wrapper.find('#selection-pricelist-select').on('change', function() {
|
|
||||||
me.filters.price_list = $(this).val();
|
|
||||||
me.current_page = 0;
|
|
||||||
me.load_items();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Фильтр "Только в наличии"
|
// Фильтр "Только в наличии"
|
||||||
this.dialog.$wrapper.find('#selection-only-in-stock').on('change', function() {
|
this.dialog.$wrapper.find('#selection-only-in-stock').on('change', function() {
|
||||||
if (!me.filters.warehouse) {
|
if (!me.filters.warehouse) {
|
||||||
|
|
@ -232,34 +241,6 @@ class SelectionDialog {
|
||||||
me.load_items();
|
me.load_items();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Кнопки сортировки
|
|
||||||
this.dialog.$wrapper.find('.sort-btn').on('click', function() {
|
|
||||||
const $btn = $(this);
|
|
||||||
const sort_by = $btn.data('sort');
|
|
||||||
const current_order = $btn.data('order');
|
|
||||||
|
|
||||||
if (me.sort_by === sort_by) {
|
|
||||||
// Переключаем направление
|
|
||||||
me.sort_order = current_order === 'asc' ? 'desc' : 'asc';
|
|
||||||
$btn.data('order', me.sort_order);
|
|
||||||
} else {
|
|
||||||
me.sort_by = sort_by;
|
|
||||||
me.sort_order = 'asc';
|
|
||||||
$btn.data('order', 'asc');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем иконки
|
|
||||||
me.dialog.$wrapper.find('.sort-btn').removeClass('active').each(function() {
|
|
||||||
$(this).find('i').attr('class', 'fa fa-sort');
|
|
||||||
});
|
|
||||||
$btn.addClass('active');
|
|
||||||
const icon_class = me.sort_order === 'asc' ? `fa-sort-${sort_by === 'item_name' ? 'alpha' : 'amount'}-asc` : `fa-sort-${sort_by === 'item_name' ? 'alpha' : 'amount'}-desc`;
|
|
||||||
$btn.find('i').attr('class', `fa ${icon_class}`);
|
|
||||||
|
|
||||||
me.current_page = 0;
|
|
||||||
me.load_items();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Очистить фильтры
|
// Очистить фильтры
|
||||||
this.dialog.$wrapper.find('#selection-clear-filters').on('click', function() {
|
this.dialog.$wrapper.find('#selection-clear-filters').on('click', function() {
|
||||||
me.filters = {};
|
me.filters = {};
|
||||||
|
|
@ -267,12 +248,17 @@ class SelectionDialog {
|
||||||
me.sort_order = 'asc';
|
me.sort_order = 'asc';
|
||||||
me.current_page = 0;
|
me.current_page = 0;
|
||||||
me.dialog.$wrapper.find('#selection-search-input').val('');
|
me.dialog.$wrapper.find('#selection-search-input').val('');
|
||||||
me.dialog.$wrapper.find('#selection-warehouse-select').val('');
|
if (me.warehouse_field) me.warehouse_field.set_value('');
|
||||||
me.dialog.$wrapper.find('#selection-pricelist-select').val('');
|
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);
|
||||||
me.dialog.$wrapper.find('.item-group-tree .active').removeClass('active');
|
me.dialog.$wrapper.find('.item-group-tree .active').removeClass('active');
|
||||||
me.dialog.$wrapper.find('.sort-btn').removeClass('active').eq(0).addClass('active');
|
|
||||||
me._update_stock_toggle_state();
|
me._update_stock_toggle_state();
|
||||||
|
// Очищаем произвольные фильтры
|
||||||
|
me.custom_filters = [];
|
||||||
|
me.dialog.$wrapper.find('#custom-filters-list').empty();
|
||||||
|
// Скрываем инфо-панель
|
||||||
|
me.dialog.$wrapper.find('#selection-info-panel').hide();
|
||||||
|
me.info_panel_item_code = null;
|
||||||
me.load_items();
|
me.load_items();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -311,6 +297,44 @@ class SelectionDialog {
|
||||||
me.dialog.$wrapper.find('.stock-warehouses-tooltip').hide();
|
me.dialog.$wrapper.find('.stock-warehouses-tooltip').hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Закрыть инфо-панель
|
||||||
|
this.dialog.$wrapper.on('click', '#info-panel-close', function() {
|
||||||
|
me.dialog.$wrapper.find('#selection-info-panel').hide();
|
||||||
|
me.info_panel_item_code = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_resizable_panels() {
|
||||||
|
const me = this;
|
||||||
|
const $filters = me.dialog.$wrapper.find('.selection-filters');
|
||||||
|
const $cart = me.dialog.$wrapper.find('.selection-cart');
|
||||||
|
|
||||||
|
function make_resizable(handle_id, $panel, direction) {
|
||||||
|
me.dialog.$wrapper.find(handle_id).on('mousedown', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startWidth = $panel[0].getBoundingClientRect().width;
|
||||||
|
|
||||||
|
$('body').addClass('selection-resizing');
|
||||||
|
|
||||||
|
$(document).on('mousemove.sel-resize', function(e) {
|
||||||
|
const delta = direction === 'right'
|
||||||
|
? e.clientX - startX
|
||||||
|
: startX - e.clientX;
|
||||||
|
const newWidth = Math.max(180, Math.min(500, startWidth + delta));
|
||||||
|
$panel.css('flex', `0 0 ${newWidth}px`);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('mouseup.sel-resize', function() {
|
||||||
|
$(document).off('mousemove.sel-resize mouseup.sel-resize');
|
||||||
|
$('body').removeClass('selection-resizing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
make_resizable('#resize-handle-left', $filters, 'right');
|
||||||
|
make_resizable('#resize-handle-right', $cart, 'left');
|
||||||
}
|
}
|
||||||
|
|
||||||
_update_stock_toggle_state() {
|
_update_stock_toggle_state() {
|
||||||
|
|
@ -382,78 +406,57 @@ class SelectionDialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load_warehouses() {
|
setup_warehouse_field() {
|
||||||
const me = this;
|
const me = this;
|
||||||
|
this.warehouse_field = frappe.ui.form.make_control({
|
||||||
frappe.call({
|
df: {
|
||||||
method: 'frappe.client.get_list',
|
fieldtype: 'Link',
|
||||||
args: {
|
options: 'Warehouse',
|
||||||
doctype: 'Warehouse',
|
placeholder: __('Select Warehouse...'),
|
||||||
fields: ['name'],
|
filters: { is_group: 0, disabled: 0 },
|
||||||
filters: {
|
onchange: function() {
|
||||||
'is_group': 0,
|
me.filters.warehouse = this.value || '';
|
||||||
'disabled': 0
|
me.current_page = 0;
|
||||||
|
me._update_stock_toggle_state();
|
||||||
|
me.load_items();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
order_by: 'name asc',
|
parent: me.dialog.$wrapper.find('#selection-warehouse-link')[0],
|
||||||
limit_page_length: 0
|
render_input: true,
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
const $select = me.dialog.$wrapper.find('#selection-warehouse-select');
|
|
||||||
|
|
||||||
r.message.forEach(function(warehouse) {
|
|
||||||
$select.append($('<option>', {
|
|
||||||
value: warehouse.name,
|
|
||||||
text: warehouse.name
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const default_warehouse = me.frm.doc.set_warehouse || '';
|
const default_warehouse = me.frm.doc.set_warehouse || '';
|
||||||
if (default_warehouse) {
|
if (default_warehouse) {
|
||||||
$select.val(default_warehouse);
|
me.warehouse_field.set_value(default_warehouse);
|
||||||
me.filters.warehouse = default_warehouse;
|
me.filters.warehouse = default_warehouse;
|
||||||
me._update_stock_toggle_state();
|
me._update_stock_toggle_state();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
load_price_lists() {
|
setup_pricelist_field() {
|
||||||
const me = this;
|
const me = this;
|
||||||
|
this.pricelist_field = frappe.ui.form.make_control({
|
||||||
frappe.call({
|
df: {
|
||||||
method: 'frappe.client.get_list',
|
fieldtype: 'Link',
|
||||||
args: {
|
options: 'Price List',
|
||||||
doctype: 'Price List',
|
placeholder: __('Select Price List...'),
|
||||||
fields: ['name'],
|
filters: { selling: 1, enabled: 1 },
|
||||||
filters: {
|
onchange: function() {
|
||||||
'selling': 1,
|
me.filters.price_list = this.value || '';
|
||||||
'enabled': 1
|
me.current_page = 0;
|
||||||
|
me.load_items();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
order_by: 'name asc',
|
parent: me.dialog.$wrapper.find('#selection-pricelist-link')[0],
|
||||||
limit_page_length: 0
|
render_input: true,
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
const $select = me.dialog.$wrapper.find('#selection-pricelist-select');
|
|
||||||
|
|
||||||
r.message.forEach(function(price_list) {
|
|
||||||
$select.append($('<option>', {
|
|
||||||
value: price_list.name,
|
|
||||||
text: price_list.name
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const default_price_list = me.frm.doc.selling_price_list || '';
|
const default_price_list = me.frm.doc.selling_price_list || '';
|
||||||
if (default_price_list) {
|
if (default_price_list) {
|
||||||
$select.val(default_price_list);
|
me.pricelist_field.set_value(default_price_list);
|
||||||
me.filters.price_list = default_price_list;
|
me.filters.price_list = default_price_list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
load_item_groups() {
|
load_item_groups() {
|
||||||
const me = this;
|
const me = this;
|
||||||
|
|
@ -538,7 +541,10 @@ class SelectionDialog {
|
||||||
args: {
|
args: {
|
||||||
filters: filters,
|
filters: filters,
|
||||||
start: start,
|
start: start,
|
||||||
page_length: this.page_length
|
page_length: this.page_length,
|
||||||
|
custom_filters: this.custom_filters.filter(cf =>
|
||||||
|
cf.field && (cf.operator === 'is set' || cf.operator === 'is not set' || cf.value !== '')
|
||||||
|
)
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
|
|
@ -673,11 +679,12 @@ class SelectionDialog {
|
||||||
}, 600);
|
}, 600);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Клик по строке для навигации
|
// Клик по строке для навигации и инфо-панели
|
||||||
$grid.find('tbody tr').on('click', function(e) {
|
$grid.find('tbody tr').on('click', function(e) {
|
||||||
if (!$(e.target).closest('.btn-add-to-cart, .row-qty-input, .stock-indicator').length) {
|
if (!$(e.target).closest('.btn-add-to-cart, .row-qty-input, .stock-indicator').length) {
|
||||||
me.focused_row_index = parseInt($(this).data('item-index'));
|
me.focused_row_index = parseInt($(this).data('item-index'));
|
||||||
me._update_focused_row($grid.find('tbody tr'));
|
me._update_focused_row($grid.find('tbody tr'));
|
||||||
|
me.show_item_info(me.focused_row_index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -702,6 +709,296 @@ class SelectionDialog {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Произвольные фильтры ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
setup_custom_filters() {
|
||||||
|
const me = this;
|
||||||
|
// Загружаем мету Item для списка полей
|
||||||
|
frappe.model.with_doctype('Item', function() {
|
||||||
|
const meta = frappe.get_meta('Item');
|
||||||
|
const excluded_types = ['Section Break', 'Column Break', 'HTML', 'Button', 'Tab Break'];
|
||||||
|
me._item_fields = meta.fields
|
||||||
|
.filter(f => !excluded_types.includes(f.fieldtype) && f.fieldname)
|
||||||
|
.sort((a, b) => (a.label || a.fieldname).localeCompare(b.label || b.fieldname));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dialog.$wrapper.find('#selection-add-custom-filter').on('click', function() {
|
||||||
|
me.add_custom_filter_row();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add_custom_filter_row() {
|
||||||
|
this.custom_filters.push({
|
||||||
|
id: Date.now(),
|
||||||
|
field: '',
|
||||||
|
operator: '=',
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
this.render_custom_filter_rows();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_custom_filter_rows() {
|
||||||
|
const me = this;
|
||||||
|
const $list = this.dialog.$wrapper.find('#custom-filters-list');
|
||||||
|
$list.empty();
|
||||||
|
|
||||||
|
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 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 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 = $(`
|
||||||
|
<div class="custom-filter-row" data-id="${cf.id}">
|
||||||
|
<select class="cf-field form-control">${field_options}</select>
|
||||||
|
<select class="cf-operator form-control">${op_options}</select>
|
||||||
|
${value_html}
|
||||||
|
<button class="btn btn-xs btn-default cf-remove" title="${__('Remove')}">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Изменение поля → сбрасываем оператор и значение
|
||||||
|
$row.find('.cf-field').on('change', function() {
|
||||||
|
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
me.custom_filters[idx].field = $(this).val();
|
||||||
|
me.custom_filters[idx].operator = '=';
|
||||||
|
me.custom_filters[idx].value = '';
|
||||||
|
}
|
||||||
|
me.render_custom_filter_rows();
|
||||||
|
me.current_page = 0;
|
||||||
|
me.load_items();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменение оператора
|
||||||
|
$row.find('.cf-operator').on('change', function() {
|
||||||
|
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
me.custom_filters[idx].operator = $(this).val();
|
||||||
|
}
|
||||||
|
me.render_custom_filter_rows();
|
||||||
|
me.current_page = 0;
|
||||||
|
me.load_items();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменение значения (только для обычных инпутов — не Link-обёрток)
|
||||||
|
$row.find('.cf-value').on('change input', function() {
|
||||||
|
if ($(this).hasClass('cf-value-link-wrapper')) return;
|
||||||
|
const idx = me.custom_filters.findIndex(x => x.id === cf.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
me.custom_filters[idx].value = $(this).val();
|
||||||
|
}
|
||||||
|
// debounce через timeout
|
||||||
|
clearTimeout(me._cf_value_timeout);
|
||||||
|
me._cf_value_timeout = setTimeout(() => {
|
||||||
|
me.current_page = 0;
|
||||||
|
me.load_items();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаление строки
|
||||||
|
$row.find('.cf-remove').on('click', function() {
|
||||||
|
me.custom_filters = me.custom_filters.filter(x => x.id !== cf.id);
|
||||||
|
me.render_custom_filter_rows();
|
||||||
|
me.current_page = 0;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get_operators_for_fieldtype(fieldtype) {
|
||||||
|
const text_ops = [
|
||||||
|
{ value: '=', label: '=' },
|
||||||
|
{ value: '!=', label: '≠' },
|
||||||
|
{ value: 'like', label: __('contains') },
|
||||||
|
{ value: 'not like', label: __('not contains') },
|
||||||
|
{ value: 'is set', label: __('is set') },
|
||||||
|
{ value: 'is not set', label: __('not set') }
|
||||||
|
];
|
||||||
|
const num_ops = [
|
||||||
|
{ value: '=', label: '=' },
|
||||||
|
{ value: '!=', label: '≠' },
|
||||||
|
{ value: '>', label: '>' },
|
||||||
|
{ value: '<', label: '<' },
|
||||||
|
{ value: '>=', label: '≥' },
|
||||||
|
{ value: '<=', label: '≤' },
|
||||||
|
{ value: 'is set', label: __('is set') },
|
||||||
|
{ value: 'is not set', label: __('not set') }
|
||||||
|
];
|
||||||
|
const eq_ops = [
|
||||||
|
{ value: '=', label: '=' },
|
||||||
|
{ value: '!=', label: '≠' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const num_types = ['Int', 'Float', 'Currency', 'Percent'];
|
||||||
|
const eq_types = ['Select', 'Check'];
|
||||||
|
const date_types = ['Date', 'Datetime'];
|
||||||
|
|
||||||
|
if (num_types.includes(fieldtype) || date_types.includes(fieldtype)) return num_ops;
|
||||||
|
if (eq_types.includes(fieldtype)) return eq_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) {
|
||||||
|
const item = this.items_data[index];
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const $panel = this.dialog.$wrapper.find('#selection-info-panel');
|
||||||
|
|
||||||
|
// Заголовок
|
||||||
|
$panel.find('#info-panel-title').text(item.item_name);
|
||||||
|
$panel.find('#info-panel-subtitle').text(
|
||||||
|
item.item_code + (item.item_group ? ' · ' + item.item_group : '')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Изображение
|
||||||
|
if (item.image) {
|
||||||
|
$panel.find('#info-panel-img').attr('src', item.image).show();
|
||||||
|
$panel.find('#info-panel-no-img').hide();
|
||||||
|
} else {
|
||||||
|
$panel.find('#info-panel-img').hide();
|
||||||
|
$panel.find('#info-panel-no-img').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Описание
|
||||||
|
$panel.find('#info-panel-description').html(
|
||||||
|
item.description || `<span class="text-muted">${__('No description')}</span>`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Остатки по складам — загружаем если сменился товар
|
||||||
|
if (this.info_panel_item_code !== item.item_code) {
|
||||||
|
this.info_panel_item_code = item.item_code;
|
||||||
|
$panel.find('#info-panel-stock').html(`<i class="fa fa-spinner fa-spin"></i>`);
|
||||||
|
|
||||||
|
const me = this;
|
||||||
|
frappe.call({
|
||||||
|
method: 'selections.api.get_stock_across_warehouses',
|
||||||
|
args: { item_code: item.item_code },
|
||||||
|
callback: function(r) {
|
||||||
|
me._render_info_panel_stock(r.message || []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
_render_info_panel_stock(stock_rows) {
|
||||||
|
const me = this;
|
||||||
|
const $stock = this.dialog.$wrapper.find('#info-panel-stock');
|
||||||
|
|
||||||
|
if (!stock_rows || stock_rows.length === 0) {
|
||||||
|
$stock.html(`<span class="text-muted" style="font-size:11px;">${__('No stock data')}</span>`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `<table class="info-panel-stock-table">
|
||||||
|
<thead><tr>
|
||||||
|
<th>${__('Warehouse')}</th>
|
||||||
|
<th style="text-align:right;">${__('Available')}</th>
|
||||||
|
</tr></thead><tbody>`;
|
||||||
|
stock_rows.forEach(row => {
|
||||||
|
const cls = me.get_stock_indicator_class(row.available_qty);
|
||||||
|
html += `<tr>
|
||||||
|
<td>${row.warehouse}</td>
|
||||||
|
<td style="text-align:right;" class="${cls}">${me.format_qty(row.available_qty)}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
$stock.html(html);
|
||||||
|
}
|
||||||
|
|
||||||
_sync_sort_buttons() {
|
_sync_sort_buttons() {
|
||||||
const me = this;
|
const me = this;
|
||||||
this.dialog.$wrapper.find('.sort-btn').each(function() {
|
this.dialog.$wrapper.find('.sort-btn').each(function() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue