Merge branch 'version-13-hotfix' into fix/production-plan/ordered_qty

This commit is contained in:
Sagar Sharma
2022-02-03 11:08:35 +05:30
committed by GitHub
9 changed files with 127 additions and 49 deletions

View File

@@ -43,7 +43,6 @@ class POSInvoice(SalesInvoice):
self.validate_serialised_or_batched_item() self.validate_serialised_or_batched_item()
self.validate_stock_availablility() self.validate_stock_availablility()
self.validate_return_items_qty() self.validate_return_items_qty()
self.validate_non_stock_items()
self.set_status() self.set_status()
self.set_account_for_mode_of_payment() self.set_account_for_mode_of_payment()
self.validate_pos() self.validate_pos()
@@ -176,9 +175,11 @@ class POSInvoice(SalesInvoice):
def validate_stock_availablility(self): def validate_stock_availablility(self):
if self.is_return or self.docstatus != 1: if self.is_return or self.docstatus != 1:
return return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'): for d in self.get('items'):
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
if is_service_item:
return
if d.serial_no: if d.serial_no:
self.validate_pos_reserved_serial_nos(d) self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d) self.validate_delivered_serial_nos(d)
@@ -189,7 +190,7 @@ class POSInvoice(SalesInvoice):
if allow_negative_stock: if allow_negative_stock:
return return
available_stock = get_stock_availability(d.item_code, d.warehouse) available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0: if flt(available_stock) <= 0:
@@ -260,14 +261,6 @@ class POSInvoice(SalesInvoice):
.format(d.idx, bold_serial_no, bold_return_against) .format(d.idx, bold_serial_no, bold_return_against)
) )
def validate_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
if not frappe.db.exists('Product Bundle', d.item_code):
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
def validate_mode_of_payment(self): def validate_mode_of_payment(self):
if len(self.payments) == 0: if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
@@ -507,12 +500,18 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist() @frappe.whitelist()
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):
if frappe.db.get_value('Item', item_code, 'is_stock_item'): if frappe.db.get_value('Item', item_code, 'is_stock_item'):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse) bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty return bin_qty - pos_sales_qty, is_stock_item
else: else:
is_stock_item = False
if frappe.db.exists('Product Bundle', item_code): if frappe.db.exists('Product Bundle', item_code):
return get_bundle_availability(item_code, warehouse) return get_bundle_availability(item_code, warehouse), is_stock_item
else:
# Is a service item
return 0, is_stock_item
def get_bundle_availability(bundle_item_code, warehouse): def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code) product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)

View File

@@ -70,9 +70,18 @@ class BuyingController(StockController, Subcontracting):
# set contact and address details for supplier, if they are not mentioned # set contact and address details for supplier, if they are not mentioned
if getattr(self, "supplier", None): if getattr(self, "supplier", None):
self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions, self.update_if_missing(
doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'), get_party_details(
fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'))) self.supplier,
party_type="Supplier",
doctype=self.doctype,
company=self.company,
party_address=self.get("supplier_address"),
shipping_address=self.get('shipping_address'),
fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'),
ignore_permissions=self.flags.ignore_permissions
)
)
self.set_missing_item_details(for_validate) self.set_missing_item_details(for_validate)

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.functions import Min
from frappe.utils import comma_and, get_link_to_form, getdate from frappe.utils import comma_and, get_link_to_form, getdate
@@ -60,8 +61,15 @@ class ProgramEnrollment(Document):
frappe.throw(_("Student is already enrolled.")) frappe.throw(_("Student is already enrolled."))
def update_student_joining_date(self): def update_student_joining_date(self):
date = frappe.db.sql("select min(enrollment_date) from `tabProgram Enrollment` where student= %s", self.student) table = frappe.qb.DocType('Program Enrollment')
frappe.db.set_value("Student", self.student, "joining_date", date) date = (
frappe.qb.from_(table)
.select(Min(table.enrollment_date).as_('enrollment_date'))
.where(table.student == self.student)
).run(as_dict=True)
if date:
frappe.db.set_value("Student", self.student, "joining_date", date[0].enrollment_date)
def make_fee_records(self): def make_fee_records(self):
from erpnext.education.api import get_fee_components from erpnext.education.api import get_fee_components

