feat: extend SelectionDialog to 11 additional DocTypes

- Add child_doctype, items_field, has_rate, warehouse_field_name,
  price_list_type, price_list_field constructor options to SelectionDialog
- No-rate mode hides price column, price list filter, discount and total
  in cart (Material Request, Stock Entry, Pick List, Request for Quotation)
- Stock Entry uses s_warehouse field; Pick List uses locations child table
- Buying documents (Purchase Order/Invoice, Supplier Quotation, Purchase
  Receipt) now use buying price list filter and buying_price_list field
- api.py: add skip_price param to skip price enrichment for no-rate docs
- hooks.py: register 11 new doctype JS integrations

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Ali 2026-02-19 15:23:32 +04:00
parent f7e1657f1a
commit ed8c31e4ad
16 changed files with 479 additions and 25 deletions

View File

@ -11,7 +11,7 @@ from erpnext.stock.get_item_details import get_item_details
@frappe.whitelist()
def get_items_for_selection(filters=None, start=0, page_length=50, custom_filters=None):
def get_items_for_selection(filters=None, start=0, page_length=50, custom_filters=None, skip_price=0):
"""
Получить список товаров с остатками и ценами для формы подбора
@ -33,6 +33,8 @@ def get_items_for_selection(filters=None, start=0, page_length=50, custom_filter
if isinstance(custom_filters, str):
custom_filters = frappe.parse_json(custom_filters)
skip_price = flt(skip_price)
if not filters:
filters = {}
@ -166,7 +168,7 @@ def get_items_for_selection(filters=None, start=0, page_length=50, custom_filter
})
# Цены
if price_list and company:
if not skip_price and price_list and company:
price_data = get_price_data(
item.item_code,
warehouse=warehouse,
@ -180,7 +182,7 @@ def get_items_for_selection(filters=None, start=0, page_length=50, custom_filter
item_dict.update({
'price_list_rate': 0.0,
'rate': 0.0,
'currency': frappe.defaults.get_global_default('currency') or 'USD'
'currency': '' if skip_price else (frappe.defaults.get_global_default('currency') or 'USD')
})
result_items.append(item_dict)

View File

@ -44,7 +44,18 @@ app_include_js = "/assets/selections/js/selection_dialog.js"
# include js in doctype views
doctype_js = {
"Sales Invoice": "public/js/sales_invoice.js"
"Sales Invoice": "public/js/sales_invoice.js",
"Sales Order": "public/js/sales_order.js",
"Quotation": "public/js/quotation.js",
"Purchase Order": "public/js/purchase_order.js",
"Purchase Invoice": "public/js/purchase_invoice.js",
"Supplier Quotation": "public/js/supplier_quotation.js",
"Request for Quotation": "public/js/request_for_quotation.js",
"Delivery Note": "public/js/delivery_note.js",
"Purchase Receipt": "public/js/purchase_receipt.js",
"Material Request": "public/js/material_request.js",
"Stock Entry": "public/js/stock_entry.js",
"Pick List": "public/js/pick_list.js",
}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}

View File

@ -853,3 +853,11 @@ body.selection-resizing * {
.selection-filters::-webkit-scrollbar-thumb:hover {
background: var(--text-color);
}
/* No-rate mode: hide price-related elements */
.selection-no-rate #selection-pricelist-link,
.selection-no-rate label[for="selection-pricelist-link"],
.selection-no-rate .cart-item-discount,
.selection-no-rate .cart-total,
.selection-no-rate .cart-item-price,
.selection-no-rate .line-through { display: none !important; }

View File

@ -0,0 +1,41 @@
/**
* Интеграция формы подбора с Delivery Note
*/
frappe.ui.form.on('Delivery Note', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
if (!frm.doc.customer) {
frappe.msgprint({
title: __('Warning'),
message: __('Please select Customer first'),
indicator: 'orange'
});
return;
}
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Delivery Note',
child_doctype: 'Delivery Note Item',
has_rate: true
});
selection_dialog.show();
});
}

View File

@ -0,0 +1,32 @@
/**
* Интеграция формы подбора с Material Request
*/
frappe.ui.form.on('Material Request', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Material Request',
child_doctype: 'Material Request Item',
has_rate: false
});
selection_dialog.show();
});
}

View File

@ -0,0 +1,33 @@
/**
* Интеграция формы подбора с Pick List
*/
frappe.ui.form.on('Pick List', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.locations.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Pick List',
child_doctype: 'Pick List Item',
items_field: 'locations',
has_rate: false
});
selection_dialog.show();
});
}

View File

@ -0,0 +1,42 @@
/**
* Интеграция формы подбора с Purchase Invoice
*/
frappe.ui.form.on('Purchase Invoice', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
if (!frm.doc.supplier) {
frappe.msgprint({
title: __('Warning'),
message: __('Please select Supplier first'),
indicator: 'orange'
});
return;
}
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Purchase Invoice',
child_doctype: 'Purchase Invoice Item',
has_rate: true,
price_list_type: 'buying'
});
selection_dialog.show();
});
}

View File

@ -0,0 +1,42 @@
/**
* Интеграция формы подбора с Purchase Order
*/
frappe.ui.form.on('Purchase Order', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
if (!frm.doc.supplier) {
frappe.msgprint({
title: __('Warning'),
message: __('Please select Supplier first'),
indicator: 'orange'
});
return;
}
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Purchase Order',
child_doctype: 'Purchase Order Item',
has_rate: true,
price_list_type: 'buying'
});
selection_dialog.show();
});
}

View File

@ -0,0 +1,42 @@
/**
* Интеграция формы подбора с Purchase Receipt
*/
frappe.ui.form.on('Purchase Receipt', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
if (!frm.doc.supplier) {
frappe.msgprint({
title: __('Warning'),
message: __('Please select Supplier first'),
indicator: 'orange'
});
return;
}
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Purchase Receipt',
child_doctype: 'Purchase Receipt Item',
has_rate: true,
price_list_type: 'buying'
});
selection_dialog.show();
});
}

View File

@ -0,0 +1,41 @@
/**
* Интеграция формы подбора с Quotation
*/
frappe.ui.form.on('Quotation', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
if (!frm.doc.party_name) {
frappe.msgprint({
title: __('Warning'),
message: __('Please select Party first'),
indicator: 'orange'
});
return;
}
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Quotation',
child_doctype: 'Quotation Item',
has_rate: true
});
selection_dialog.show();
});
}

View File

@ -0,0 +1,32 @@
/**
* Интеграция формы подбора с Request for Quotation
*/
frappe.ui.form.on('Request for Quotation', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Request for Quotation',
child_doctype: 'Request for Quotation Item',
has_rate: false
});
selection_dialog.show();
});
}

View File

@ -38,7 +38,8 @@ function setup_selection_button(frm) {
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Sales Invoice'
doctype: 'Sales Invoice',
child_doctype: 'Sales Invoice Item'
});
selection_dialog.show();
});

View File

@ -0,0 +1,41 @@
/**
* Интеграция формы подбора с Sales Order
*/
frappe.ui.form.on('Sales Order', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
if (!frm.doc.customer) {
frappe.msgprint({
title: __('Warning'),
message: __('Please select Customer first'),
indicator: 'orange'
});
return;
}
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Sales Order',
child_doctype: 'Sales Order Item',
has_rate: true
});
selection_dialog.show();
});
}

View File

@ -6,6 +6,12 @@ class SelectionDialog {
constructor(opts) {
this.frm = opts.frm;
this.doctype = opts.doctype;
this.child_doctype = opts.child_doctype;
this.items_field = opts.items_field || 'items';
this.has_rate = opts.has_rate !== false;
this.warehouse_field_name = opts.warehouse_field_name || 'warehouse';
this.price_list_type = opts.price_list_type || 'selling';
this.price_list_field = opts.price_list_field || (this.price_list_type === 'buying' ? 'buying_price_list' : 'selling_price_list');
this.cart = {}; // { item_code: {item_code, item_name, qty, rate, discount_pct, ...} }
this.items_data = [];
this.current_page = 0;
@ -60,10 +66,10 @@ class SelectionDialog {
<label>${__('Warehouse')}</label>
<div id="selection-warehouse-link"></div>
</div>
<div class="filter-group">
${this.has_rate ? `<div class="filter-group">
<label>${__('Price List')}</label>
<div id="selection-pricelist-link"></div>
</div>
</div>` : ''}
<div class="filter-group">
<div class="selection-stock-toggle">
<label class="stock-toggle-label">
@ -179,6 +185,9 @@ class SelectionDialog {
// Add unique class to modal wrapper for CSS targeting
this.dialog.$wrapper.addClass('selection-dialog-wrapper');
if (!this.has_rate) {
this.dialog.$wrapper.addClass('selection-no-rate');
}
// Force fullscreen with setTimeout to override Frappe's styles
setTimeout(() => {
@ -486,13 +495,14 @@ class SelectionDialog {
}
setup_pricelist_field() {
if (!this.has_rate) return;
const me = this;
this.pricelist_field = frappe.ui.form.make_control({
df: {
fieldtype: 'Link',
options: 'Price List',
placeholder: __('Select Price List...'),
filters: { selling: 1, enabled: 1 },
filters: { [me.price_list_type]: 1, enabled: 1 },
onchange: function() {
me.filters.price_list = this.value || '';
me.current_page = 0;
@ -503,7 +513,7 @@ class SelectionDialog {
render_input: true,
});
const default_price_list = me.frm.doc.selling_price_list || '';
const default_price_list = me.frm.doc[me.price_list_field] || '';
if (default_price_list) {
me.pricelist_field.set_value(default_price_list);
me.filters.price_list = default_price_list;
@ -571,7 +581,7 @@ class SelectionDialog {
item_group: this.filters.item_group || '',
warehouse: this.filters.warehouse || this.frm.doc.set_warehouse || '',
customer: this.frm.doc.customer || '',
price_list: this.filters.price_list || this.frm.doc.selling_price_list || '',
price_list: this.filters.price_list || this.frm.doc[this.price_list_field] || '',
company: this.frm.doc.company || '',
transaction_date: this.frm.doc.posting_date || frappe.datetime.get_today(),
only_in_stock: this.filters.only_in_stock || false,
@ -597,7 +607,8 @@ class SelectionDialog {
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 !== '')
)
),
skip_price: this.has_rate ? 0 : 1
},
callback: function(r) {
if (r.message) {
@ -627,7 +638,7 @@ class SelectionDialog {
const columns = [
{ key: 'item_name', label: __('Name'), sortable: true },
{ key: 'available_qty', label: __('Stock'), sortable: true },
{ key: 'rate', label: __('Price'), sortable: true },
...(this.has_rate ? [{ key: 'rate', label: __('Price'), sortable: true }] : []),
{ key: 'qty_action', label: __('Qty / Add'), sortable: false }
];
@ -683,9 +694,7 @@ class SelectionDialog {
${stock_text} ${item.stock_uom || ''}
</span>
</td>
<td>
<strong>${me.format_currency(item.rate, item.currency)}</strong>
</td>
${me.has_rate ? `<td><strong>${me.format_currency(item.rate, item.currency)}</strong></td>` : ''}
<td class="action-cell">
<div class="row-action-group">
<input type="text" inputmode="decimal" class="form-control input-sm row-qty-input"
@ -1414,7 +1423,7 @@ class SelectionDialog {
}
cart_array.forEach(item => {
const row = frappe.model.add_child(me.frm.doc, 'Sales Invoice Item', 'items');
const row = frappe.model.add_child(me.frm.doc, me.child_doctype, me.items_field);
row.item_code = item.item_code;
row.item_name = item.item_name;
row.qty = item.qty;
@ -1422,19 +1431,21 @@ class SelectionDialog {
row.uom = item.stock_uom;
row.conversion_factor = 1;
const rate = flt(item.rate || 0);
const discount = flt(item.discount_pct || 0);
const discounted_rate = discount > 0 ? flt(rate * (1 - discount / 100), 2) : rate;
if (me.has_rate) {
const rate = flt(item.rate || 0);
const discount = flt(item.discount_pct || 0);
const discounted_rate = discount > 0 ? flt(rate * (1 - discount / 100), 2) : rate;
row.price_list_rate = rate;
row.discount_percentage = discount > 0 ? discount : 0;
row.rate = discounted_rate;
row.amount = flt(item.qty * discounted_rate, 2);
row.price_list_rate = rate;
row.discount_percentage = discount > 0 ? discount : 0;
row.rate = discounted_rate;
row.amount = flt(item.qty * discounted_rate, 2);
}
if (item.warehouse) row.warehouse = item.warehouse;
if (item.warehouse) row[me.warehouse_field_name] = item.warehouse;
});
me.frm.refresh_field('items');
me.frm.refresh_field(me.items_field);
me.dialog.hide();
frappe.show_alert({

View File

@ -0,0 +1,33 @@
/**
* Интеграция формы подбора с Stock Entry
*/
frappe.ui.form.on('Stock Entry', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Stock Entry',
child_doctype: 'Stock Entry Detail',
has_rate: false,
warehouse_field_name: 's_warehouse'
});
selection_dialog.show();
});
}

View File

@ -0,0 +1,42 @@
/**
* Интеграция формы подбора с Supplier Quotation
*/
frappe.ui.form.on('Supplier Quotation', {
refresh: function(frm) {
if (frm.doc.docstatus === 0) {
setup_selection_button(frm);
}
}
});
function setup_selection_button(frm) {
const grid = frm.fields_dict.items.grid;
grid.wrapper.find('.grid-add-multiple-rows').remove();
$('<button type="button" class="grid-add-multiple-rows btn btn-xs btn-secondary">')
.html(__('Add Multiple'))
.insertAfter(grid.wrapper.find('.grid-add-row'))
.on('click', function(e) {
e.stopImmediatePropagation();
if (!frm.doc.supplier) {
frappe.msgprint({
title: __('Warning'),
message: __('Please select Supplier first'),
indicator: 'orange'
});
return;
}
const selection_dialog = new SelectionDialog({
frm: frm,
doctype: 'Supplier Quotation',
child_doctype: 'Supplier Quotation Item',
has_rate: true,
price_list_type: 'buying'
});
selection_dialog.show();
});
}