diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index df54a63b661..fba60cef632 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -683,6 +683,19 @@ class PurchaseInvoice(BuyingController): where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""", } ) + self.status_updater.append( + { + "source_dt": "Purchase Invoice Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "received_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_received", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + "percent_join_field": "material_request", + } + ) if cint(self.is_return): self.status_updater.append( { diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 3ea88f195d8..3c3b081fa2f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -17,6 +17,8 @@ from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.exceptions import InvalidCurrency from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.material_request.material_request import make_purchase_order +from erpnext.stock.doctype.material_request.test_material_request import make_material_request from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as create_purchase_invoice_from_receipt, ) @@ -72,6 +74,31 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): # teardown pi.delete() + def test_update_received_qty_in_material_request(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice + + """ + Test if the received_qty in Material Request is updated correctly when + a Purchase Invoice with update_stock=True is submitted. + """ + mr = make_material_request(item_code="_Test Item", qty=10) + mr.save() + mr.submit() + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.save() + po.submit() + + # Create a Purchase Invoice with update_stock=True + pi = make_purchase_invoice(po.name) + pi.update_stock = True + pi.insert() + pi.submit() + + # Check if the received quantity is updated in Material Request + mr.reload() + self.assertEqual(mr.items[0].received_qty, 10) + def test_gl_entries_without_perpetual_inventory(self): frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") pi = frappe.copy_doc(test_records[0]) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 66df76a3af0..1d7b0c2f461 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -105,6 +105,8 @@ "purchase_receipt", "pr_detail", "sales_invoice_item", + "material_request", + "material_request_item", "item_weight_details", "weight_per_unit", "total_weight", @@ -934,12 +936,34 @@ { "fieldname": "column_break_vbbb", "fieldtype": "Column Break" + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "no_copy": 1, + "options": "Material Request", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Material Request Item", + "no_copy": 1, + "oldfieldname": "pr_detail", + "oldfieldtype": "Data", + "print_hide": 1, + "read_only": 1, + "search_index": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-19 19:09:47.210965", + "modified": "2024-06-14 11:57:07.171700", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -949,4 +973,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py index ccbc34749d7..baeece4815c 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py @@ -52,6 +52,8 @@ class PurchaseInvoiceItem(Document): manufacturer_part_no: DF.Data | None margin_rate_or_amount: DF.Float margin_type: DF.Literal["", "Percentage", "Amount"] + material_request: DF.Link | None + material_request_item: DF.Data | None net_amount: DF.Currency net_rate: DF.Currency page_break: DF.Check diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 80c246cad55..1cd9b872a9b 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -289,7 +289,7 @@ def get_columns(additional_table_columns, filters): def apply_conditions(query, pi, pii, filters): - for opts in ("company", "supplier", "item_code", "mode_of_payment"): + for opts in ("company", "supplier", "mode_of_payment"): if filters.get(opts): query = query.where(pi[opts] == filters[opts]) @@ -299,6 +299,9 @@ def apply_conditions(query, pi, pii, filters): if filters.get("to_date"): query = query.where(pi.posting_date <= filters.get("to_date")) + if filters.get("item_code"): + query = query.where(pii.item_code == filters.get("item_code")) + if filters.get("item_group"): query = query.where(pii.item_group == filters.get("item_group")) @@ -322,7 +325,7 @@ def get_items(filters, additional_table_columns): .left_join(Item) .on(pii.item_code == Item.name) .select( - pii.name.as_("pii_name"), + pii.name, pii.parent, pi.posting_date, pi.credit_to, diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js index 1f155de63a0..16a97733393 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js @@ -54,6 +54,12 @@ frappe.query_reports["Item-wise Sales Register"] = { fieldtype: "Link", options: "Brand", }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + }, { fieldname: "item_group", label: __("Item Group"), diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index cd50b118715..cf08e45c537 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -342,7 +342,7 @@ def get_columns(additional_table_columns, filters): def apply_conditions(query, si, sii, filters, additional_conditions=None): - for opts in ("company", "customer", "item_code"): + for opts in ("company", "customer"): if filters.get(opts): query = query.where(si[opts] == filters[opts]) @@ -371,6 +371,9 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None): if filters.get("brand"): query = query.where(sii.brand == filters.get("brand")) + if filters.get("item_code"): + query = query.where(sii.item_code == filters.get("item_code")) + if filters.get("item_group"): query = query.where(sii.item_group == filters.get("item_group")) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index f27569531b1..6c0bf91e3f8 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -80,6 +80,7 @@ def _execute(filters, additional_table_columns=None): delivery_note = list(set(invoice_so_dn_map.get(inv.name, {}).get("delivery_note", []))) cost_center = list(set(invoice_cc_wh_map.get(inv.name, {}).get("cost_center", []))) warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", []))) + inv_customer_details = customer_details.get(inv.customer, {}) row = { "voucher_type": inv.doctype, @@ -88,9 +89,9 @@ def _execute(filters, additional_table_columns=None): "customer": inv.customer, "customer_name": inv.customer_name, **get_values_for_columns(additional_table_columns, inv), - "customer_group": customer_details.get(inv.customer).get("customer_group"), - "territory": customer_details.get(inv.customer).get("territory"), - "tax_id": customer_details.get(inv.customer).get("tax_id"), + "customer_group": inv_customer_details.get("customer_group"), + "territory": inv_customer_details.get("territory"), + "tax_id": inv_customer_details.get("tax_id"), "receivable_account": inv.debit_to, "mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])), "project": inv.project, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index cf383021b06..bd92ebef3d7 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller(); frappe.ui.form.on("Purchase Order", { setup: function (frm) { + frm.ignore_doctypes_on_cancel_all = ["Unreconcile Payment", "Unreconcile Payment Entries"]; if (frm.doc.is_old_subcontracting_flow) { frm.set_query("reserve_warehouse", "supplied_items", function () { return { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index d1f19841ac5..0508483a0fc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -484,7 +484,13 @@ class PurchaseOrder(BuyingController): self.auto_create_subcontracting_order() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry") + self.ignore_linked_doctypes = ( + "GL Entry", + "Payment Ledger Entry", + "Unreconcile Payment", + "Unreconcile Payment Entries", + ) + super().on_cancel() if self.is_against_so(): @@ -794,6 +800,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "field_map": { "name": "po_detail", "parent": "purchase_order", + "material_request": "material_request", + "material_request_item": "material_request_item", "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 14bf8ad7436..bf6e3cd663a 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -8,6 +8,7 @@ from frappe.contacts.doctype.address.address import render_address from frappe.utils import cint, flt, getdate from frappe.utils.data import nowtime +import erpnext from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.party import get_party_details from erpnext.buying.utils import update_last_purchase_rate, validate_for_items @@ -332,6 +333,8 @@ class BuyingController(SubcontractingController): else: item.valuation_rate = 0.0 + update_regional_item_valuation_rate(self) + def set_incoming_rate(self): if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"): return @@ -935,3 +938,8 @@ def validate_item_type(doc, fieldname, message): ).format(items, message) frappe.throw(error_message) + + +@erpnext.allow_regional +def update_regional_item_valuation_rate(doc): + pass diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 3414b5b2520..b61d1773a67 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -883,6 +883,9 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"): key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field) + if row.voucher_type in ["Sales Invoice", "Delivery Note"]: + row.qty = -1 * row.qty + if key not in available_dict: available_dict[key] = frappe._dict( {"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)} diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 4f579fd500e..212c9adf4af 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -97,7 +97,7 @@ class StockController(AccountsController): ) ) - def make_gl_entries(self, gl_entries=None, from_repost=False): + def make_gl_entries(self, gl_entries=None, from_repost=False, via_landed_cost_voucher=False): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -118,7 +118,11 @@ class StockController(AccountsController): if self.docstatus == 1: if not gl_entries: - gl_entries = self.get_gl_entries(warehouse_account) + gl_entries = ( + self.get_gl_entries(warehouse_account, via_landed_cost_voucher) + if self.doctype == "Purchase Receipt" + else self.get_gl_entries(warehouse_account) + ) make_gl_entries(gl_entries, from_repost=from_repost) def validate_serialized_batch(self): diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 5a734d8684f..037a3fe0e81 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -77,7 +77,7 @@ "fieldname": "time_in_mins", "fieldtype": "Float", "in_list_view": 1, - "label": "Operation Time ", + "label": "Operation Time", "oldfieldname": "time_in_mins", "oldfieldtype": "Currency", "reqd": 1 @@ -203,4 +203,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 6db901c71a4..5b4ef233926 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -400,13 +400,20 @@ frappe.ui.form.on("Production Plan", { }, download_materials_required(frm) { + const warehouses_data = []; + + if (frm.doc.for_warehouse) { + warehouses_data.push({ warehouse: frm.doc.for_warehouse }); + } + const fields = [ { fieldname: "warehouses", fieldtype: "Table MultiSelect", label: __("Warehouses"), - default: frm.doc.from_warehouse, + default: warehouses_data, options: "Production Plan Material Request Warehouse", + reqd: 1, get_query: function () { return { filters: { diff --git a/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py b/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py index ec72527552c..d740a5b9c8e 100644 --- a/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py +++ b/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py @@ -9,15 +9,13 @@ def execute(): dt = frappe.qb.DocType(doctype) records = ( - frappe.qb.from_(dt) - .select(dt.name, dt.notes, dt.modified_by, dt.modified) - .where(dt.notes.isnotnull() & dt.notes != "") + frappe.qb.from_(dt).select(dt.name, dt.notes).where(dt.notes.isnotnull() & dt.notes != "") ).run(as_dict=True) for d in records: if strip_html(cstr(d.notes)).strip(): doc = frappe.get_doc(doctype, d.name) - doc.append("notes", {"note": d.notes, "added_by": d.modified_by, "added_on": d.modified}) + doc.append("notes", {"note": d.notes}) doc.update_child_table("notes") frappe.db.sql_ddl(f"alter table `tab{doctype}` drop column `notes`") diff --git a/erpnext/public/js/templates/crm_notes.html b/erpnext/public/js/templates/crm_notes.html index 53df9330784..a20e6c2723c 100644 --- a/erpnext/public/js/templates/crm_notes.html +++ b/erpnext/public/js/templates/crm_notes.html @@ -12,6 +12,7 @@ {% for(var i=0, l=notes.length; i
+ {% if (notes[i].added_by && notes[i].added_on) %}
{{ frappe.avatar(notes[i].added_by) }} @@ -25,6 +26,7 @@
+ {% } %}
{{ notes[i].note }} diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index fcb541f71a8..a7d88edcafa 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -933,7 +933,13 @@ erpnext.utils.map_current_doc = function (opts) { frappe.msgprint(__("Please select {0}", [opts.source_doctype])); return; } - opts.source_name = values; + + if (values.constructor === Array) { + opts.source_name = [...new Set(values)]; + } else { + opts.source_name = values; + } + if ( opts.allow_child_item_selection || ["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index d903f7eedf5..5885d092cce 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -217,7 +217,11 @@ frappe.ui.form.on("Sales Order", { frm.set_value("advance_paid", 0); } - frm.ignore_doctypes_on_cancel_all = ["Purchase Order"]; + frm.ignore_doctypes_on_cancel_all = [ + "Purchase Order", + "Unreconcile Payment", + "Unreconcile Payment Entries", + ]; }, delivery_date: function (frm) { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e5e67ed38b7..af67f07a360 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -421,7 +421,13 @@ class SalesOrder(SellingController): self.create_stock_reservation_entries() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Payment Ledger Entry", + "Unreconcile Payment", + "Unreconcile Payment Entries", + ) super().on_cancel() # Cannot cancel closed SO diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index fbee9c16267..694f70d4db5 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -857,7 +857,7 @@ erpnext.PointOfSale.ItemCart = class { }); this.$customer_section.find(".customer-details").html( `
-
Contact Details
+
${__("Contact Details")}
@@ -877,7 +877,7 @@ erpnext.PointOfSale.ItemCart = class {
-
Recent Transactions
` +
${__("Recent Transactions")}
` ); // transactions need to be in diff div from sticky elem for scrolling this.$customer_section.append(`
`); diff --git a/erpnext/setup/doctype/department/department.json b/erpnext/setup/doctype/department/department.json index 99deca5c19d..fa6b9ad4a55 100644 --- a/erpnext/setup/doctype/department/department.json +++ b/erpnext/setup/doctype/department/department.json @@ -90,7 +90,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2023-08-28 17:26:46.826501", + "modified": "2024-06-12 16:10:31.451257", "modified_by": "Administrator", "module": "Setup", "name": "Department", @@ -132,6 +132,10 @@ "role": "HR Manager", "share": 1, "write": 1 + }, + { + "role": "Employee", + "select": 1 } ], "show_name_in_global_search": 1, diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 5ebcc795fa0..f24c912e362 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1814,6 +1814,110 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(sle_data.actual_qty, 1 * -1) self.assertEqual(sle_data.stock_value_difference, 200.0 * -1) + def test_sales_return_batch_no_for_batched_item_in_dn(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_code = make_item( + "Test Batched Item for Sales Return 11", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "B11-TESTBATCH.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + use_serial_batch_fields=0, + batch_no=batch_no, + ) + + dn_return = make_sales_return(dn.name) + dn_return.save().submit() + returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, returned_batch_no) + + def test_partial_sales_return_batch_no_for_batched_item_in_dn(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_code = make_item( + "Test Partial Batched Item for Sales Return 11", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BPART11-TESTBATCH.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + use_serial_batch_fields=0, + batch_no=batch_no, + ) + + dn_return = make_sales_return(dn.name) + dn_return.items[0].qty = 3 * -1 + dn_return.save().submit() + + returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, returned_batch_no) + sabb_qty = frappe.db.get_value( + "Serial and Batch Bundle", dn_return.items[0].serial_and_batch_bundle, "total_qty" + ) + self.assertEqual(sabb_qty, 3) + + dn_return = make_sales_return(dn.name) + dn_return.items[0].qty = 2 * -1 + dn_return.save().submit() + + returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, returned_batch_no) + + sabb_qty = frappe.db.get_value( + "Serial and Batch Bundle", dn_return.items[0].serial_and_batch_bundle, "total_qty" + ) + self.assertEqual(sabb_qty, 2) + + def test_sales_return_serial_no_for_serial_item_in_dn(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_code = make_item( + "Test Serial Item for Sales Return 11", + properties={ + "has_serial_no": 1, + "serial_no_series": "SNN11-TESTBATCH.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + use_serial_batch_fields=0, + serial_no=serial_nos, + ) + + dn_return = make_sales_return(dn.name) + dn_return.save().submit() + returned_serial_nos = get_serial_nos_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(serial_nos, returned_serial_nos) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 683e946298a..eb84fdbc7c0 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -252,7 +252,10 @@ class LandedCostVoucher(Document): doc.docstatus = 1 doc.make_bundle_using_old_serial_batch_fields(via_landed_cost_voucher=True) doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) - doc.make_gl_entries() + if d.receipt_document_type == "Purchase Receipt": + doc.make_gl_entries(via_landed_cost_voucher=True) + else: + doc.make_gl_entries() doc.repost_future_sle_and_gle(via_landed_cost_voucher=True) def validate_asset_qty_and_status(self, receipt_document_type, receipt_document): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0742ba3b590..6a00fed5588 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -422,13 +422,13 @@ class PurchaseReceipt(BuyingController): self.delete_auto_created_batches() self.set_consumed_qty_in_subcontract_order() - def get_gl_entries(self, warehouse_account=None): + def get_gl_entries(self, warehouse_account=None, via_landed_cost_voucher=False): from erpnext.accounts.general_ledger import process_gl_map gl_entries = [] self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) - self.make_tax_gl_entries(gl_entries) + self.make_tax_gl_entries(gl_entries, via_landed_cost_voucher) update_regional_gl_entries(gl_entries, self) return process_gl_map(gl_entries) @@ -776,7 +776,7 @@ class PurchaseReceipt(BuyingController): posting_date=posting_date, ) - def make_tax_gl_entries(self, gl_entries): + def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False): negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) # Cost center-wise amount breakup for other charges included for valuation @@ -811,18 +811,17 @@ class PurchaseReceipt(BuyingController): i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): - negative_expense_booked_in_pi = frappe.db.sql( - """select name from `tabPurchase Invoice Item` pi - where docstatus = 1 and purchase_receipt=%s - and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' - and voucher_no=pi.parent and account=%s)""", - (self.name, tax.account_head), - ) - - if negative_expense_booked_in_pi: - account = stock_rbnb - else: + if via_landed_cost_voucher: account = tax.account_head + else: + negative_expense_booked_in_pi = frappe.db.sql( + """select name from `tabPurchase Invoice Item` pi + where docstatus = 1 and purchase_receipt=%s + and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' + and voucher_no=pi.parent and account=%s)""", + (self.name, tax.account_head), + ) + account = stock_rbnb if negative_expense_booked_in_pi else tax.account_head if i == len(valuation_tax): applicable_amount = amount_including_divisional_loss diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 88e9c94c2b1..42747818d2b 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -9,6 +9,7 @@ from pypika import functions as fn import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.controllers.buying_controller import QtyMismatchError +from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( @@ -1681,7 +1682,6 @@ class TestPurchaseReceipt(FrappeTestCase): frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) def test_internal_pr_gl_entries(self): - from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -2913,6 +2913,54 @@ class TestPurchaseReceipt(FrappeTestCase): ), ) + def test_valuation_taxes_lcv_repost_after_billing(self): + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + make_landed_cost_voucher, + ) + + old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled("_Test Company") + frappe.local.enable_perpetual_inventory["_Test Company"] = 1 + frappe.db.set_value( + "Company", + "_Test Company", + "stock_received_but_not_billed", + "Stock Received But Not Billed - _TC", + ) + + pr = make_purchase_receipt(qty=10, rate=1000, do_not_submit=1) + pr.append( + "taxes", + { + "category": "Valuation and Total", + "charge_type": "Actual", + "account_head": "Freight and Forwarding Charges - _TC", + "tax_amount": 2000, + "description": "Test", + }, + ) + pr.submit() + pi = make_purchase_invoice(pr.name) + pi.submit() + make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=2000, + distribute_charges_based_on="Qty", + expense_account="Expenses Included In Valuation - _TC", + ) + + gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True, as_dict=False) + warehouse_account = get_warehouse_account_map("_Test Company") + expected_gle = ( + ("Stock Received But Not Billed - _TC", 0, 10000, "Main - _TC"), + ("Freight and Forwarding Charges - _TC", 0, 2000, "Main - _TC"), + ("Expenses Included In Valuation - _TC", 0, 2000, "Main - _TC"), + (warehouse_account[pr.items[0].warehouse]["account"], 14000, 0, "Main - _TC"), + ) + self.assertSequenceEqual(expected_gle, gl_entries) + frappe.local.enable_perpetual_inventory["_Test Company"] = old_perpetual_inventory + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -2959,14 +3007,24 @@ def get_sl_entries(voucher_type, voucher_no): ) -def get_gl_entries(voucher_type, voucher_no): - return frappe.db.sql( - """select account, debit, credit, cost_center, is_cancelled - from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account desc""", - (voucher_type, voucher_no), - as_dict=1, +def get_gl_entries(voucher_type, voucher_no, skip_cancelled=False, as_dict=True): + gl = frappe.qb.DocType("GL Entry") + gl_query = ( + frappe.qb.from_(gl) + .select( + gl.account, + gl.debit, + gl.credit, + gl.cost_center, + ) + .where((gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no)) + .orderby(gl.account, order=frappe.qb.desc) ) + if skip_cancelled: + gl_query = gl_query.where(gl.is_cancelled == 0) + else: + gl_query = gl_query.select(gl.is_cancelled) + return gl_query.run(as_dict=as_dict) def get_taxes(**args): diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index d8b44765251..40767704f4e 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -77,7 +77,7 @@ class RepostItemValuation(Document): def validate_period_closing_voucher(self): # Period Closing Voucher - year_end_date = self.get_max_year_end_date(self.company) + year_end_date = self.get_max_period_closing_date(self.company) if year_end_date and getdate(self.posting_date) <= getdate(year_end_date): date = frappe.format(year_end_date, "Date") msg = f"Due to period closing, you cannot repost item valuation before {date}" @@ -120,24 +120,16 @@ class RepostItemValuation(Document): return frappe.get_all("Closing Stock Balance", fields=["name", "to_date"], filters=filters) @staticmethod - def get_max_year_end_date(company): - data = frappe.get_all( - "Period Closing Voucher", fields=["fiscal_year"], filters={"docstatus": 1, "company": company} - ) - - if not data: - return - - fiscal_years = [d.fiscal_year for d in data] - table = frappe.qb.DocType("Fiscal Year") + def get_max_period_closing_date(company): + table = frappe.qb.DocType("Period Closing Voucher") query = ( frappe.qb.from_(table) - .select(Max(table.year_end_date)) - .where((table.name.isin(fiscal_years)) & (table.disabled == 0)) + .select(Max(table.posting_date)) + .where((table.company == company) & (table.docstatus == 1)) ).run() - return query[0][0] if query else None + return query[0][0] if query and query[0][0] else None def validate_accounts_freeze(self): acc_settings = frappe.db.get_value(