View File

@@ -207,8 +207,8 @@ class TestEmployeeReminders(unittest.TestCase):
# teardown: enable emp 2 # teardown: enable emp 2
frappe.db.set_value('Employee', self.test_employee_2.name, { frappe.db.set_value('Employee', self.test_employee_2.name, {
'status': 'Left', 'status': 'Active',
'holiday_list': self.holiday_list_2 'holiday_list': self.holiday_list_2.name
}) })
def test_advance_holiday_reminders_weekly(self): def test_advance_holiday_reminders_weekly(self):
@@ -232,8 +232,8 @@ class TestEmployeeReminders(unittest.TestCase):
# teardown: enable emp 2 # teardown: enable emp 2
frappe.db.set_value('Employee', self.test_employee_2.name, { frappe.db.set_value('Employee', self.test_employee_2.name, {
'status': 'Left', 'status': 'Active',
'holiday_list': self.holiday_list_2 'holiday_list': self.holiday_list_2.name
}) })
def test_reminder_not_sent_if_no_holdays(self): def test_reminder_not_sent_if_no_holdays(self):

View File

@@ -24,7 +24,7 @@ def search_by_term(search_term, warehouse, price_list):
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"], ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"],
as_dict=1) as_dict=1)
item_stock_qty = get_stock_availability(item_code, warehouse) item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
price_list_rate, currency = frappe.db.get_value('Item Price', { price_list_rate, currency = frappe.db.get_value('Item Price', {
'price_list': price_list, 'price_list': price_list,
'item_code': item_code 'item_code': item_code
@@ -99,7 +99,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
), {'warehouse': warehouse}, as_dict=1) ), {'warehouse': warehouse}, as_dict=1)
if items_data: if items_data:
items_data = filter_service_items(items_data)
items = [d.item_code for d in items_data] items = [d.item_code for d in items_data]
item_prices_data = frappe.get_all("Item Price", item_prices_data = frappe.get_all("Item Price",
fields = ["item_code", "price_list_rate", "currency"], fields = ["item_code", "price_list_rate", "currency"],
@@ -112,7 +111,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
for item in items_data: for item in items_data:
item_code = item.item_code item_code = item.item_code
item_price = item_prices.get(item_code) or {} item_price = item_prices.get(item_code) or {}
item_stock_qty = get_stock_availability(item_code, warehouse) item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
row = {} row = {}
row.update(item) row.update(item)
@@ -144,14 +143,6 @@ def search_for_serial_or_batch_or_barcode_number(search_value):
return {} return {}
def filter_service_items(items):
for item in items:
if not item['is_stock_item']:
if not frappe.db.exists('Product Bundle', item['item_code']):
items.remove(item)
return items
def get_conditions(search_term): def get_conditions(search_term):
condition = "(" condition = "("
condition += """item.name like {search_term} condition += """item.name like {search_term}

View File

@@ -248,7 +248,7 @@ erpnext.PointOfSale.Controller = class {
numpad_event: (value, action) => this.update_item_field(value, action), numpad_event: (value, action) => this.update_item_field(value, action),
checkout: () => this.payment.checkout(), checkout: () => this.save_and_checkout(),
edit_cart: () => this.payment.edit_cart(), edit_cart: () => this.payment.edit_cart(),
@@ -630,18 +630,24 @@ erpnext.PointOfSale.Controller = class {
} }
async check_stock_availability(item_row, qty_needed, warehouse) { async check_stock_availability(item_row, qty_needed, warehouse) {
const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message; const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message;
const available_qty = resp[0];
const is_stock_item = resp[1];
frappe.dom.unfreeze(); frappe.dom.unfreeze();
const bold_item_code = item_row.item_code.bold(); const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold(); const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold() const bold_available_qty = available_qty.toString().bold()
if (!(available_qty > 0)) { if (!(available_qty > 0)) {
if (is_stock_item) {
frappe.model.clear_doc(item_row.doctype, item_row.name); frappe.model.clear_doc(item_row.doctype, item_row.name);
frappe.throw({ frappe.throw({
title: __("Not Available"), title: __("Not Available"),
message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
}) });
} else {
return;
}
} else if (available_qty < qty_needed) { } else if (available_qty < qty_needed) {
frappe.throw({ frappe.throw({
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
@@ -675,8 +681,8 @@ erpnext.PointOfSale.Controller = class {
}, },
callback(res) { callback(res) {
if (!me.item_stock_map[item_code]) if (!me.item_stock_map[item_code])
me.item_stock_map[item_code] = {} me.item_stock_map[item_code] = {};
me.item_stock_map[item_code][warehouse] = res.message; me.item_stock_map[item_code][warehouse] = res.message[0];
} }
}); });
} }
@@ -707,4 +713,9 @@ erpnext.PointOfSale.Controller = class {
}) })
.catch(e => console.log(e)); .catch(e => console.log(e));
} }
async save_and_checkout() {
this.frm.is_dirty() && await this.frm.save();
this.payment.checkout();
}
}; };

