diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 3186d07adcc..2c831cf7c57 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -453,7 +453,10 @@ frappe.ui.form.on("Journal Entry Account", { } }, cost_center: function (frm, dt, dn) { - erpnext.journal_entry.set_account_details(frm, dt, dn); + // Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0' + if (frm.doc.voucher_type != "Exchange Gain Or Loss") { + erpnext.journal_entry.set_account_details(frm, dt, dn); + } }, account: function (frm, dt, dn) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 7c2e14ea751..a37a6fe5465 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -579,6 +579,7 @@ "fieldtype": "Select", "hidden": 1, "label": "Payment Order Status", + "no_copy": 1, "options": "Initiated\nPayment Ordered", "read_only": 1 }, @@ -821,4 +822,4 @@ "states": [], "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 6ed6ca22e63..b8c7271ae4e 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -671,7 +671,7 @@ class ReceivablePayableReport(object): else: future_amount_field = "future_amount_in_base_currency" - if row.remaining_balance > 0 and future.get(future_amount_field): + if row.remaining_balance != 0 and future.get(future_amount_field): if future.get(future_amount_field) > row.outstanding: row.future_amount = row.outstanding future[future_amount_field] = future.get(future_amount_field) - row.outstanding diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index de49139adc1..01129824bca 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -469,11 +469,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): ) def test_future_payments(self): + sr = self.create_sales_invoice(do_not_submit=True) + sr.is_return = 1 + sr.items[0].qty = -1 + sr.items[0].rate = 10 + sr.calculate_taxes_and_totals() + sr.submit() + si = self.create_sales_invoice() pe = get_payment_entry(si.doctype, si.name) + pe.append( + "references", + { + "reference_doctype": sr.doctype, + "reference_name": sr.name, + "due_date": sr.due_date, + "total_amount": sr.grand_total, + "outstanding_amount": sr.outstanding_amount, + "allocated_amount": sr.outstanding_amount, + }, + ) + pe.posting_date = add_days(today(), 1) - pe.paid_amount = 90.0 - pe.references[0].allocated_amount = 90.0 + pe.paid_amount = 80 + pe.references[0].allocated_amount = 90.0 # pe.paid_amount + sr.grand_total pe.save().submit() filters = { "company": self.company, @@ -485,16 +504,21 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "show_future_payments": True, } report = execute(filters)[1] - self.assertEqual(len(report), 1) + self.assertEqual(len(report), 2) - expected_data = [100.0, 100.0, 10.0, 90.0] + expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]} - row = report[0] - self.assertEqual( - expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] - ) + rows = report[:2] + for row in rows: + self.assertEqual( + expected_data[row.voucher_no], + [row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount], + ) pe.cancel() + sr.load_from_db() # Outstanding amount is updated so a updated timestamp is needed. + sr.cancel() + # full payment in future date pe = get_payment_entry(si.doctype, si.name) pe.posting_date = add_days(today(), 1) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 9f89078de67..4a8cd8bf9e6 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -462,7 +462,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Markdown Editor", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -928,7 +928,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:03:59.069145", + "modified": "2024-03-28 10:20:30.231915", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index b716f7f0427..4e67eee2da6 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -71,7 +71,7 @@ class SupplierQuotation(BuyingController): naming_series: DF.Literal["PUR-SQTN-.YYYY.-"] net_total: DF.Currency opportunity: DF.Link | None - other_charges_calculation: DF.MarkdownEditor | None + other_charges_calculation: DF.TextEditor | None plc_conversion_rate: DF.Float price_list_currency: DF.Link | None pricing_rules: DF.Table[PricingRuleDetail] diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 747b4e061b6..c1b3a6fcedf 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -750,12 +750,12 @@ def get_serial_and_batch_bundle(child, parent): "item_code": child.item_code, "warehouse": child.warehouse, "voucher_type": parent.doctype, - "voucher_no": parent.name, + "voucher_no": parent.name if parent.docstatus < 2 else None, "voucher_detail_no": child.name, "posting_date": parent.posting_date, "posting_time": parent.posting_time, "qty": child.qty, - "type_of_transaction": "Outward" if child.qty > 0 else "Inward", + "type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward", "company": parent.company, "do_not_submit": "True", } diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 1104204d856..80f5108944e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -913,7 +913,7 @@ class StockController(AccountsController): self.validate_multi_currency() self.validate_packed_items() - if self.get("is_internal_supplier"): + if self.get("is_internal_supplier") and self.docstatus == 1: self.validate_internal_transfer_qty() else: self.validate_internal_transfer_warehouse() diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 50838736816..4ce8342af6e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -238,7 +238,7 @@ "fieldname": "rm_cost_as_per", "fieldtype": "Select", "label": "Rate Of Materials Based On", - "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual" + "options": "Valuation Rate\nLast Purchase Rate\nPrice List" }, { "allow_on_submit": 1, @@ -637,7 +637,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-12-26 19:34:08.159312", + "modified": "2024-04-02 16:22:47.518411", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", @@ -676,4 +676,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8c4574ed90a..d544272e437 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -150,7 +150,7 @@ class BOM(WebsiteGenerator): quality_inspection_template: DF.Link | None quantity: DF.Float raw_material_cost: DF.Currency - rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"] + rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"] route: DF.SmallText | None routing: DF.Link | None scrap_items: DF.Table[BOMScrapItem] @@ -742,6 +742,7 @@ class BOM(WebsiteGenerator): def calculate_rm_cost(self, save=False): """Fetch RM rate as per today's valuation rate and calculate totals""" + total_rm_cost = 0 base_total_rm_cost = 0 @@ -750,7 +751,7 @@ class BOM(WebsiteGenerator): continue old_rate = d.rate - if self.rm_cost_as_per != "Manual": + if not self.bom_creator: d.rate = self.get_rm_rate( { "company": self.company, @@ -1022,8 +1023,6 @@ def get_bom_item_rate(args, bom_doc): item_doc = frappe.get_cached_doc("Item", args.get("item_code")) price_list_data = get_price_list_rate(bom_args, item_doc) rate = price_list_data.price_list_rate - elif bom_doc.rm_cost_as_per == "Manual": - return return flt(rate) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json index fb4c6c5c95a..9d2384e31a0 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -66,7 +66,7 @@ "fieldname": "rm_cost_as_per", "fieldtype": "Select", "label": "Rate Of Materials Based On", - "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual", + "options": "Valuation Rate\nLast Purchase Rate\nPrice List", "reqd": 1 }, { @@ -288,7 +288,7 @@ "link_fieldname": "bom_creator" } ], - "modified": "2023-08-07 15:45:06.176313", + "modified": "2024-04-02 16:30:59.779190", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator", @@ -327,4 +327,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 1709a1f71af..d2f0c29fe63 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -59,7 +59,7 @@ class BOMCreator(Document): qty: DF.Float raw_material_cost: DF.Currency remarks: DF.TextEditor | None - rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"] + rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"] set_rate_based_on_warehouse: DF.Check status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"] uom: DF.Link | None @@ -143,9 +143,6 @@ class BOMCreator(Document): self.submit() def set_rate_for_items(self): - if self.rm_cost_as_per == "Manual": - return - amount = self.get_raw_material_cost() self.raw_material_cost = amount @@ -240,6 +237,9 @@ class BOMCreator(Document): (row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}) ) + if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)): + frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item)) + production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row) reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items()))) @@ -283,7 +283,6 @@ class BOMCreator(Document): "allow_alternative_item": 1, "bom_creator": self.name, "bom_creator_item": bom_creator_item, - "rm_cost_as_per": "Manual", } ) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 656550aaafc..abfb4062af3 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -145,6 +145,7 @@ class Project(Document): is_group=task_details.is_group, color=task_details.color, template_task=task_details.name, + priority=task_details.priority, ) ).insert() diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index e49fecd1f47..b8340ffe193 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -23,7 +23,11 @@ class TestProject(FrappeTestCase): task1 = task_exists("Test Template Task with No Parent and Dependency") if not task1: task1 = create_task( - subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3 + subject="Test Template Task with No Parent and Dependency", + is_template=1, + begin=5, + duration=3, + priority="High", ) template = make_project_template( @@ -32,11 +36,12 @@ class TestProject(FrappeTestCase): project = get_project(project_name, template) tasks = frappe.get_all( "Task", - ["subject", "exp_end_date", "depends_on_tasks"], + ["subject", "exp_end_date", "depends_on_tasks", "priority"], dict(project=project.name), order_by="creation asc", ) + self.assertEqual(tasks[0].priority, "High") self.assertEqual(tasks[0].subject, "Test Template Task with No Parent and Dependency") self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3)) self.assertEqual(len(tasks), 1) diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index c0333f8f590..ea7d6edcdf9 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -122,6 +122,7 @@ def create_task( begin=0, duration=0, save=True, + priority=None, ): if not frappe.db.exists("Task", subject): task = frappe.new_doc("Task") @@ -139,6 +140,7 @@ def create_task( task.duration = duration task.is_group = is_group task.parent_task = parent_task + task.priority = priority if save: task.save() else: diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 1e94c0032ab..b5a8b757706 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -342,7 +342,6 @@ erpnext.buying = { add_serial_batch_bundle(doc, cdt, cdn) { let item = locals[cdt][cdn]; let me = this; - let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) .then((r) => { @@ -352,30 +351,28 @@ erpnext.buying = { item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; item.is_rejected = false; - frappe.require(path, function() { - new erpnext.SerialBatchPackageSelector( - me.frm, item, (r) => { - if (r) { - let qty = Math.abs(r.total_qty); - if (doc.is_return) { - qty = qty * -1; - } - - let update_values = { - "serial_and_batch_bundle": r.name, - "use_serial_batch_fields": 0, - "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) - } - - if (r.warehouse) { - update_values["warehouse"] = r.warehouse; - } - - frappe.model.set_value(item.doctype, item.name, update_values); + new erpnext.SerialBatchPackageSelector( + me.frm, item, (r) => { + if (r) { + let qty = Math.abs(r.total_qty); + if (doc.is_return) { + qty = qty * -1; } + + let update_values = { + "serial_and_batch_bundle": r.name, + "use_serial_batch_fields": 0, + "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) + } + + if (r.warehouse) { + update_values["warehouse"] = r.warehouse; + } + + frappe.model.set_value(item.doctype, item.name, update_values); } - ); - }); + } + ); } }); } @@ -383,40 +380,37 @@ erpnext.buying = { add_serial_batch_for_rejected_qty(doc, cdt, cdn) { let item = locals[cdt][cdn]; let me = this; - let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) .then((r) => { if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { item.has_serial_no = r.message.has_serial_no; item.has_batch_no = r.message.has_batch_no; - item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; + item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward"; item.is_rejected = true; - frappe.require(path, function() { - new erpnext.SerialBatchPackageSelector( - me.frm, item, (r) => { - if (r) { - let qty = Math.abs(r.total_qty); - if (doc.is_return) { - qty = qty * -1; - } - - let update_values = { - "serial_and_batch_bundle": r.name, - "use_serial_batch_fields": 0, - "rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) - } - - if (r.warehouse) { - update_values["rejected_warehouse"] = r.warehouse; - } - - frappe.model.set_value(item.doctype, item.name, update_values); + new erpnext.SerialBatchPackageSelector( + me.frm, item, (r) => { + if (r) { + let qty = Math.abs(r.total_qty); + if (doc.is_return) { + qty = qty * -1; } + + let update_values = { + "serial_and_batch_bundle": r.name, + "use_serial_batch_fields": 0, + "rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) + } + + if (r.warehouse) { + update_values["rejected_warehouse"] = r.warehouse; + } + + frappe.model.set_value(item.doctype, item.name, update_values); } - ); - }); + } + ); } }); } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 53edceee627..24e85295f4b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -415,7 +415,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let row = locals[cdt][cdn]; if (row.barcode) { erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => { - debugger frappe.model.set_value(cdt, cdn, { "item_code": r.message.item_code, "qty": 1, @@ -2499,27 +2498,25 @@ erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close } } - frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() { - if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) { - item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward"; - } else { - item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward"; - } + if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) { + item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward"; + } else { + item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward"; + } - new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { - if (r) { - let update_values = { - "serial_and_batch_bundle": r.name, - "qty": Math.abs(r.total_qty) - } - - if (r.warehouse) { - update_values[warehouse_field] = r.warehouse; - } - - frappe.model.set_value(item_row.doctype, item_row.name, update_values); + new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { + if (r) { + let update_values = { + "serial_and_batch_bundle": r.name, + "qty": Math.abs(r.total_qty) } - }); + + if (r.warehouse) { + update_values[warehouse_field] = r.warehouse; + } + + frappe.model.set_value(item_row.doctype, item_row.name, update_values); + } }); } diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index b75a21bfc97..527d452a450 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -4,6 +4,7 @@ import "./queries"; import "./sms_manager"; import "./utils/party"; import "./controllers/stock_controller"; +import "./utils/serial_no_batch_selector"; import "./payment/payments"; import "./templates/visual_plant_floor_template.html"; import "./plant_floor_visual/visual_plant"; diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 03bf648b60d..7bf8b9bed6d 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -430,25 +430,23 @@ $.extend(erpnext.utils, { item_row.has_batch_no = r.message.has_batch_no; item_row.has_serial_no = r.message.has_serial_no; - frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function () { - new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { - if (r) { - let update_values = { - serial_and_batch_bundle: r.name, - qty: Math.abs(r.total_qty), - }; + new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { + if (r) { + let update_values = { + serial_and_batch_bundle: r.name, + qty: Math.abs(r.total_qty), + }; - if (!warehouse_field) { - warehouse_field = "warehouse"; - } - - if (r.warehouse) { - update_values[warehouse_field] = r.warehouse; - } - - frappe.model.set_value(item_row.doctype, item_row.name, update_values); + if (!warehouse_field) { + warehouse_field = "warehouse"; } - }); + + if (r.warehouse) { + update_values[warehouse_field] = r.warehouse; + } + + frappe.model.set_value(item_row.doctype, item_row.name, update_values); + } }); }); }, diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 00df1c5c191..5bc4ffe1bff 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -350,7 +350,6 @@ erpnext.sales_common = { pick_serial_and_batch(doc, cdt, cdn) { let item = locals[cdt][cdn]; let me = this; - let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => { if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { @@ -364,26 +363,24 @@ erpnext.sales_common = { item.title = __("Select Serial and Batch"); } - frappe.require(path, function () { - new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => { - if (r) { - let qty = Math.abs(r.total_qty); - if (doc.is_return) { - qty = qty * -1; - } - - frappe.model.set_value(item.doctype, item.name, { - serial_and_batch_bundle: r.name, - use_serial_batch_fields: 0, - qty: - qty / - flt( - item.conversion_factor || 1, - precision("conversion_factor", item) - ), - }); + new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => { + if (r) { + let qty = Math.abs(r.total_qty); + if (doc.is_return) { + qty = qty * -1; } - }); + + frappe.model.set_value(item.doctype, item.name, { + serial_and_batch_bundle: r.name, + use_serial_batch_fields: 0, + qty: + qty / + flt( + item.conversion_factor || 1, + precision("conversion_factor", item) + ), + }); + } }); } }); 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 9646a04e400..f89b70ef45b 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -394,19 +394,17 @@ erpnext.PointOfSale.ItemDetails = class { bind_auto_serial_fetch_event() { this.$form_container.on("click", ".auto-fetch-btn", () => { - frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => { - let frm = this.events.get_frm(); - let item_row = this.item_row; - item_row.type_of_transaction = "Outward"; + let frm = this.events.get_frm(); + let item_row = this.item_row; + item_row.type_of_transaction = "Outward"; - new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { - if (r) { - frappe.model.set_value(item_row.doctype, item_row.name, { - serial_and_batch_bundle: r.name, - qty: Math.abs(r.total_qty), - }); - } - }); + new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { + if (r) { + frappe.model.set_value(item_row.doctype, item_row.name, { + serial_and_batch_bundle: r.name, + qty: Math.abs(r.total_qty), + }); + } }); }); } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index fb19d0ec20b..4b8c1e9d2c6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1287,6 +1287,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")): target.append("taxes", tax) + if not target.get("items"): + frappe.throw(_("All items have already been received")) + def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name if target_doc.doctype == "Purchase Receipt": @@ -1342,6 +1345,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): shipping_address_name=target_doc.shipping_address_name, ) + def update_item(source, target, source_parent): + if source_parent.doctype == "Delivery Note" and source.received_qty: + target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty) + doclist = get_mapped_doc( doctype, source_name, @@ -1363,6 +1370,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "Material_request_item": "material_request_item", }, "field_no_map": ["warehouse"], + "condition": lambda item: item.received_qty < item.qty + item.returned_qty, + "postprocess": update_item, }, }, target_doc, diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index b237f73026b..b369b2e37f6 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -32,7 +32,7 @@ test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] -def make_item(item_code=None, properties=None, uoms=None): +def make_item(item_code=None, properties=None, uoms=None, barcode=None): if not item_code: item_code = frappe.generate_hash(length=16) @@ -61,6 +61,14 @@ def make_item(item_code=None, properties=None, uoms=None): for uom in uoms: item.append("uoms", uom) + if barcode: + item.append( + "barcodes", + { + "barcode": barcode, + }, + ) + item.insert() return item diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 43519e76467..518782d759b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -355,19 +355,15 @@ frappe.ui.form.on("Pick List Item", { item.title = __("Select Serial and Batch"); } - frappe.require(path, function () { - new erpnext.SerialBatchPackageSelector(frm, item, (r) => { - if (r) { - let qty = Math.abs(r.total_qty); - frappe.model.set_value(item.doctype, item.name, { - serial_and_batch_bundle: r.name, - use_serial_batch_fields: 0, - qty: - qty / - flt(item.conversion_factor || 1, precision("conversion_factor", item)), - }); - } - }); + new erpnext.SerialBatchPackageSelector(frm, item, (r) => { + if (r) { + let qty = Math.abs(r.total_qty); + frappe.model.set_value(item.doctype, item.name, { + serial_and_batch_bundle: r.name, + use_serial_batch_fields: 0, + qty: qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)), + }); + } }); } }); diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 0c474342a97..0e466e44d1b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -18,6 +18,7 @@ "parent_warehouse", "consider_rejected_warehouses", "get_item_locations", + "pick_manually", "section_break_6", "scan_barcode", "column_break_13", @@ -192,11 +193,18 @@ "fieldname": "consider_rejected_warehouses", "fieldtype": "Check", "label": "Consider Rejected Warehouses" + }, + { + "default": "0", + "description": "If enabled then system won't override the picked qty / batches / serial numbers.", + "fieldname": "pick_manually", + "fieldtype": "Check", + "label": "Pick Manually" } ], "is_submittable": 1, "links": [], - "modified": "2024-02-02 16:17:44.877426", + "modified": "2024-03-27 22:49:16.954637", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 4eab7e8a0cf..e6fb984785d 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -42,6 +42,7 @@ class PickList(Document): amended_from: DF.Link | None company: DF.Link + consider_rejected_warehouses: DF.Check customer: DF.Link | None customer_name: DF.Data | None for_qty: DF.Float @@ -50,6 +51,7 @@ class PickList(Document): material_request: DF.Link | None naming_series: DF.Literal["STO-PICK-.YYYY.-"] parent_warehouse: DF.Link | None + pick_manually: DF.Check prompt_qty: DF.Check purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"] scan_barcode: DF.Data | None @@ -71,7 +73,8 @@ class PickList(Document): def before_save(self): self.update_status() - self.set_item_locations() + if not self.pick_manually: + self.set_item_locations() if self.get("locations"): self.validate_sales_order_percentage() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 8348804ae57..9b929f9f1b8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1335,18 +1335,16 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { item.has_batch_no = r.message.has_batch_no; item.type_of_transaction = item.s_warehouse ? "Outward" : "Inward"; - frappe.require(path, function () { - new erpnext.SerialBatchPackageSelector(frm, item, (r) => { - if (r) { - frappe.model.set_value(item.doctype, item.name, { - serial_and_batch_bundle: r.name, - use_serial_batch_fields: 0, - qty: - Math.abs(r.total_qty) / - flt(item.conversion_factor || 1, precision("conversion_factor", item)), - }); - } - }); + new erpnext.SerialBatchPackageSelector(frm, item, (r) => { + if (r) { + frappe.model.set_value(item.doctype, item.name, { + serial_and_batch_bundle: r.name, + use_serial_batch_fields: 0, + qty: + Math.abs(r.total_qty) / + flt(item.conversion_factor || 1, precision("conversion_factor", item)), + }); + } }); } }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e6214290fda..3c19eb99350 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -31,6 +31,7 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( OpeningEntryAccountError, ) from erpnext.stock.get_item_details import ( + get_barcode_data, get_bin_details, get_conversion_factor, get_default_cost_center, @@ -428,7 +429,14 @@ class StockEntry(StockController): for field in reset_fields: item.set(field, item_details.get(field)) - update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor") + update_fields = ( + "uom", + "description", + "expense_account", + "cost_center", + "conversion_factor", + "barcode", + ) for field in update_fields: if not item.get(field): @@ -1609,6 +1617,10 @@ class StockEntry(StockController): if subcontract_items and len(subcontract_items) == 1: ret["subcontracted_item"] = subcontract_items[0].main_item_code + barcode_data = get_barcode_data(item_code=item.name) + if barcode_data and len(barcode_data.get(item.name)) == 1: + ret["barcode"] = barcode_data.get(item.name)[0] + return ret @frappe.whitelist() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index ce3c4958cec..1fde55f8af4 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -98,6 +98,12 @@ class TestStockEntry(FrappeTestCase): self._test_auto_material_request("_Test Item") self._test_auto_material_request("_Test Item", material_request_type="Transfer") + def test_barcode_item_stock_entry(self): + item_code = make_item("_Test Item Stock Entry For Barcode", barcode="BDD-1234567890") + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + self.assertEqual(se.items[0].barcode, "BDD-1234567890") + def test_auto_material_request_for_variant(self): fields = [{"field_name": "reorder_levels"}] set_item_variant_settings(fields) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 4a85360235a..8532b60d59c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -81,6 +81,18 @@ frappe.ui.form.on("Stock Reconciliation", { if (frm.doc.company) { frm.trigger("toggle_display_account_head"); } + + frm.events.set_fields_onload_for_line_item(frm); + }, + + set_fields_onload_for_line_item(frm) { + if (frm.is_new() && frm.doc?.items && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) { + frm.doc.items.forEach((item) => { + if (!item.serial_and_batch_bundle) { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + } + }); + } }, scan_barcode: function (frm) { @@ -155,6 +167,9 @@ frappe.ui.form.on("Stock Reconciliation", { item.qty = item.qty || 0; item.valuation_rate = item.valuation_rate || 0; + item.use_serial_batch_fields = cint( + frappe.user_defaults?.use_serial_batch_fields + ); }); frm.refresh_field("items"); }, @@ -298,6 +313,10 @@ frappe.ui.form.on("Stock Reconciliation Item", { if (!item.warehouse && frm.doc.set_warehouse) { frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.set_warehouse); } + + if (item.docstatus === 0 && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + } }, add_serial_batch_bundle(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 195ecb646a2..96cac9c06b2 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -49,6 +49,7 @@ frappe.ui.form.on("Warehouse", { frm.add_custom_button(__("Stock Balance"), function () { frappe.set_route("query-report", "Stock Balance", { warehouse: frm.doc.name, + company: frm.doc.company, }); }); diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e13d7a354ad..f01f170b90a 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -499,12 +499,21 @@ def update_barcode_value(out): out["barcode"] = barcode_data.get(out.item_code)[0] -def get_barcode_data(items_list): +def get_barcode_data(items_list=None, item_code=None): # get item-wise batch no data # example: {'LED-GRE': [Batch001, Batch002]} # where LED-GRE is item code, SN0001 is serial no and Pune is warehouse itemwise_barcode = {} + if not items_list and item_code: + _dict_item_code = frappe._dict( + { + "item_code": item_code, + } + ) + + items_list = [frappe._dict(_dict_item_code)] + for item in items_list: barcodes = frappe.db.get_all( "Item Barcode", filters={"parent": item.item_code}, fields="barcode" diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index d0929a082ce..c4156e7e64e 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -2,8 +2,8 @@ # License: GNU General Public License v3. See license.txt +from collections.abc import Iterator from operator import itemgetter -from typing import Dict, List, Tuple, Union import frappe from frappe import _ @@ -14,7 +14,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict -def execute(filters: Filters = None) -> Tuple: +def execute(filters: Filters = None) -> tuple: to_date = filters["to_date"] columns = get_columns(filters) @@ -26,14 +26,14 @@ def execute(filters: Filters = None) -> Tuple: return columns, data, None, chart_data -def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]: +def format_report_data(filters: Filters, item_details: dict, to_date: str) -> list[dict]: "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) - for item, item_dict in item_details.items(): + for _item, item_dict in item_details.items(): if not flt(item_dict.get("total_qty"), precision): continue @@ -74,12 +74,12 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li return data -def get_average_age(fifo_queue: List, to_date: str) -> float: +def get_average_age(fifo_queue: list, to_date: str) -> float: batch_age = age_qty = total_qty = 0.0 for batch in fifo_queue: batch_age = date_diff(to_date, batch[1]) - if isinstance(batch[0], (int, float)): + if isinstance(batch[0], int | float): age_qty += batch_age * batch[0] total_qty += batch[0] else: @@ -89,8 +89,7 @@ def get_average_age(fifo_queue: List, to_date: str) -> float: return flt(age_qty / total_qty, 2) if total_qty else 0.0 -def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: - +def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> tuple: precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) range1 = range2 = range3 = above_range3 = 0.0 @@ -111,7 +110,7 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D return range1, range2, range3, above_range3 -def get_columns(filters: Filters) -> List[Dict]: +def get_columns(filters: Filters) -> list[dict]: range_columns = [] setup_ageing_columns(filters, range_columns) columns = [ @@ -169,7 +168,7 @@ def get_columns(filters: Filters) -> List[Dict]: return columns -def get_chart_data(data: List, filters: Filters) -> Dict: +def get_chart_data(data: list, filters: Filters) -> dict: if not data: return [] @@ -193,7 +192,7 @@ def get_chart_data(data: List, filters: Filters) -> Dict: } -def setup_ageing_columns(filters: Filters, range_columns: List): +def setup_ageing_columns(filters: Filters, range_columns: list): ranges = [ f"0 - {filters['range1']}", f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}", @@ -205,23 +204,21 @@ def setup_ageing_columns(filters: Filters, range_columns: List): add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname) -def add_column( - range_columns: List, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140 -): +def add_column(range_columns: list, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140): range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width)) class FIFOSlots: "Returns FIFO computed slots of inwarded stock as per date." - def __init__(self, filters: Dict = None, sle: List = None): + def __init__(self, filters: dict | None = None, sle: list | None = None): self.item_details = {} self.transferred_item_details = {} self.serial_no_batch_purchase_details = {} self.filters = filters self.sle = sle - def generate(self) -> Dict: + def generate(self) -> dict: """ Returns dict of the foll.g structure: Key = Item A / (Item A, Warehouse A) @@ -231,25 +228,45 @@ class FIFOSlots: consumed/updated and maintained via FIFO. ** } """ - if self.sle is None: - self.sle = self.__get_stock_ledger_entries() - for d in self.sle: - key, fifo_queue, transferred_item_key = self.__init_key_stores(d) + from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_serial_nos_from_bundle, + ) - if d.voucher_type == "Stock Reconciliation": - # get difference in qty shift as actual qty - prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) - d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) + stock_ledger_entries = self.sle - serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] + bundle_wise_serial_nos = frappe._dict({}) + if stock_ledger_entries is None: + bundle_wise_serial_nos = self.__get_bundle_wise_serial_nos() - if d.actual_qty > 0: - self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) - else: - self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos) + with frappe.db.unbuffered_cursor(): + if stock_ledger_entries is None: + stock_ledger_entries = self.__get_stock_ledger_entries() - self.__update_balances(d, key) + for d in stock_ledger_entries: + key, fifo_queue, transferred_item_key = self.__init_key_stores(d) + + if d.voucher_type == "Stock Reconciliation": + # get difference in qty shift as actual qty + prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) + d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) + + serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] + if d.serial_and_batch_bundle and d.has_serial_no: + if bundle_wise_serial_nos: + serial_nos = bundle_wise_serial_nos.get(d.serial_and_batch_bundle) or [] + else: + serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) or [] + + if d.actual_qty > 0: + self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) + else: + self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos) + + self.__update_balances(d, key) + + # Note that stock_ledger_entries is an iterator, you can not reuse it like a list + del stock_ledger_entries if not self.filters.get("show_warehouse_wise_stock"): # (Item 1, WH 1), (Item 1, WH 2) => (Item 1) @@ -257,7 +274,7 @@ class FIFOSlots: return self.item_details - def __init_key_stores(self, row: Dict) -> Tuple: + def __init_key_stores(self, row: dict) -> tuple: "Initialise keys and FIFO Queue." key = (row.name, row.warehouse) @@ -269,9 +286,7 @@ class FIFOSlots: return key, fifo_queue, transferred_item_key - def __compute_incoming_stock( - self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List - ): + def __compute_incoming_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list): "Update FIFO Queue on inward stock." transfer_data = self.transferred_item_details.get(transfer_key) @@ -297,9 +312,7 @@ class FIFOSlots: self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date) fifo_queue.append([serial_no, row.posting_date]) - def __compute_outgoing_stock( - self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List - ): + def __compute_outgoing_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list): "Update FIFO Queue on outward stock." if serial_nos: fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos] @@ -325,7 +338,7 @@ class FIFOSlots: self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]]) qty_to_pop = 0 - def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): + def __adjust_incoming_transfer_qty(self, transfer_data: dict, fifo_queue: list, row: dict): "Add previously removed stock back to FIFO Queue." transfer_qty_to_pop = flt(row.actual_qty) @@ -352,7 +365,7 @@ class FIFOSlots: add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]]) transfer_qty_to_pop = 0 - def __update_balances(self, row: Dict, key: Union[Tuple, str]): + def __update_balances(self, row: dict, key: tuple | str): self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction if "total_qty" not in self.item_details[key]: @@ -362,7 +375,7 @@ class FIFOSlots: self.item_details[key]["has_serial_no"] = row.has_serial_no - def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict: + def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict: "Aggregate Item-Wh wise data into single Item entry." item_aggregated_data = {} for key, row in wh_wise_data.items(): @@ -370,7 +383,12 @@ class FIFOSlots: if not item_aggregated_data.get(item): item_aggregated_data.setdefault( item, - {"details": frappe._dict(), "fifo_queue": [], "qty_after_transaction": 0.0, "total_qty": 0.0}, + { + "details": frappe._dict(), + "fifo_queue": [], + "qty_after_transaction": 0.0, + "total_qty": 0.0, + }, ) item_row = item_aggregated_data.get(item) item_row["details"].update(row["details"]) @@ -381,7 +399,7 @@ class FIFOSlots: return item_aggregated_data - def __get_stock_ledger_entries(self) -> List[Dict]: + def __get_stock_ledger_entries(self) -> Iterator[dict]: sle = frappe.qb.DocType("Stock Ledger Entry") item = self.__get_item_query() # used as derived table in sle query @@ -403,6 +421,7 @@ class FIFOSlots: sle.serial_no, sle.batch_no, sle.qty_after_transaction, + sle.serial_and_batch_bundle, sle.warehouse, ) .where( @@ -418,7 +437,34 @@ class FIFOSlots: sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty) - return sle_query.run(as_dict=True) + return sle_query.run(as_dict=True, as_iterator=True) + + def __get_bundle_wise_serial_nos(self) -> dict: + bundle = frappe.qb.DocType("Serial and Batch Bundle") + entry = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(bundle) + .join(entry) + .on(bundle.name == entry.parent) + .select(bundle.name, entry.serial_no) + .where( + (bundle.docstatus == 1) + & (entry.serial_no.isnotnull()) + & (bundle.company == self.filters.get("company")) + & (bundle.posting_date <= self.filters.get("to_date")) + ) + ) + + for field in ["item_code", "warehouse"]: + if self.filters.get(field): + query = query.where(bundle[field] == self.filters.get(field)) + + bundle_wise_serial_nos = frappe._dict({}) + for bundle_name, serial_no in query.run(): + bundle_wise_serial_nos.setdefault(bundle_name, []).append(serial_no) + + return bundle_wise_serial_nos def __get_item_query(self) -> str: item_table = frappe.qb.DocType("Item") diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 500affa51e2..5f85a672ea0 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -295,6 +295,8 @@ class StockBalanceReport(object): sle.stock_value, sle.batch_no, sle.serial_no, + sle.serial_and_batch_bundle, + sle.has_serial_no, item_table.item_group, item_table.stock_uom, item_table.item_name, diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 9625b20da41..fb63b0181d3 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -856,6 +856,11 @@ class SerialBatchCreation: if not doc.get("entries"): return frappe._dict({}) + if ( + doc.voucher_no and frappe.get_cached_value(doc.voucher_type, doc.voucher_no, "docstatus") == 2 + ): + doc.voucher_no = "" + doc.save() self.validate_qty(doc) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index aeff2f6d58f..d407d9c82d7 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -335,12 +335,115 @@ frappe.ui.form.on("Subcontracting Receipt Item", { items_remove: (frm) => { set_missing_values(frm); }, + + add_serial_batch_bundle(frm, cdt, cdn) { + let item = locals[cdt][cdn]; + + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; + item.is_rejected = false; + + new erpnext.SerialBatchPackageSelector(frm, item, (r) => { + if (r) { + let qty = Math.abs(r.total_qty); + if (frm.doc.is_return) { + qty = qty * -1; + } + + let update_values = { + serial_and_batch_bundle: r.name, + use_serial_batch_fields: 0, + qty: qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)), + }; + + if (r.warehouse) { + update_values["warehouse"] = r.warehouse; + } + + frappe.model.set_value(item.doctype, item.name, update_values); + } + }); + } + }); + }, + + add_serial_batch_for_rejected_qty(frm, cdt, cdn) { + let item = locals[cdt][cdn]; + + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward"; + item.is_rejected = true; + + new erpnext.SerialBatchPackageSelector(frm, item, (r) => { + if (r) { + let qty = Math.abs(r.total_qty); + if (frm.doc.is_return) { + qty = qty * -1; + } + + let update_values = { + serial_and_batch_bundle: r.name, + use_serial_batch_fields: 0, + rejected_qty: + qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)), + }; + + if (r.warehouse) { + update_values["rejected_warehouse"] = r.warehouse; + } + + frappe.model.set_value(item.doctype, item.name, update_values); + } + }); + } + }); + }, }); frappe.ui.form.on("Subcontracting Receipt Supplied Item", { consumed_qty(frm) { set_missing_values(frm); }, + + add_serial_batch_bundle(frm, cdt, cdn) { + let item = locals[cdt][cdn]; + + item.item_code = item.rm_item_code; + item.qty = item.consumed_qty; + item.warehouse = frm.doc.supplier_warehouse; + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.type_of_transaction = item.qty > 0 ? "Outward" : "Inward"; + item.is_rejected = false; + + new erpnext.SerialBatchPackageSelector(frm, item, (r) => { + if (r) { + let qty = Math.abs(r.total_qty); + if (frm.doc.is_return) { + qty = qty * -1; + } + + let update_values = { + serial_and_batch_bundle: r.name, + use_serial_batch_fields: 0, + consumed_qty: + qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)), + }; + + frappe.model.set_value(item.doctype, item.name, update_values); + } + }); + } + }); + }, }); let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => { diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index ca14f09c9c5..75e263e2c1c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -47,9 +47,11 @@ "schedule_date", "reference_name", "section_break_45", + "add_serial_batch_bundle", "serial_and_batch_bundle", "use_serial_batch_fields", "col_break5", + "add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle", "section_break_jshh", "serial_no", @@ -563,12 +565,24 @@ { "fieldname": "column_break_henr", "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 0", + "fieldname": "add_serial_batch_bundle", + "fieldtype": "Button", + "label": "Add Serial / Batch Bundle" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 0", + "fieldname": "add_serial_batch_for_rejected_qty", + "fieldtype": "Button", + "label": "Add Serial / Batch No (Rejected Qty)" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-07 11:43:38.954262", + "modified": "2024-03-29 15:42:43.425544", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", @@ -579,4 +593,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index 957b6a2a654..e1927a5467a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -25,6 +25,7 @@ "consumed_qty", "current_stock", "secbreak_3", + "add_serial_batch_bundle", "serial_and_batch_bundle", "use_serial_batch_fields", "col_break4", @@ -224,12 +225,18 @@ { "fieldname": "column_break_qibi", "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 0", + "fieldname": "add_serial_batch_bundle", + "fieldtype": "Button", + "label": "Add Serial / Batch Bundle" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-02-04 16:32:17.534162", + "modified": "2024-03-30 10:26:27.237371", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item", @@ -240,4 +247,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/support/web_form/issues/issues.json b/erpnext/support/web_form/issues/issues.json index 1df9fb79696..f3834c73a7c 100644 --- a/erpnext/support/web_form/issues/issues.json +++ b/erpnext/support/web_form/issues/issues.json @@ -1,14 +1,14 @@ { - "accept_payment": 0, "allow_comments": 1, "allow_delete": 1, "allow_edit": 1, "allow_incomplete": 0, "allow_multiple": 1, "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 0, + "anonymous": 0, + "apply_document_permissions": 0, "breadcrumbs": "[{\"label\":_(\"Issues\"), \"route\":\"issues\"}]", + "condition_json": "[]", "creation": "2016-06-24 15:50:33.186483", "doc_type": "Issue", "docstatus": 0, @@ -16,20 +16,19 @@ "idx": 0, "introduction_text": "", "is_standard": 1, + "list_columns": [], "login_required": 1, "max_attachment_size": 0, - "modified": "2020-05-19 13:01:10.729088", + "modified": "2024-03-27 16:16:03.621730", "modified_by": "Administrator", "module": "Support", "name": "issues", "owner": "Administrator", "published": 1, "route": "issues", - "route_to_success_link": 0, "show_attachments": 0, - "show_in_grid": 0, + "show_list": 1, "show_sidebar": 1, - "sidebar_items": [], "success_message": "", "success_url": "/issues", "title": "Issue",