diff --git a/.flake8 b/.flake8 index 5735456ae7d..4ff88403244 100644 --- a/.flake8 +++ b/.flake8 @@ -29,6 +29,8 @@ ignore = B950, W191, E124, # closing bracket, irritating while writing QB code + E131, # continuation line unaligned for hanging indent + E123, # closing bracket does not match indentation of opening bracket's line max-line-length = 200 exclude=.github/helper/semgrep_rules diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 88aa7ef8b59..2bdaa1049b7 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -134,7 +134,8 @@ { "fieldname": "allocated_amount", "fieldtype": "Currency", - "label": "Allocated Amount" + "label": "Allocated Amount", + "options": "currency" }, { "fieldname": "amended_from", @@ -152,7 +153,8 @@ { "fieldname": "unallocated_amount", "fieldtype": "Currency", - "label": "Unallocated Amount" + "label": "Unallocated Amount", + "options": "currency" }, { "fieldname": "party_section", @@ -192,10 +194,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-04-14 17:31:58.963529", + "modified": "2022-03-21 19:05:04.208222", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -242,6 +245,7 @@ ], "sort_field": "date", "sort_order": "DESC", + "states": [], "title_field": "bank_account", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json index 5c19091c3fb..ed8ff7c0f7a 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json @@ -110,13 +110,12 @@ "description": "Reference number of the invoice from the previous system", "fieldname": "invoice_number", "fieldtype": "Data", - "in_list_view": 1, "label": "Invoice Number" } ], "istable": 1, "links": [], - "modified": "2021-12-17 19:25:06.053187", + "modified": "2022-03-21 19:31:45.382656", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool Item", @@ -125,5 +124,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 0c6e7edeb02..b8500270d1a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -264,7 +264,6 @@ "print_hide": 1 }, { - "allow_on_submit": 1, "default": "0", "fieldname": "is_return", "fieldtype": "Check", @@ -1573,7 +1572,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-10-05 12:11:53.871828", + "modified": "2022-03-22 13:00:24.166684", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", @@ -1623,6 +1622,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 91c07ade7fb..275eeb30f03 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -16,7 +16,11 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( ) from erpnext.accounts.party import get_due_date, get_party_account from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty -from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivered_serial_nos, + get_pos_reserved_serial_nos, + get_serial_nos, +) class POSInvoice(SalesInvoice): @@ -145,12 +149,7 @@ class POSInvoice(SalesInvoice): .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable")) def validate_delivered_serial_nos(self, item): - serial_nos = get_serial_nos(item.serial_no) - delivered_serial_nos = frappe.db.get_list('Serial No', { - 'item_code': item.item_code, - 'name': ['in', serial_nos], - 'sales_invoice': ['is', 'set'] - }, pluck='name') + delivered_serial_nos = get_delivered_serial_nos(item.serial_no) if delivered_serial_nos: bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos)) @@ -172,10 +171,14 @@ class POSInvoice(SalesInvoice): frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_stock_availablility(self): + if self.is_return: + return + + if self.docstatus.is_draft() and not frappe.db.get_value('POS Profile', self.pos_profile, 'validate_stock_on_save'): + return + from erpnext.stock.stock_ledger import is_negative_stock_allowed - if self.is_return or self.docstatus != 1: - return 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: @@ -485,16 +488,15 @@ class POSInvoice(SalesInvoice): "payment_account": pay.account, }, ["name"]) - args = { - 'doctype': 'Payment Request', + filters = { 'reference_doctype': 'POS Invoice', 'reference_name': self.name, 'payment_gateway_account': payment_gateway_account, 'email_to': self.contact_mobile } - pr = frappe.db.exists(args) + pr = frappe.db.get_value('Payment Request', filters=filters) if pr: - return frappe.get_doc('Payment Request', pr[0][0]) + return frappe.get_doc('Payment Request', pr) @frappe.whitelist() def get_stock_availability(item_code, warehouse): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index cf8affdd010..e06f7aa9a7d 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -340,6 +340,7 @@ class TestPOSInvoice(unittest.TestCase): item=se.get("items")[0].item_code, rate=1000, do_not_save=1) si.get("items")[0].serial_no = serial_nos[0] + si.update_stock = 1 si.insert() si.submit() @@ -610,6 +611,78 @@ class TestPOSInvoice(unittest.TestCase): pos_inv.delete() pr.delete() + def test_delivered_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint('before_test_delivered_serial_no_case') + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", serial_no=serial_no + ) + + delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") + self.assertEquals(delivery_document_no, dn.name) + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=True + ) + + self.assertRaises(frappe.ValidationError, pos_inv.submit) + + finally: + frappe.db.rollback(save_point='before_test_delivered_serial_no_case') + frappe.set_user("Administrator") + + def test_returned_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint('before_test_returned_serial_no_case') + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + ) + + pos_return = make_sales_return(pos_inv.name) + pos_return.flags.ignore_validate = True + pos_return.insert() + pos_return.submit() + + pos_reserved_serial_nos = get_pos_reserved_serial_nos({ + 'item_code': '_Test Serialized Item With Series', + 'warehouse': '_Test Warehouse - _TC' + }) + self.assertTrue(serial_no not in pos_reserved_serial_nos) + + finally: + frappe.db.rollback(save_point='before_test_returned_serial_no_case') + frappe.set_user("Administrator") def create_pos_invoice(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index d4513c6a686..b3d9c15f086 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -53,7 +53,7 @@ class POSInvoiceMergeLog(Document): frappe.throw(msg) def on_submit(self): - pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] sales = [d for d in pos_invoice_docs if d.get('is_return') == 0] @@ -70,7 +70,7 @@ class POSInvoiceMergeLog(Document): self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) def on_cancel(self): - pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] self.update_pos_invoices(pos_invoice_docs) self.cancel_linked_invoices() @@ -254,7 +254,7 @@ def get_all_unconsolidated_invoices(): 'docstatus': 1 } pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, - fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer']) + fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer', 'is_return', 'return_against']) return pos_invoices @@ -294,17 +294,62 @@ def unconsolidate_pos_invoices(closing_entry): else: cancel_merge_logs(merge_logs, closing_entry) +def split_invoices(invoices): + ''' + Splits invoices into multiple groups + Use-case: + If a serial no is sold and later it is returned + then split the invoices such that the selling entry is merged first and then the return entry + ''' + # Input + # invoices = [ + # {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}, + # {'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, + # {'pos_invoice': 'Invoice with SR#2', 'is_return': 0} + # ] + # Output + # _invoices = [ + # [{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}], + # [{'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}], + # ] + + _invoices = [] + special_invoices = [] + pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against] + for pos_invoice in pos_return_docs: + for item in pos_invoice.items: + if not item.serial_no: + continue + + return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) + if return_against_is_added: + break + + return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated' + if return_against_is_consolidated: + break + + pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against] + _invoices.append(pos_invoice_row) + special_invoices.append(pos_invoice.return_against) + break + + _invoices.append([d for d in invoices if d.pos_invoice not in special_invoices]) + + return _invoices + def create_merge_logs(invoice_by_customer, closing_entry=None): try: for customer, invoices in invoice_by_customer.items(): - merge_log = frappe.new_doc('POS Invoice Merge Log') - merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() - merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None + for _invoices in split_invoices(invoices): + merge_log = frappe.new_doc('POS Invoice Merge Log') + merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() + merge_log.customer = customer + merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None - merge_log.set('pos_invoices', invoices) - merge_log.save(ignore_permissions=True) - merge_log.submit() + merge_log.set('pos_invoices', _invoices) + merge_log.save(ignore_permissions=True) + merge_log.submit() if closing_entry: closing_entry.set_status(update=True, status='Submitted') diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 8909da96fcf..fe57ce2f0f0 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -382,6 +382,68 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) self.assertEqual(consolidated_invoice.rounding_adjustment, 1) + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_serial_no_case_1(self): + ''' + Create a POS Invoice with serial no + Create a Return Invoice with serial no + Create a POS Invoice with serial no again + Consolidate the invoices + + The first POS Invoice should be consolidated with a separate single Merge Log + The second and third POS Invoice should be consolidated with a single Merge Log + ''' + + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=1 + ) + pos_inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100 + }) + pos_inv.submit() + + pos_inv_cn = make_sales_return(pos_inv.name) + pos_inv_cn.paid_amount = -100 + pos_inv_cn.submit() + + pos_inv2 = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=1 + ) + pos_inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100 + }) + pos_inv2.submit() + + consolidate_pos_invoices() + + pos_inv.load_from_db() + pos_inv2.load_from_db() + + self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice) + finally: frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") diff --git a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json index 205c4ede901..387c4b0f360 100644 --- a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json +++ b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json @@ -9,7 +9,9 @@ "posting_date", "column_break_3", "customer", - "grand_total" + "grand_total", + "is_return", + "return_against" ], "fields": [ { @@ -48,11 +50,27 @@ "in_list_view": 1, "label": "Amount", "reqd": 1 + }, + { + "default": "0", + "fetch_from": "pos_invoice.is_return", + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "read_only": 1 + }, + { + "fetch_from": "pos_invoice.return_against", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against", + "options": "POS Invoice", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-05-29 15:08:42.194979", + "modified": "2022-03-24 13:32:02.366257", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Reference", @@ -61,5 +79,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 9c9f37bba27..11646a6517d 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -22,6 +22,7 @@ "hide_images", "hide_unavailable_items", "auto_add_item_to_cart", + "validate_stock_on_save", "column_break_16", "update_stock", "ignore_pricing_rule", @@ -351,6 +352,12 @@ { "fieldname": "column_break_25", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "validate_stock_on_save", + "fieldtype": "Check", + "label": "Validate Stock on Save" } ], "icon": "icon-cog", @@ -378,10 +385,11 @@ "link_fieldname": "pos_profile" } ], - "modified": "2021-10-14 14:17:00.469298", + "modified": "2022-03-21 13:29:28.480533", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -404,5 +412,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e107912fd27..94334892f0d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1711,6 +1711,7 @@ def make_delivery_note(source_name, target_doc=None): } }, target_doc, set_missing_values) + doclist.set_onload('ignore_price_list', True) return doclist @frappe.whitelist() diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2c6654285ff..f93f9feb88d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -442,6 +442,8 @@ def make_purchase_receipt(source_name, target_doc=None): } }, target_doc, set_missing_values) + doc.set_onload('ignore_price_list', True) + return doc @frappe.whitelist() @@ -509,6 +511,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions doc = get_mapped_doc("Purchase Order", source_name, fields, target_doc, postprocess, ignore_permissions=ignore_permissions) + doc.set_onload('ignore_price_list', True) return doc diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 81fc32424e6..d5788638f70 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -139,6 +139,7 @@ def make_purchase_order(source_name, target_doc=None): }, }, target_doc, set_missing_values) + doclist.set_onload('ignore_price_list', True) return doclist @frappe.whitelist() diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index dd9b45cc3f9..d870823ad1d 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -168,7 +168,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): {account_type_condition} AND is_group = 0 AND company = %(company)s - AND account_currency = %(currency)s + AND (account_currency = %(currency)s or ifnull(account_currency, '') = '') AND `{searchfield}` LIKE %(txt)s {mcond} ORDER BY idx DESC, name diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 21666336e03..d6296eb589e 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -399,6 +399,8 @@ def make_return_doc(doctype, source_name, target_doc=None): } }, target_doc, set_missing_values) + doclist.set_onload('ignore_price_list', True) + return doclist def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 19acc105b37..29da5f11ab0 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -114,20 +114,16 @@ class calculate_taxes_and_totals(object): for item in self.doc.get("items"): self.doc.round_floats_in(item) - if not item.rate: - item.rate = item.price_list_rate - if item.discount_percentage == 100: item.rate = 0.0 elif item.price_list_rate: - if item.pricing_rules or abs(item.discount_percentage) > 0: + if not item.rate or (item.pricing_rules and item.discount_percentage > 0): item.rate = flt(item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) - if abs(item.discount_percentage) > 0: - item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) + item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) - elif item.discount_amount or item.pricing_rules: + elif item.discount_amount and item.pricing_rules: item.rate = item.price_list_rate - item.discount_amount if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 20fb987c601..88a9c10131e 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -225,9 +225,7 @@ def _check_agent_availability(agent_email, scheduled_time): def _get_employee_from_user(user): - employee_docname = frappe.db.exists( - {'doctype': 'Employee', 'user_id': user}) + employee_docname = frappe.db.get_value('Employee', {'user_id': user}) if employee_docname: - # frappe.db.exists returns a tuple of a tuple - return frappe.get_doc('Employee', employee_docname[0][0]) + return frappe.get_doc('Employee', employee_docname) return None diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index f4086dc37c8..776e6043331 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -8,50 +8,44 @@ import frappe def create_test_lead(): - test_lead = frappe.db.exists({'doctype': 'Lead', 'email_id':'test@example.com'}) - if test_lead: - return frappe.get_doc('Lead', test_lead[0][0]) - test_lead = frappe.get_doc({ - 'doctype': 'Lead', - 'lead_name': 'Test Lead', - 'email_id': 'test@example.com' - }) - test_lead.insert(ignore_permissions=True) - return test_lead + test_lead = frappe.db.get_value("Lead", {"email_id": "test@example.com"}) + if test_lead: + return frappe.get_doc("Lead", test_lead) + test_lead = frappe.get_doc( + {"doctype": "Lead", "lead_name": "Test Lead", "email_id": "test@example.com"} + ) + test_lead.insert(ignore_permissions=True) + return test_lead def create_test_appointments(): - test_appointment = frappe.db.exists( - {'doctype': 'Appointment', 'scheduled_time':datetime.datetime.now(),'email':'test@example.com'}) - if test_appointment: - return frappe.get_doc('Appointment', test_appointment[0][0]) - test_appointment = frappe.get_doc({ - 'doctype': 'Appointment', - 'email': 'test@example.com', - 'status': 'Open', - 'customer_name': 'Test Lead', - 'customer_phone_number': '666', - 'customer_skype': 'test', - 'customer_email': 'test@example.com', - 'scheduled_time': datetime.datetime.now() - }) - test_appointment.insert() - return test_appointment + test_appointment = frappe.get_doc( + { + "doctype": "Appointment", + "email": "test@example.com", + "status": "Open", + "customer_name": "Test Lead", + "customer_phone_number": "666", + "customer_skype": "test", + "customer_email": "test@example.com", + "scheduled_time": datetime.datetime.now(), + } + ) + test_appointment.insert() + return test_appointment class TestAppointment(unittest.TestCase): - test_appointment = test_lead = None + test_appointment = test_lead = None - def setUp(self): - self.test_lead = create_test_lead() - self.test_appointment = create_test_appointments() + def setUp(self): + self.test_lead = create_test_lead() + self.test_appointment = create_test_appointments() - def test_calendar_event_created(self): - cal_event = frappe.get_doc( - 'Event', self.test_appointment.calendar_event) - self.assertEqual(cal_event.starts_on, - self.test_appointment.scheduled_time) + def test_calendar_event_created(self): + cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event) + self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time) - def test_lead_linked(self): - lead = frappe.get_doc('Lead', self.test_lead.name) - self.assertIsNotNone(lead) + def test_lead_linked(self): + lead = frappe.get_doc("Lead", self.test_lead.name) + self.assertIsNotNone(lead) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js index 741e78f4a55..7108cabfb3f 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.js +++ b/erpnext/e_commerce/doctype/website_item/website_item.js @@ -5,6 +5,12 @@ frappe.ui.form.on('Website Item', { onload: function(frm) { // should never check Private frm.fields_dict["website_image"].df.is_private = 0; + + frm.set_query("website_warehouse", () => { + return { + filters: {"is_group": 0} + }; + }); }, image: function() { diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js index 6dce79dd72b..fb63b21a083 100644 --- a/erpnext/e_commerce/product_ui/views.js +++ b/erpnext/e_commerce/product_ui/views.js @@ -418,6 +418,22 @@ erpnext.ProductView = class { me.change_route_with_filters(); }); + + // bind filter lookup input box + $('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => { + const $input = $(e.target); + const keyword = ($input.val() || '').toLowerCase(); + const $filter_options = $input.next('.filter-options'); + + $filter_options.find('.filter-lookup-wrapper').show(); + $filter_options.find('.filter-lookup-wrapper').each((i, el) => { + const $el = $(el); + const value = $el.data('value').toLowerCase(); + if (!value.includes(keyword)) { + $el.hide(); + } + }); + }, 300)); } change_route_with_filters() { diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index ee098e16e7a..3eb75cb82d0 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -15,6 +15,7 @@ class TestVariantSelector(FrappeTestCase): @classmethod def setUpClass(cls): + super().setUpClass() template_item = make_item("Test-Tshirt-Temp", { "has_variant": 1, "variant_based_on": "Item Attribute", diff --git a/erpnext/education/setup.py b/erpnext/education/setup.py index b7169261764..663f1cab4fd 100644 --- a/erpnext/education/setup.py +++ b/erpnext/education/setup.py @@ -3,7 +3,6 @@ import frappe -from erpnext.setup.utils import insert_record def setup_education(): @@ -13,6 +12,21 @@ def setup_education(): return create_academic_sessions() + +def insert_record(records): + for r in records: + doc = frappe.new_doc(r.get("doctype")) + doc.update(r) + try: + doc.insert(ignore_permissions=True) + except frappe.DuplicateEntryError as e: + # pass DuplicateEntryError and continue + if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: + # make sure DuplicateEntryError is for the exact same doc and not a related doc + pass + else: + raise + def create_academic_sessions(): data = [ {"doctype": "Academic Year", "academic_year_name": "2015-16"}, diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 7d32fd8865b..3a309902684 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -25,6 +25,7 @@ from erpnext.hr.doctype.leave_application.leave_application import ( LeaveDayBlockedError, NotAnOptionalHoliday, OverlapError, + get_leave_allocation_records, get_leave_balance_on, get_leave_details, ) @@ -882,6 +883,27 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(leave_allocation['leaves_pending_approval'], 1) self.assertEqual(leave_allocation['remaining_leaves'], 26) + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_get_leave_allocation_records(self): + employee = get_employee() + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave_expiry", + is_carry_forward=1, + expire_carry_forwarded_leaves_after_days=90) + leave_type.insert() + + leave_alloc = create_carry_forwarded_allocation(employee, leave_type) + details = get_leave_allocation_records(employee.name, getdate(), leave_type.name) + expected_data = { + "from_date": getdate(leave_alloc.from_date), + "to_date": getdate(leave_alloc.to_date), + "total_leaves_allocated": 30.0, + "unused_leaves": 15.0, + "new_leaves_allocated": 15.0, + "leave_type": leave_type.name + } + self.assertEqual(details.get(leave_type.name), expected_data) + def create_carry_forwarded_allocation(employee, leave_type): # initial leave allocation @@ -903,6 +925,8 @@ def create_carry_forwarded_allocation(employee, leave_type): carry_forward=1) leave_allocation.submit() + return leave_allocation + def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None): allocation = frappe.get_doc({ "doctype": "Leave Allocation", @@ -931,12 +955,9 @@ def set_leave_approver(): dept_doc.save(ignore_permissions=True) def get_leave_period(): - leave_period_name = frappe.db.exists({ - "doctype": "Leave Period", - "company": "_Test Company" - }) + leave_period_name = frappe.db.get_value("Leave Period", {"company": "_Test Company"}) if leave_period_name: - return frappe.get_doc("Leave Period", leave_period_name[0][0]) + return frappe.get_doc("Leave Period", leave_period_name) else: return frappe.get_doc(dict( name = 'Test Leave Period', diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 5f492d7cf27..960d0e51524 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -500,6 +500,9 @@ class JobCard(Document): 2: "Cancelled" }[self.docstatus or 0] + if self.for_quantity <= self.transferred_qty: + self.status = 'Material Transferred' + if self.time_logs: self.status = 'Work In Progress' @@ -507,10 +510,6 @@ class JobCard(Document): (self.for_quantity <= self.total_completed_qty or not self.items)): self.status = 'Completed' - if self.status != 'Completed': - if self.for_quantity <= self.transferred_qty: - self.status = 'Material Transferred' - if update_status: self.db_set('status', self.status) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 33425d23142..c5841c16f2d 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -169,6 +169,7 @@ class TestJobCard(FrappeTestCase): job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) job_card = frappe.get_doc("Job Card", job_card_name) + self.assertEqual(job_card.status, "Open") # fully transfer both RMs transfer_entry_1 = make_stock_entry_from_jc(job_card_name) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index e8759f55284..59ddf1f0c58 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -2,6 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Production Plan', { + + before_save: function(frm) { + // preserve temporary names on production plan item to re-link sub-assembly items + frm.doc.po_items.forEach(item => { + item.temporary_name = item.name; + }); + }, setup: function(frm) { frm.custom_make_buttons = { 'Work Order': 'Work Order / Subcontract PO', diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 2b6e6968bd3..349f40e9c7b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -32,6 +32,7 @@ class ProductionPlan(Document): self.set_pending_qty_in_row_without_reference() self.calculate_total_planned_qty() self.set_status() + self._rename_temporary_references() def set_pending_qty_in_row_without_reference(self): "Set Pending Qty in independent rows (not from SO or MR)." @@ -57,6 +58,18 @@ class ProductionPlan(Document): if not flt(d.planned_qty): frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx)) + def _rename_temporary_references(self): + """ po_items and sub_assembly_items items are both constructed client side without saving. + + Attempt to fix linkages by using temporary names to map final row names. + """ + new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name} + actual_names = {d.name for d in self.po_items} + + for sub_assy in self.sub_assembly_items: + if sub_assy.production_plan_item not in actual_names: + sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item) + @frappe.whitelist() def get_open_sales_orders(self): """ Pull sales orders which are pending to deliver based on criteria selected""" diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 6425374b1b2..ec497035fe5 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -667,6 +667,39 @@ class TestProductionPlan(FrappeTestCase): wo_doc.submit() self.assertEqual(wo_doc.qty, 0.55) + def test_temporary_name_relinking(self): + + pp = frappe.new_doc("Production Plan") + + # this can not be unittested so mocking data that would be expected + # from client side. + for _ in range(10): + po_item = pp.append("po_items", { + "name": frappe.generate_hash(length=10), + "temporary_name": frappe.generate_hash(length=10), + }) + pp.append("sub_assembly_items", { + "production_plan_item": po_item.temporary_name + }) + pp._rename_temporary_references() + + for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): + self.assertEqual(po_item.name, subassy_item.production_plan_item) + + # bad links should be erased + pp.append("sub_assembly_items", { + "production_plan_item": frappe.generate_hash(length=16) + }) + pp._rename_temporary_references() + self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item) + pp.sub_assembly_items.pop() + + # reattempting on same doc shouldn't change anything + pp._rename_temporary_references() + for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): + self.assertEqual(po_item.name, subassy_item.production_plan_item) + + def create_production_plan(**args): """ sales_order (obj): Sales Order Doc Object diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json index f829d57475a..df5862fcac8 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -27,7 +27,8 @@ "material_request", "material_request_item", "product_bundle_item", - "item_reference" + "item_reference", + "temporary_name" ], "fields": [ { @@ -204,17 +205,25 @@ "fieldtype": "Data", "hidden": 1, "label": "Item Reference" + }, + { + "fieldname": "temporary_name", + "fieldtype": "Data", + "hidden": 1, + "label": "temporary name" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-28 18:31:06.822168", + "modified": "2022-03-24 04:54:09.940224", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 7eb40ec6601..e832ac9c7f8 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -457,7 +457,8 @@ class WorkOrder(Document): mr_obj.update_requested_qty([self.material_request_item]) def update_ordered_qty(self): - if self.production_plan and self.production_plan_item: + if self.production_plan and self.production_plan_item \ + and not self.production_plan_sub_assembly_item: qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0 if self.docstatus == 1: @@ -644,9 +645,13 @@ class WorkOrder(Document): if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) - if self.production_plan and self.production_plan_item: + if self.production_plan and self.production_plan_item \ + and not self.production_plan_sub_assembly_item: qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1) + if not qty_dict: + return + allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0) @@ -1150,6 +1155,10 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create doc.insert() frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True) + if enable_capacity_planning: + # automatically added scheduling rows shouldn't change status to WIP + doc.db_set("status", "Open") + return doc def get_work_order_operation_data(work_order, operation, workstation): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 31999126214..dc1d6929aaf 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,7 +1,7 @@ [pre_model_sync] erpnext.patches.v12_0.update_is_cancelled_field -erpnext.patches.v13_0.add_bin_unique_constraint erpnext.patches.v11_0.rename_production_order_to_work_order +erpnext.patches.v13_0.add_bin_unique_constraint erpnext.patches.v11_0.refactor_naming_series erpnext.patches.v11_0.refactor_autoname_naming execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28 @@ -360,4 +360,5 @@ erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.delete_non_profit_doctypes erpnext.patches.v14_0.update_employee_advance_status erpnext.patches.v13_0.add_cost_center_in_loans -erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items +erpnext.patches.v13_0.set_return_against_in_pos_invoice_references +erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index bf5438c4d2e..80d51652abe 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -60,7 +60,7 @@ def execute(): def convert_to_seconds(value, unit): seconds = 0 - if value == 0: + if not value: return seconds if unit == 'Hours': seconds = value * 3600 diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py new file mode 100644 index 00000000000..6c24f520274 --- /dev/null +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -0,0 +1,38 @@ +import frappe + + +def execute(): + ''' + Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table. + ''' + + POSClosingEntry = frappe.qb.DocType("POS Closing Entry") + open_pos_closing_entries = ( + frappe.qb + .from_(POSClosingEntry) + .select(POSClosingEntry.name) + .where(POSClosingEntry.docstatus == 0) + .run(pluck=True) + ) + + if not open_pos_closing_entries: + return + + POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") + POSInvoice = frappe.qb.DocType("POS Invoice") + pos_invoice_references = ( + frappe.qb + .from_(POSInvoiceReference) + .join(POSInvoice) + .on(POSInvoiceReference.pos_invoice == POSInvoice.name) + .select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against) + .where(POSInvoiceReference.parent.isin(open_pos_closing_entries)) + .run(as_dict=True) + ) + + for row in pos_invoice_references: + frappe.db.set_value("POS Invoice Reference", row.name, "is_return", row.is_return) + if row.is_return: + frappe.db.set_value("POS Invoice Reference", row.name, "return_against", row.return_against) + else: + frappe.db.set_value("POS Invoice Reference", row.name, "return_against", None) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 32b0f0f20ce..9061c5f1ee5 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -708,6 +708,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr if not_submitted_ss: frappe.msgprint(_("Could not submit some Salary Slips")) + frappe.flags.via_payroll_entry = False + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 5e41b661f86..f0721357d35 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -38,6 +38,8 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar class TestSalarySlip(unittest.TestCase): def setUp(self): setup_test() + frappe.flags.pop("via_payroll_entry", None) + def tearDown(self): frappe.db.rollback() @@ -409,15 +411,17 @@ class TestSalarySlip(unittest.TestCase): "email_salary_slip_to_employee": 1 }) def test_email_salary_slip(self): - frappe.db.sql("delete from `tabEmail Queue`") + frappe.db.delete("Email Queue") - make_employee("test_email_salary_slip@salary.com", company="_Test Company") - ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email") + user_id = "test_email_salary_slip@salary.com" + + make_employee(user_id, company="_Test Company") + ss = make_employee_salary_slip(user_id, "Monthly", "Test Salary Slip Email") ss.company = "_Test Company" ss.save() ss.submit() - email_queue = frappe.db.sql("""select name from `tabEmail Queue`""") + email_queue = frappe.db.a_row_exists("Email Queue") self.assertTrue(email_queue) def test_loan_repayment_salary_slip(self): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 00373a65138..19e12e3c692 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1042,9 +1042,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var me = this; this.set_dynamic_labels(); var company_currency = this.get_company_currency(); - // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc + // Added `ignore_price_list` to determine if document is loading after mapping from another doc if(this.frm.doc.currency && this.frm.doc.currency !== company_currency - && !this.frm.doc.ignore_pricing_rule) { + && !this.frm.doc.__onload.ignore_price_list) { this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency, function(exchange_rate) { @@ -1070,7 +1070,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } if(flt(this.frm.doc.conversion_rate)>0.0) { - if(this.frm.doc.ignore_pricing_rule) { + if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) { this.calculate_taxes_and_totals(); } else if (!this.in_apply_price_list){ this.apply_price_list(); @@ -1144,8 +1144,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.set_dynamic_labels(); var company_currency = this.get_company_currency(); - // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc - if(this.frm.doc.price_list_currency !== company_currency && !this.frm.doc.ignore_pricing_rule) { + // Added `ignore_price_list` to determine if document is loading after mapping from another doc + if(this.frm.doc.price_list_currency !== company_currency && !this.frm.doc.__onload.ignore_price_list) { this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency, function(exchange_rate) { me.frm.set_value("plc_conversion_rate", exchange_rate); @@ -1884,6 +1884,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe callback: function(r) { if(!r.exc) { item.item_tax_rate = r.message; + me.add_taxes_from_item_tax_template(item.item_tax_rate); me.calculate_taxes_and_totals(); } } diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 81ff351d373..f4845459839 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -608,6 +608,11 @@ function check_can_calculate_pending_qty(me) { && doc.fg_completed_qty && erpnext.stock.bom && erpnext.stock.bom.name === doc.bom_no; - const itemChecks = !!item && !item.allow_alternative_item; + const itemChecks = !!item + && !item.allow_alternative_item + && erpnext.stock.bom && erpnext.stock.items + && (item.item_code in erpnext.stock.bom.items); return docChecks && itemChecks; } + +//# sourceURL=serial_no_batch_selector.js diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 019496d295a..6ae464d2c21 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -264,6 +264,15 @@ body.product-page { font-size: 13px; } + .filter-lookup-input { + background-color: white; + border: 1px solid var(--gray-300); + + &:focus { + border: 1px solid var(--primary); + } + } + .filter-label { font-size: 11px; font-weight: 600; diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index d443f9c15c0..55b563e1cd9 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -19,8 +19,9 @@ PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}") def validate_gstin_for_india(doc, method): - if hasattr(doc, 'gst_state') and doc.gst_state: - doc.gst_state_number = state_numbers[doc.gst_state] + if hasattr(doc, 'gst_state'): + set_gst_state_and_state_number(doc) + if not hasattr(doc, 'gstin') or not doc.gstin: return @@ -50,7 +51,6 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN")) validate_gstin_check_digit(doc.gstin) - set_gst_state_and_state_number(doc) if not doc.gst_state: frappe.throw(_("Please enter GST state"), title=_("Invalid State")) @@ -82,17 +82,14 @@ def update_gst_category(doc, method): frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular') def set_gst_state_and_state_number(doc): - if not doc.gst_state: - if not doc.state: - return + if not doc.gst_state and doc.state: state = doc.state.lower() states_lowercase = {s.lower():s for s in states} if state in states_lowercase: doc.gst_state = states_lowercase[state] else: return - - doc.gst_state_number = state_numbers[doc.gst_state] + doc.gst_state_number = state_numbers.get(doc.gst_state) def validate_gstin_check_digit(gstin, label='GSTIN'): ''' Function to validate the check digit of the GSTIN.''' diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 06b4ff18bf4..d602f0ca94b 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -206,6 +206,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): }, target_doc, set_missing_values, ignore_permissions=ignore_permissions) # postprocess: fetch shipping address, set missing values + doclist.set_onload('ignore_price_list', True) return doclist @@ -269,6 +270,8 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): } }, target_doc, set_missing_values, ignore_permissions=ignore_permissions) + doclist.set_onload('ignore_price_list', True) + return doclist def _make_customer(source_name, ignore_permissions=False): diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 73e3d193d02..b906ec06315 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -584,6 +584,8 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values) + target_doc.set_onload('ignore_price_list', True) + return target_doc @frappe.whitelist() @@ -664,6 +666,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if automatically_fetch_payment_terms: doclist.set_payment_schedule() + doclist.set_onload('ignore_price_list', True) + return doclist @frappe.whitelist() diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 993c61d5639..67948d779f5 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -8,7 +8,7 @@ import frappe from frappe.utils.nestedset import get_root_of from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability -from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups +from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups def search_by_term(search_term, warehouse, price_list): @@ -275,3 +275,16 @@ def set_customer_info(fieldname, customer, value=""): contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}]) frappe.db.set_value('Customer', customer, 'mobile_no', value) contact_doc.save() + +@frappe.whitelist() +def get_pos_profile_data(pos_profile): + pos_profile = frappe.get_doc('POS Profile', pos_profile) + pos_profile = pos_profile.as_dict() + + _customer_groups_with_children = [] + for row in pos_profile.customer_groups: + children = get_child_nodes('Customer Group', row.customer_group) + _customer_groups_with_children.extend(children) + + pos_profile.customer_groups = _customer_groups_with_children + return pos_profile \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ea8459f970b..6974bed4f1f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -119,10 +119,15 @@ erpnext.PointOfSale.Controller = class { this.allow_negative_stock = flt(message.allow_negative_stock) || false; }); - frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => { - Object.assign(this.settings, profile); - this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group); - this.make_app(); + frappe.call({ + method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data", + args: { "pos_profile": this.pos_profile }, + callback: (res) => { + const profile = res.message; + Object.assign(this.settings, profile); + this.settings.customer_groups = profile.customer_groups.map(group => group.name); + this.make_app(); + } }); } @@ -555,7 +560,7 @@ erpnext.PointOfSale.Controller = class { if (this.item_details.$component.is(':visible')) this.edit_item_details_of(item_row); - if (this.check_serial_batch_selection_needed(item_row)) + if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible')) this.edit_item_details_of(item_row); } @@ -704,7 +709,7 @@ erpnext.PointOfSale.Controller = class { frappe.dom.freeze(); const { doctype, name, current_item } = this.item_details; - frappe.model.set_value(doctype, name, 'qty', 0) + return frappe.model.set_value(doctype, name, 'qty', 0) .then(() => { frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true); @@ -715,7 +720,14 @@ erpnext.PointOfSale.Controller = class { } async save_and_checkout() { - this.frm.is_dirty() && await this.frm.save(); - this.payment.checkout(); + if (this.frm.is_dirty()) { + // only move to payment section if save is successful + frappe.route_hooks.after_save = () => this.payment.checkout(); + return this.frm.save( + null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error + ); + } else { + this.payment.checkout(); + } } }; diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index fb69b63f82a..b75ffb235ed 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -60,12 +60,18 @@ erpnext.PointOfSale.ItemDetails = class { return item && item.name == this.current_item.name; } - toggle_item_details_section(item) { + async toggle_item_details_section(item) { const current_item_changed = !this.compare_with_current_item(item); // if item is null or highlighted cart item is clicked twice const hide_item_details = !Boolean(item) || !current_item_changed; + if ((!hide_item_details && current_item_changed) || hide_item_details) { + // if item details is being closed OR if item details is opened but item is changed + // in both cases, if the current item is a serialized item, then validate and remove the item + await this.validate_serial_batch_item(); + } + this.events.toggle_item_selector(!hide_item_details); this.toggle_component(!hide_item_details); @@ -83,7 +89,6 @@ erpnext.PointOfSale.ItemDetails = class { this.render_form(item); this.events.highlight_cart_item(item); } else { - this.validate_serial_batch_item(); this.current_item = {}; } } @@ -103,11 +108,11 @@ erpnext.PointOfSale.ItemDetails = class { (serialized && batched && (no_batch_selected || no_serial_selected))) { frappe.show_alert({ - message: __("Item will be removed since no serial / batch no selected."), + message: __("Item is removed since no serial / batch no selected."), indicator: 'orange' }); frappe.utils.play_sound("cancel"); - this.events.remove_item_from_cart(); + return this.events.remove_item_from_cart(); } } diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 1e9f6d7d920..b4ece46e6e1 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -170,20 +170,24 @@ erpnext.PointOfSale.Payment = class { }); frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { - if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); - } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { - frappe.show_alert({ - message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), - indicator: "orange" - }); + if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { + if (!frm.doc.ignore_pricing_rule) { + frm.applying_pos_coupon_code = true; + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc), + () => (frm.applying_pos_coupon_code = false) + ]); + } else if (frm.doc.ignore_pricing_rule) { + frappe.show_alert({ + message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), + indicator: "orange" + }); + } } }); diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js index b71a92f8a98..d86a8f3d984 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.js +++ b/erpnext/setup/doctype/sales_person/sales_person.js @@ -4,8 +4,12 @@ frappe.ui.form.on('Sales Person', { refresh: function(frm) { if(frm.doc.__onload && frm.doc.__onload.dashboard_info) { - var info = frm.doc.__onload.dashboard_info; - frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue'); + let info = frm.doc.__onload.dashboard_info; + frm.dashboard.add_indicator(__('Total Contribution Amount Against Orders: {0}', + [format_currency(info.allocated_amount_against_order, info.currency)]), 'blue'); + + frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}', + [format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue'); } }, diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index b79a566578d..6af1b312bdd 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -28,14 +28,17 @@ class SalesPerson(NestedSet): def load_dashboard_info(self): company_default_currency = get_default_currency() - allocated_amount = frappe.db.sql(""" - select sum(allocated_amount) - from `tabSales Team` - where sales_person = %s and docstatus=1 and parenttype = 'Sales Order' - """,(self.sales_person_name)) + allocated_amount_against_order = flt(frappe.db.get_value('Sales Team', + {'docstatus': 1, 'parenttype': 'Sales Order', 'sales_person': self.sales_person_name}, + 'sum(allocated_amount)')) + + allocated_amount_against_invoice = flt(frappe.db.get_value('Sales Team', + {'docstatus': 1, 'parenttype': 'Sales Invoice', 'sales_person': self.sales_person_name}, + 'sum(allocated_amount)')) info = {} - info["allocated_amount"] = flt(allocated_amount[0][0]) if allocated_amount else 0 + info["allocated_amount_against_order"] = allocated_amount_against_order + info["allocated_amount_against_invoice"] = allocated_amount_against_invoice info["currency"] = company_default_currency self.set_onload('dashboard_info', info) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 1d7bad2686a..1d95ddb2032 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -21,9 +21,7 @@ default_mail_footer = """
Task due today:

\n\n
\n{{ doc.description }}\n
\n\n
\n

\nThis is a notification for a task that is due today, and a sample Notification. In ERPNext you can setup notifications on anything, Invoices, Orders, Leads, Opportunities, so you never miss a thing.\n
To edit this, and setup other alerts, just type Notification in the search bar.

", - "method": null, - "modified": "2017-03-09 07:34:58.168370", - "module": null, - "name": "Task Due Alert", - "recipients": [ - { - "cc": null, - "condition": null, - "email_by_document_field": "owner" - } - ], - "subject": "{{ doc.subject }}", - "value_changed": null - } -] \ No newline at end of file diff --git a/erpnext/setup/setup_wizard/utils.py b/erpnext/setup/setup_wizard/utils.py deleted file mode 100644 index f1ec50afcea..00000000000 --- a/erpnext/setup/setup_wizard/utils.py +++ /dev/null @@ -1,12 +0,0 @@ -import json -import os - -from frappe.desk.page.setup_wizard.setup_wizard import setup_complete - - -def complete(): - with open(os.path.join(os.path.dirname(__file__), - 'data', 'test_mfg.json'), 'r') as f: - data = json.loads(f.read()) - - setup_complete(data) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index a4f2207f113..6db1961045c 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -5,28 +5,17 @@ import frappe from frappe import _ from frappe.utils import add_days, flt, get_datetime_str, nowdate +from frappe.utils.data import now_datetime +from frappe.utils.nestedset import get_ancestors_of, get_root_of # noqa from erpnext import get_default_company -def get_root_of(doctype): - """Get root element of a DocType with a tree structure""" - result = frappe.db.sql_list("""select name from `tab%s` - where lft=1 and rgt=(select max(rgt) from `tab%s` where docstatus < 2)""" % - (doctype, doctype)) - return result[0] if result else None - -def get_ancestors_of(doctype, name): - """Get ancestor elements of a DocType with a tree structure""" - lft, rgt = frappe.db.get_value(doctype, name, ["lft", "rgt"]) - result = frappe.db.sql_list("""select name from `tab%s` - where lft<%s and rgt>%s order by lft desc""" % (doctype, "%s", "%s"), (lft, rgt)) - return result or [] - def before_tests(): frappe.clear_cache() # complete setup if missing from frappe.desk.page.setup_wizard.setup_wizard import setup_complete + current_year = now_datetime().year if not frappe.get_list("Company"): setup_complete({ "currency" :"USD", @@ -36,8 +25,8 @@ def before_tests(): "company_abbr" :"WP", "industry" :"Manufacturing", "country" :"United States", - "fy_start_date" :"2021-01-01", - "fy_end_date" :"2021-12-31", + "fy_start_date" :f"{current_year}-01-01", + "fy_end_date" :f"{current_year}-12-31", "language" :"english", "company_tagline" :"Testing", "email" :"test@erpnext.com", @@ -51,7 +40,6 @@ def before_tests(): frappe.db.sql("delete from `tabSalary Slip`") frappe.db.sql("delete from `tabItem Price`") - frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) enable_all_roles_and_domains() set_defaults_for_tests() @@ -142,13 +130,13 @@ def enable_all_roles_and_domains(): add_all_roles_to('Administrator') def set_defaults_for_tests(): - from frappe.utils.nestedset import get_root_of - selling_settings = frappe.get_single("Selling Settings") selling_settings.customer_group = get_root_of("Customer Group") selling_settings.territory = get_root_of("Territory") selling_settings.save() + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) + def insert_record(records): for r in records: diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index 57d78a21e14..75aa1d36c7b 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -40,18 +40,15 @@ def get_data(item_code=None, warehouse=None, item_group=None, filters=filters, order_by=sort_by + ' ' + sort_order, limit_start=start, - limit_page_length='21') + limit_page_length=21) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item in items: item.update({ - 'item_name': frappe.get_cached_value( - "Item", item.item_code, 'item_name'), - 'disable_quick_entry': frappe.get_cached_value( - "Item", item.item_code, 'has_batch_no') - or frappe.get_cached_value( - "Item", item.item_code, 'has_serial_no'), + 'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'), + 'disable_quick_entry': frappe.get_cached_value( "Item", item.item_code, 'has_batch_no') + or frappe.get_cached_value( "Item", item.item_code, 'has_serial_no'), 'projected_qty': flt(item.projected_qty, precision), 'reserved_qty': flt(item.reserved_qty, precision), 'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision), diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index ffa2f93b961..492f90b3029 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -519,6 +519,8 @@ def make_sales_invoice(source_name, target_doc=None): if automatically_fetch_payment_terms: doc.set_payment_schedule() + doc.set_onload('ignore_price_list', True) + return doc @frappe.whitelist() diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index c7971878506..524c3d14236 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -422,7 +422,6 @@ "fieldname": "has_batch_no", "fieldtype": "Check", "label": "Has Batch No", - "no_copy": 1, "oldfieldname": "has_batch_no", "oldfieldtype": "Select" }, @@ -472,7 +471,6 @@ "fieldname": "has_serial_no", "fieldtype": "Check", "label": "Has Serial No", - "no_copy": 1, "oldfieldname": "has_serial_no", "oldfieldtype": "Select" }, @@ -921,7 +919,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-11 08:07:46.663220", + "modified": "2022-03-25 06:38:55.942304", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -994,4 +992,4 @@ "states": [], "title_field": "item_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 112420ff7b7..05e6e76e21d 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -685,6 +685,12 @@ class TestItem(FrappeTestCase): # standalone return make_purchase_receipt(is_return=True, qty=-1, **typical_args) + def test_item_dashboard(self): + from erpnext.stock.dashboard.item_dashboard import get_data + + self.assertTrue(get_data(item_code="_Test Item")) + self.assertTrue(get_data(warehouse="_Test Warehouse - _TC")) + self.assertTrue(get_data(item_group="All Item Groups")) def set_item_variant_settings(fields): diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js index 5e1f7d53221..058783c4ae3 100644 --- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js +++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js @@ -13,10 +13,16 @@ frappe.ui.form.on('Item Variant Settings', { const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only']; frappe.model.with_doctype('Item', () => { + const field_label_map = {}; frappe.get_meta('Item').fields.forEach(d => { + field_label_map[d.fieldname] = __(d.label) + ` (${d.fieldname})`; + if (!in_list(exclude_field_types, d.fieldtype) && !d.no_copy && !in_list(exclude_fields, d.fieldname)) { - allow_fields.push(d.fieldname); + allow_fields.push({ + label: field_label_map[d.fieldname], + value: d.fieldname, + }); } }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 5bb337ed903..4bf37fee2cf 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -823,6 +823,7 @@ def make_purchase_invoice(source_name, target_doc=None): } }, target_doc, set_missing_values) + doclist.set_onload('ignore_price_list', True) return doclist def get_invoiced_qty_map(purchase_receipt): diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index 4e8d71fe5e4..0ec812c655a 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -396,6 +396,21 @@ class TestPutawayRule(FrappeTestCase): rule_1.delete() rule_2.delete() + def test_warehouse_capacity_dashbord(self): + from erpnext.stock.dashboard.warehouse_capacity_dashboard import get_data + + item = "_Rice" + rule = create_putaway_rule(item_code=item, warehouse=self.warehouse_1, capacity=500, + uom="Kg") + + capacities = get_data(warehouse=self.warehouse_1) + for capacity in capacities: + if capacity.item_code == item and capacity.warehouse == self.warehouse_1: + self.assertEqual(capacity.stock_capacity, 500) + + get_data(warehouse=self.warehouse_1) + rule.delete() + def create_putaway_rule(**args): args = frappe._dict(args) putaway = frappe.new_doc("Putaway Rule") diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index c77c6c3ba95..2808c219ea8 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -3,6 +3,7 @@ import json +from typing import List, Optional, Union import frappe from frappe import ValidationError, _ @@ -574,22 +575,30 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note): return serial_nos @frappe.whitelist() -def auto_fetch_serial_number(qty, item_code, warehouse, - posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None): +def auto_fetch_serial_number( + qty: float, + item_code: str, + warehouse: str, + posting_date: Optional[str] = None, + batch_nos: Optional[Union[str, List[str]]] = None, + for_doctype: Optional[str] = None, + exclude_sr_nos: Optional[List[str]] = None + ) -> List[str]: filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) if exclude_sr_nos is None: exclude_sr_nos = [] else: + exclude_sr_nos = safe_json_loads(exclude_sr_nos) exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos))) if batch_nos: batch_nos = safe_json_loads(batch_nos) if isinstance(batch_nos, list): filters.batch_no = batch_nos - elif isinstance(batch_nos, str): - filters.batch_no = [batch_nos] + else: + filters.batch_no = [str(batch_nos)] if posting_date: filters.expiry_date = posting_date @@ -602,25 +611,60 @@ def auto_fetch_serial_number(qty, item_code, warehouse, return sorted([d.get('name') for d in serial_numbers]) +def get_delivered_serial_nos(serial_nos): + ''' + Returns serial numbers that delivered from the list of serial numbers + ''' + from frappe.query_builder.functions import Coalesce + + SerialNo = frappe.qb.DocType("Serial No") + serial_nos = get_serial_nos(serial_nos) + query = frappe.qb.select(SerialNo.name).from_(SerialNo).where( + (SerialNo.name.isin(serial_nos)) + & (Coalesce(SerialNo.delivery_document_type, "") != "") + ) + + result = query.run() + if result and len(result) > 0: + delivered_serial_nos = [row[0] for row in result] + return delivered_serial_nos + @frappe.whitelist() def get_pos_reserved_serial_nos(filters): if isinstance(filters, str): filters = json.loads(filters) - pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no - from `tabPOS Invoice` p, `tabPOS Invoice Item` item - where p.name = item.parent - and p.consolidated_invoice is NULL - and p.docstatus = 1 - and item.docstatus = 1 - and item.item_code = %(item_code)s - and item.warehouse = %(warehouse)s - and item.serial_no is NOT NULL and item.serial_no != '' - """, filters, as_dict=1) + POSInvoice = frappe.qb.DocType("POS Invoice") + POSInvoiceItem = frappe.qb.DocType("POS Invoice Item") + query = frappe.qb.from_( + POSInvoice + ).from_( + POSInvoiceItem + ).select( + POSInvoice.is_return, + POSInvoiceItem.serial_no + ).where( + (POSInvoice.name == POSInvoiceItem.parent) + & (POSInvoice.docstatus == 1) + & (POSInvoiceItem.docstatus == 1) + & (POSInvoiceItem.item_code == filters.get('item_code')) + & (POSInvoiceItem.warehouse == filters.get('warehouse')) + & (POSInvoiceItem.serial_no.isnotnull()) + & (POSInvoiceItem.serial_no != '') + ) + + pos_transacted_sr_nos = query.run(as_dict=True) reserved_sr_nos = [] + returned_sr_nos = [] for d in pos_transacted_sr_nos: - reserved_sr_nos += get_serial_nos(d.serial_no) + if d.is_return == 0: + reserved_sr_nos += get_serial_nos(d.serial_no) + elif d.is_return == 1: + returned_sr_nos += get_serial_nos(d.serial_no) + + for sr_no in returned_sr_nos: + reserved_sr_nos.remove(sr_no) return reserved_sr_nos diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index cca63078402..7df0a56b7f3 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -274,7 +274,8 @@ class TestSerialNo(FrappeTestCase): msg=f"{partial_fetch} should be subset of {first_fetch}") # exclusion - remaining = auto_fetch_serial_number(3, item_code, warehouse, exclude_sr_nos=partial_fetch) + remaining = auto_fetch_serial_number(3, item_code, warehouse, + exclude_sr_nos=json.dumps(partial_fetch)) self.assertEqual(sorted(remaining + partial_fetch), first_fetch) # batchwise diff --git a/erpnext/stock/doctype/variant_field/variant_field.json b/erpnext/stock/doctype/variant_field/variant_field.json index ae9088486f4..5a7da15433f 100644 --- a/erpnext/stock/doctype/variant_field/variant_field.json +++ b/erpnext/stock/doctype/variant_field/variant_field.json @@ -1,72 +1,32 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-08-29 16:33:33.978574", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2022-02-11 11:26:20.611960", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "field_name" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "field_name", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Field Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "field_name", + "fieldtype": "Autocomplete", + "in_list_view": 1, + "label": "Field Name", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-08-29 17:19:20.353197", - "modified_by": "Administrator", - "module": "Stock", - "name": "Variant Field", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2022-03-25 05:48:30.946201", + "modified_by": "Administrator", + "module": "Stock", + "name": "Variant Field", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index e099cdde6aa..956c3c51e6e 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -52,24 +52,6 @@
- diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 47413077371..fb4cecf8266 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -300,13 +300,13 @@ {% if values | len > 20 %} - + {% endif %} {% if values %}
{% for value in values %} -
+