View File

@@ -191,10 +191,10 @@ erpnext.PointOfSale.ItemCart = class {
this.numpad_value = ''; this.numpad_value = '';
}); });
this.$component.on('click', '.checkout-btn', function() { this.$component.on('click', '.checkout-btn', async function() {
if ($(this).attr('style').indexOf('--blue-500') == -1) return; if ($(this).attr('style').indexOf('--blue-500') == -1) return;
me.events.checkout(); await me.events.checkout();
me.toggle_checkout_btn(false); me.toggle_checkout_btn(false);
me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); me.allow_discount_change && me.$add_discount_elem.removeClass("d-none");
@@ -985,6 +985,7 @@ erpnext.PointOfSale.ItemCart = class {
$(frm.wrapper).off('refresh-fields'); $(frm.wrapper).off('refresh-fields');
$(frm.wrapper).on('refresh-fields', () => { $(frm.wrapper).on('refresh-fields', () => {
if (frm.doc.items.length) { if (frm.doc.items.length) {
this.$cart_items_wrapper.html('');
frm.doc.items.forEach(item => { frm.doc.items.forEach(item => {
this.update_item_html(item); this.update_item_html(item);
}); });

View File

@@ -79,15 +79,21 @@ erpnext.PointOfSale.ItemSelector = class {
const me = this; const me = this;
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item; const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item;
const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange";
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
let indicator_color;
let qty_to_display = actual_qty; let qty_to_display = actual_qty;
if (item.is_stock_item) {
indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange");
if (Math.round(qty_to_display) > 999) { if (Math.round(qty_to_display) > 999) {
qty_to_display = Math.round(qty_to_display)/1000; qty_to_display = Math.round(qty_to_display)/1000;
qty_to_display = qty_to_display.toFixed(1) + 'K'; qty_to_display = qty_to_display.toFixed(1) + 'K';
} }
} else {
indicator_color = '';
qty_to_display = '';
}
function get_item_image_html() { function get_item_image_html() {
if (!me.hide_images && item_image) { if (!me.hide_images && item_image) {

View File

@@ -0,0 +1,53 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.selling.page.point_of_sale.point_of_sale import get_items
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.tests.utils import ERPNextTestCase
class TestPointOfSale(ERPNextTestCase):
def test_item_search(self):
"""
Test Stock and Service Item Search.
"""
pos_profile = make_pos_profile()
item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
make_stock_entry(
item_code="Test Search Stock Item",
qty=10,
to_warehouse="_Test Warehouse - _TC",
rate=500,
)
result = get_items(
start=0,
page_length=20,
price_list=None,
item_group=item1.item_group,
pos_profile=pos_profile.name,
search_term="Test Search Stock Item",
)
filtered_items = result.get("items")
self.assertEqual(len(filtered_items), 1)
self.assertEqual(filtered_items[0]["item_code"], item1.item_code)
self.assertEqual(filtered_items[0]["actual_qty"], 10)
item2 = make_item("Test Search Service Item", {"is_stock_item": 0})
result = get_items(
start=0,
page_length=20,
price_list=None,
item_group=item2.item_group,
pos_profile=pos_profile.name,
search_term="Test Search Service Item",
)
filtered_items = result.get("items")
self.assertEqual(len(filtered_items), 1)
self.assertEqual(filtered_items[0]["item_code"], item2.item_code)