diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0186b3b9366..da746e26d02 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1761,6 +1761,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/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5e4a6d619cb..fcdf8474427 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -812,12 +812,37 @@ class TestSalesInvoice(unittest.TestCase): pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50}) pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60}) - pos.change_amount = 5.0 + pos.write_off_outstanding_amount_automatically = 1 pos.insert() pos.submit() self.assertEqual(pos.grand_total, 100.0) - self.assertEqual(pos.write_off_amount, -5) + self.assertEqual(pos.write_off_amount, 0) + + def test_auto_write_off_amount(self): + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") + + make_purchase_receipt(company= "_Test Company with perpetual inventory", + item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1") + + pos = create_sales_invoice(company= "_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", + income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", do_not_save=True) + + pos.is_pos = 1 + pos.update_stock = 1 + + pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 40}) + + pos.write_off_outstanding_amount_automatically = 1 + pos.insert() + pos.submit() + + self.assertEqual(pos.grand_total, 100.0) + self.assertEqual(pos.write_off_amount, 10) def test_pos_with_no_gl_entry_for_change_amount(self): frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0) diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index 20f7fcfb1b3..e81e0d73cdb 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.query_builder.functions import Sum -from frappe.utils import add_to_date, get_date_str +from frappe.utils import add_to_date, flt, get_date_str from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( @@ -442,8 +442,8 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate else: gl_sum = 0 - total += gl_sum - data.setdefault(period["key"], gl_sum) + total += flt(gl_sum) + data.setdefault(period["key"], flt(gl_sum)) data["total"] = total return data 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 34a4f26a087..9b9c5d51680 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 f5c566023c6..ea1c90c250d 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 7d4ef587526..5b72d14dda5 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 40833b9300f..f926a893147 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', @@ -584,7 +580,11 @@ class calculate_taxes_and_totals(object): .format(self.doc.party_account_currency, invoice_total)) if self.doc.docstatus == 0: + if self.doc.get('write_off_outstanding_amount_automatically'): + self.doc.write_off_amount = 0 + self.calculate_outstanding_amount() + self.calculate_write_off_amount() def is_internal_invoice(self): """ @@ -625,7 +625,6 @@ class calculate_taxes_and_totals(object): change_amount = 0 if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'): - self.calculate_write_off_amount() self.calculate_change_amount() change_amount = self.doc.change_amount \ if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount @@ -675,19 +674,20 @@ class calculate_taxes_and_totals(object): and self.doc.paid_amount > grand_total and not self.doc.is_return \ and any(d.type == "Cash" for d in self.doc.payments): - self.doc.change_amount = flt(self.doc.paid_amount - grand_total + - self.doc.write_off_amount, self.doc.precision("change_amount")) + self.doc.change_amount = flt(self.doc.paid_amount - grand_total, + self.doc.precision("change_amount")) - self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total + - self.doc.base_write_off_amount, self.doc.precision("base_change_amount")) + self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total, + self.doc.precision("base_change_amount")) def calculate_write_off_amount(self): - if flt(self.doc.change_amount) > 0: - self.doc.write_off_amount = flt(self.doc.grand_total - self.doc.paid_amount - + self.doc.change_amount, self.doc.precision("write_off_amount")) + if self.doc.get('write_off_outstanding_amount_automatically'): + self.doc.write_off_amount = flt(self.doc.outstanding_amount, self.doc.precision("write_off_amount")) self.doc.base_write_off_amount = flt(self.doc.write_off_amount * self.doc.conversion_rate, self.doc.precision("base_write_off_amount")) + self.calculate_outstanding_amount() + def calculate_margin(self, item): rate_with_margin = 0.0 base_rate_with_margin = 0.0 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 cc51718c47f..eab1699538e 100644 --- a/erpnext/e_commerce/product_ui/views.js +++ b/erpnext/e_commerce/product_ui/views.js @@ -424,6 +424,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/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 5f2e720eb46..2744de96ec3 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import nowdate +from frappe.utils import flt, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee @@ -13,6 +13,7 @@ from erpnext.hr.doctype.employee_advance.employee_advance import ( create_return_through_additional_salary, make_bank_entry, ) +from erpnext.hr.doctype.expense_claim.expense_claim import get_advances from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure @@ -118,3 +119,24 @@ def make_employee_advance(employee_name, args=None): doc.submit() return doc + + +def get_advances_for_claim(claim, advance_name, amount=None): + advances = get_advances(claim.employee, advance_name) + + for entry in advances: + if amount: + allocated_amount = amount + else: + allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount) + + claim.append("advances", { + "employee_advance": entry.name, + "posting_date": entry.posting_date, + "advance_account": entry.advance_account, + "advance_paid": entry.paid_amount, + "unclaimed_amount": allocated_amount, + "allocated_amount": allocated_amount + }) + + return claim \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 7e3898b7d51..c1bf9c2d984 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -23,10 +23,10 @@ class ExpenseClaim(AccountsController): def validate(self): validate_active_employee(self.employee) - self.validate_advances() + set_employee_name(self) self.validate_sanctioned_amount() self.calculate_total_amount() - set_employee_name(self) + self.validate_advances() self.set_expense_account(validate=True) self.set_payable_account() self.set_cost_center() @@ -42,10 +42,18 @@ class ExpenseClaim(AccountsController): "2": "Cancelled" }[cstr(self.docstatus or 0)] - paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) precision = self.precision("grand_total") - if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 - and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved': + + if ( + # set as paid + self.is_paid + or (flt(self.total_sanctioned_amount > 0) and ( + # grand total is reimbursed + (self.docstatus == 1 and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision)) + # grand total (to be paid) is 0 since linked advances already cover the claimed amount + or (flt(self.grand_total, precision) == 0) + )) + ) and self.approval_status == "Approved": status = "Paid" elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': status = "Unpaid" diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 46958b1ec4c..01b74fb24b4 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -72,6 +72,72 @@ class TestExpenseClaim(unittest.TestCase): expense_claim = frappe.get_doc("Expense Claim", expense_claim.name) self.assertEqual(expense_claim.status, "Unpaid") + # expense claim without any sanctioned amount should not have status as Paid + claim = make_expense_claim(payable_account, 1000, 0, "_Test Company", "Travel Expenses - _TC") + self.assertEqual(claim.total_sanctioned_amount, 0) + self.assertEqual(claim.status, "Submitted") + + # no gl entries created + gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': claim.name}) + self.assertEqual(len(gl_entry), 0) + + def test_expense_claim_against_fully_paid_advances(self): + from erpnext.hr.doctype.employee_advance.test_employee_advance import ( + get_advances_for_claim, + make_employee_advance, + make_payment_entry, + ) + + frappe.db.delete("Employee Advance") + + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + # claim for already paid out advances + claim = get_advances_for_claim(claim, advance.name) + claim.save() + claim.submit() + + self.assertEqual(claim.grand_total, 0) + self.assertEqual(claim.status, "Paid") + + def test_expense_claim_partially_paid_via_advance(self): + from erpnext.hr.doctype.employee_advance.test_employee_advance import ( + get_advances_for_claim, + make_employee_advance, + ) + from erpnext.hr.doctype.employee_advance.test_employee_advance import ( + make_payment_entry as make_advance_payment, + ) + + frappe.db.delete("Employee Advance") + + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + + # link advance for partial amount + advance = make_employee_advance(claim.employee, {'advance_amount': 500}) + pe = make_advance_payment(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name) + claim.save() + claim.submit() + + self.assertEqual(claim.grand_total, 500) + self.assertEqual(claim.status, "Unpaid") + + # reimburse remaning amount + make_payment_entry(claim, payable_account, 500) + claim.reload() + + self.assertEqual(claim.total_amount_reimbursed, 500) + self.assertEqual(claim.status, "Paid") + def test_expense_claim_gl_entry(self): payable_account = get_payable_account(company_name) taxes = generate_taxes() diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index af633e776cd..4f7fcee30fe 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, ) @@ -881,6 +882,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 @@ -902,6 +924,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", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index fae09b320a4..b5e16dd3c69 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 f3ded994814..5653e1be75d 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.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index acc1b0017d0..23b32379413 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -351,7 +351,6 @@ "hide_border": 1 }, { - "depends_on": "get_items_from", "fieldname": "sub_assembly_items", "fieldtype": "Table", "label": "Sub Assembly Items", @@ -379,7 +378,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-14 03:56:23.209247", + "modified": "2022-03-25 09:15:25.017664", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 93e9c94da53..a262b91cb5f 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 d8e43db62cf..dae16e4bd30 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -617,6 +617,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/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 28226290e2f..ed7f843271b 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -352,7 +352,6 @@ class TestWorkOrder(FrappeTestCase): wo_order = make_wo_order_test_record(planned_start_date=now(), sales_order=so.name, qty=3) - wo_order.submit() self.assertEqual(wo_order.docstatus, 1) allow_overproduction("overproduction_percentage_for_sales_order", 0) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 542fc489b25..ee12597d24f 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: @@ -640,9 +641,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) @@ -1146,6 +1151,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 2385630c6bd..50050afd316 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,6 +1,6 @@ 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 @@ -352,7 +352,8 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs -erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items +erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.rename_non_profit_fields erpnext.patches.v13_0.enable_ksa_vat_docs #1 -erpnext.patches.v13_0.create_gst_custom_fields_in_quotation \ No newline at end of file +erpnext.patches.v13_0.create_gst_custom_fields_in_quotation +erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances 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/update_expense_claim_status_for_paid_advances.py b/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py new file mode 100644 index 00000000000..063de1637d0 --- /dev/null +++ b/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + """ + Update Expense Claim status to Paid if: + - the entire required amount is already covered via linked advances + - the claim is partially paid via advances and the rest is reimbursed + """ + + ExpenseClaim = frappe.qb.DocType('Expense Claim') + + (frappe.qb + .update(ExpenseClaim) + .set(ExpenseClaim.status, 'Paid') + .where( + ((ExpenseClaim.grand_total == 0) | (ExpenseClaim.grand_total == ExpenseClaim.total_amount_reimbursed)) + & (ExpenseClaim.approval_status == 'Approved') + & (ExpenseClaim.docstatus == 1) + & (ExpenseClaim.total_sanctioned_amount > 0) + ) + ).run() diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 0dc694f5a8b..579c7b2f504 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -660,6 +660,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 4e5f00de600..c989965ac59 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() @@ -413,15 +415,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/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8068879810f..1ecf3d0c3fd 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -278,26 +278,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, - add_taxes_from_item_tax_template: function(item_tax_map) { - let me = this; - - if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) { - if (typeof (item_tax_map) == "string") { - item_tax_map = JSON.parse(item_tax_map); - } - - $.each(item_tax_map, function(tax, rate) { - let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax); - if (!found) { - let child = frappe.model.add_child(me.frm.doc, "taxes"); - child.charge_type = "On Net Total"; - child.account_head = tax; - child.rate = 0; - } - }); - } - }, - calculate_taxes: function() { var me = this; this.frm.doc.rounding_adjustment = 0; @@ -687,7 +667,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ })); this.frm.doc.total_advance = flt(total_allocated_amount, precision("total_advance")); + if (this.frm.doc.write_off_outstanding_amount_automatically) { + this.frm.doc.write_off_amount = 0; + } + this.calculate_outstanding_amount(update_paid_amount); + this.calculate_write_off_amount(); }, is_internal_invoice: function() { @@ -812,7 +797,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.frm.set_value('base_paid_amount', flt(base_paid_amount, precision("base_paid_amount"))); }, - calculate_change_amount: function(){ + calculate_change_amount: function() { this.frm.doc.change_amount = 0.0; this.frm.doc.base_change_amount = 0.0; if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) @@ -823,26 +808,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; var base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; - this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total + - this.frm.doc.write_off_amount, precision("change_amount")); + this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total, + precision("change_amount")); this.frm.doc.base_change_amount = flt(this.frm.doc.base_paid_amount - - base_grand_total + this.frm.doc.base_write_off_amount, - precision("base_change_amount")); + base_grand_total, precision("base_change_amount")); } } }, - calculate_write_off_amount: function(){ - if(this.frm.doc.paid_amount > this.frm.doc.grand_total){ - this.frm.doc.write_off_amount = flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - + this.frm.doc.change_amount, precision("write_off_amount")); - + calculate_write_off_amount: function() { + if (this.frm.doc.write_off_outstanding_amount_automatically) { + this.frm.doc.write_off_amount = flt(this.frm.doc.outstanding_amount, precision("write_off_amount")); this.frm.doc.base_write_off_amount = flt(this.frm.doc.write_off_amount * this.frm.doc.conversion_rate, precision("base_write_off_amount")); - }else{ - this.frm.doc.paid_amount = 0.0; + + this.calculate_outstanding_amount(false); } - this.calculate_outstanding_amount(false); + } }); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a89776250f2..23932d117f0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -736,6 +736,26 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }); }, + add_taxes_from_item_tax_template: function(item_tax_map) { + let me = this; + + if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) { + if (typeof (item_tax_map) == "string") { + item_tax_map = JSON.parse(item_tax_map); + } + + $.each(item_tax_map, function(tax, rate) { + let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax); + if (!found) { + let child = frappe.model.add_child(me.frm.doc, "taxes"); + child.charge_type = "On Net Total"; + child.account_head = tax; + child.rate = 0; + } + }); + } + }, + serial_no: function(doc, cdt, cdn) { var me = this; var item = frappe.get_doc(cdt, cdn); @@ -1021,9 +1041,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ 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 && this.frm.doc.__onload.ignore_price_list)) { this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency, function(exchange_rate) { @@ -1049,7 +1069,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } 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(); @@ -1123,8 +1143,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ 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 && 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); @@ -1863,6 +1884,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ 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 26e559f672b..cbecb11949b 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -606,6 +606,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 ff2482d3f64..843bd86baac 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -4,6 +4,7 @@ --green-info: #38A160; --product-bg-color: white; --body-bg-color: var(--gray-50); + --text-md: 13px; // variables are in desk folder in frappe for v13, this is a temporary fix } body.product-page { @@ -264,6 +265,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 134c272e253..12ad9f1c937 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -21,8 +21,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 @@ -52,7 +53,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")) @@ -84,17 +84,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 ed8d93e7962..bf87ba46fc8 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -192,6 +192,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 @@ -255,6 +256,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 6f329e037fa..7809a9330ed 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -630,6 +630,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() @@ -710,6 +712,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/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 93d2357cfa9..7b1489c40d2 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/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 32ffba392a9..2a6b4ea34b4 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -785,6 +785,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/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index f97ebb97140..22d84e9cb7d 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, _ @@ -591,22 +592,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 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/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 218481581c5..3926f77b1fa 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 892d62513e2..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 %}