diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 3d00eb74aa0..5f53b005aaf 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = { } }, onload: function(me) { - me.page.add_action_item('Make Asset Movement', function() { + me.page.add_action_item(__("Make Asset Movement"), function() { const assets = me.get_checked_items(); frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_asset_movement", diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 06989a95da7..65a4226ebdf 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = { "label": __("Period Based On"), "fieldtype": "Select", "options": ["Fiscal Year", "Date Range"], - "default": ["Fiscal Year"], + "default": "Fiscal Year", "reqd": 1 }, { @@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = { fieldtype: "Link", options: "Asset Category" }, - { - fieldname:"finance_book", - label: __("Finance Book"), - fieldtype: "Link", - options: "Finance Book" - }, { fieldname:"cost_center", label: __("Cost Center"), @@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = { reqd: 1 }, { - fieldname:"is_existing_asset", - label: __("Is Existing Asset"), + fieldname:"finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", + depends_on: "eval: doc.only_depreciable_assets == 1", + }, + { + fieldname:"only_depreciable_assets", + label: __("Only depreciable assets"), + fieldtype: "Check" + }, + { + fieldname:"only_existing_assets", + label: __("Only existing assets"), fieldtype: "Check" }, ] diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index ae99c491a93..63f9889f054 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -45,8 +45,10 @@ def get_conditions(filters): filters.year_end_date = getdate(fiscal_year.year_end_date) conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]] - if filters.get("is_existing_asset"): - conditions["is_existing_asset"] = filters.get("is_existing_asset") + if filters.get("only_depreciable_assets"): + conditions["calculate_depreciation"] = filters.get("only_depreciable_assets") + if filters.get("only_existing_assets"): + conditions["is_existing_asset"] = filters.get("only_existing_assets") if filters.get("asset_category"): conditions["asset_category"] = filters.get("asset_category") if filters.get("cost_center"): @@ -102,19 +104,18 @@ def get_data(filters): ] assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) - assets_linked_to_fb = frappe.db.get_all( - doctype="Asset Finance Book", - filters={"finance_book": filters.finance_book or ("is", "not set")}, - pluck="parent", - ) + assets_linked_to_fb = None + + if filters.only_depreciable_assets: + assets_linked_to_fb = frappe.db.get_all( + doctype="Asset Finance Book", + filters={"finance_book": filters.finance_book or ("is", "not set")}, + pluck="parent", + ) for asset in assets_record: - if filters.finance_book: - if asset.asset_id not in assets_linked_to_fb: - continue - else: - if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb: - continue + if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb: + continue asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book) row = { @@ -172,11 +173,11 @@ def prepare_chart_data(data, filters): "datasets": [ { "name": _("Asset Value"), - "values": [d.get("asset_value") for d in labels_values_map.values()], + "values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()], }, { "name": _("Depreciatied Amount"), - "values": [d.get("depreciated_amount") for d in labels_values_map.values()], + "values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()], }, ], }, @@ -310,7 +311,7 @@ def get_columns(filters): return [ { - "label": _("Asset Id"), + "label": _("Asset ID"), "fieldtype": "Link", "fieldname": "asset_id", "options": "Asset", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c4032596f47..f1ee370e97e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -30,10 +30,6 @@ doctype_js = { override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} -override_whitelisted_methods = { - "frappe.www.contact.send_message": "erpnext.templates.utils.send_message" -} - welcome_email = "erpnext.setup.utils.welcome_email" # setup wizard diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index db699b94d8f..d02402299e3 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -9,15 +9,14 @@ "production_item_tab", "item", "company", - "item_name", "uom", + "quantity", "cb0", "is_active", "is_default", "allow_alternative_item", "set_rate_of_sub_assembly_item_based_on_bom", "project", - "quantity", "image", "currency_detail", "rm_cost_as_per", @@ -27,6 +26,8 @@ "column_break_ivyw", "currency", "conversion_rate", + "materials_section", + "items", "section_break_21", "operations_section_section", "with_operations", @@ -38,8 +39,6 @@ "operating_cost_per_bom_quantity", "operations_section", "operations", - "materials_section", - "items", "scrap_section", "scrap_items_section", "scrap_items", @@ -59,6 +58,7 @@ "total_cost", "base_total_cost", "more_info_tab", + "item_name", "description", "column_break_27", "has_variants", @@ -192,6 +192,7 @@ "options": "Quality Inspection Template" }, { + "collapsible": 1, "fieldname": "currency_detail", "fieldtype": "Section Break", "label": "Cost Configuration" @@ -417,7 +418,7 @@ { "collapsible": 1, "fieldname": "website_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Website" }, { @@ -482,7 +483,7 @@ { "fieldname": "section_break_21", "fieldtype": "Tab Break", - "label": "Operations & Materials" + "label": "Operations" }, { "fieldname": "column_break_23", @@ -605,7 +606,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-02-13 17:31:37.504565", + "modified": "2023-04-06 12:47:58.514795", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 25e16d63376..aa9049801cc 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -22,17 +22,13 @@ "produced_qty", "process_loss_qty", "project", - "serial_no_and_batch_for_finished_good_section", - "has_serial_no", - "has_batch_no", - "column_break_17", - "serial_no", - "batch_size", + "section_break_ndpq", + "required_items", "work_order_configuration", "settings_section", "allow_alternative_item", "use_multi_level_bom", - "column_break_18", + "column_break_17", "skip_transfer", "from_wip_warehouse", "update_consumed_material_cost_in_project", @@ -42,9 +38,14 @@ "column_break_12", "fg_warehouse", "scrap_warehouse", + "serial_no_and_batch_for_finished_good_section", + "has_serial_no", + "has_batch_no", + "column_break_18", + "serial_no", + "batch_size", "required_items_section", "materials_and_operations_tab", - "required_items", "operations_section", "operations", "transfer_material_against", @@ -586,7 +587,11 @@ { "fieldname": "materials_and_operations_tab", "fieldtype": "Tab Break", - "label": "Materials & Operations" + "label": "Operations" + }, + { + "fieldname": "section_break_ndpq", + "fieldtype": "Section Break" } ], "icon": "fa fa-cogs", @@ -594,7 +599,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-01-03 14:16:35.427731", + "modified": "2023-04-06 12:35:12.149827", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 07d1955bfaf..e58dd98efd4 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1696,7 +1696,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } $.each(["company", "customer"], function(i, fieldname) { - if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && me.frm.doc.doctype != "Purchase Order") { + if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) { if (!me.frm.doc[fieldname]) { frappe.msgprint(__("Please specify") + ": " + frappe.meta.get_label(me.frm.doc.doctype, fieldname, me.frm.doc.name) + diff --git a/erpnext/public/js/website_utils.js b/erpnext/public/js/website_utils.js index 2bb5255eebc..b5416065d79 100644 --- a/erpnext/public/js/website_utils.js +++ b/erpnext/public/js/website_utils.js @@ -3,6 +3,18 @@ if(!window.erpnext) window.erpnext = {}; +// Add / update a new Lead / Communication +// subject, sender, description +frappe.send_message = function(opts, btn) { + return frappe.call({ + type: "POST", + method: "erpnext.templates.utils.send_message", + btn: btn, + args: opts, + callback: opts.callback + }); +}; + erpnext.subscribe_to_newsletter = function(opts, btn) { return frappe.call({ type: "POST", @@ -12,3 +24,6 @@ erpnext.subscribe_to_newsletter = function(opts, btn) { callback: opts.callback }); } + +// for backward compatibility +erpnext.send_message = frappe.send_message; diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 2eca5cad8e2..f5432c18258 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -36,8 +36,24 @@ class ItemGroup(NestedSet, WebsiteGenerator): self.make_route() self.validate_item_group_defaults() + self.check_item_tax() ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True) + def check_item_tax(self): + """Check whether Tax Rate is not entered twice for same Tax Type""" + check_list = [] + for d in self.get("taxes"): + if d.item_tax_template: + if (d.item_tax_template, d.tax_category) in check_list: + frappe.throw( + _("{0} entered twice {1} in Item Taxes").format( + frappe.bold(d.item_tax_template), + "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "", + ) + ) + else: + check_list.append((d.item_tax_template, d.tax_category)) + def on_update(self): NestedSet.on_update(self) invalidate_cache_for(self) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 4a165212dce..3b9fe7b97cd 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last +from frappe.query_builder.functions import CurDate, Sum, Timestamp from frappe.utils import cint, flt, get_link_to_form, nowtime from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -176,49 +177,41 @@ def get_batch_qty( :param warehouse: Optional - give qty for this warehouse :param item_code: Optional - give qty for this item""" + sle = frappe.qb.DocType("Stock Ledger Entry") + out = 0 if batch_no and warehouse: - cond = "" + query = ( + frappe.qb.from_(sle) + .select(Sum(sle.actual_qty)) + .where((sle.is_cancelled == 0) & (sle.warehouse == warehouse) & (sle.batch_no == batch_no)) + ) if posting_date: if posting_time is None: posting_time = nowtime() - cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( - posting_date, posting_time + query = query.where( + Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time) ) - out = float( - frappe.db.sql( - """select sum(actual_qty) - from `tabStock Ledger Entry` - where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format( - cond - ), - (warehouse, batch_no), - )[0][0] - or 0 - ) + out = query.run(as_list=True)[0][0] or 0 if batch_no and not warehouse: - out = frappe.db.sql( - """select warehouse, sum(actual_qty) as qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and batch_no=%s - group by warehouse""", - batch_no, - as_dict=1, - ) + out = ( + frappe.qb.from_(sle) + .select(sle.warehouse, Sum(sle.actual_qty).as_("qty")) + .where((sle.is_cancelled == 0) & (sle.batch_no == batch_no)) + .groupby(sle.warehouse) + ).run(as_dict=True) if not batch_no and item_code and warehouse: - out = frappe.db.sql( - """select batch_no, sum(actual_qty) as qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and item_code = %s and warehouse=%s - group by batch_no""", - (item_code, warehouse), - as_dict=1, - ) + out = ( + frappe.qb.from_(sle) + .select(sle.batch_no, Sum(sle.actual_qty).as_("qty")) + .where((sle.is_cancelled == 0) & (sle.item_code == item_code) & (sle.warehouse == warehouse)) + .groupby(sle.batch_no) + ).run(as_dict=True) return out @@ -314,40 +307,44 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - cond = "" + batch = frappe.qb.DocType("Batch") + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(batch) + .join(sle) + .on(batch.batch_id == sle.batch_no) + .select( + batch.batch_id, + Sum(sle.actual_qty).as_("qty"), + ) + .where( + (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.is_cancelled == 0) + & ((batch.expiry_date >= CurDate()) | (batch.expiry_date.isnull())) + ) + .groupby(batch.batch_id) + .orderby(batch.expiry_date, batch.creation) + ) + if serial_no and frappe.get_cached_value("Item", item_code, "has_batch_no"): serial_nos = get_serial_nos(serial_no) - batch = frappe.get_all( + batches = frappe.get_all( "Serial No", fields=["distinct batch_no"], filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)}, ) - if not batch: + if not batches: validate_serial_no_with_batch(serial_nos, item_code) - if batch and len(batch) > 1: + if batches and len(batches) > 1: return [] - cond = " and `tabBatch`.name = %s" % (frappe.db.escape(batch[0].batch_no)) + query = query.where(batch.name == batches[0].batch_no) - return frappe.db.sql( - """ - select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty - from `tabBatch` - join `tabStock Ledger Entry` ignore index (item_code, warehouse) - on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) - where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s - and `tabStock Ledger Entry`.is_cancelled = 0 - and (`tabBatch`.expiry_date >= CURRENT_DATE or `tabBatch`.expiry_date IS NULL) {0} - group by batch_id - order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC - """.format( - cond - ), - (item_code, warehouse), - as_dict=True, - ) + return query.run(as_dict=True) def validate_serial_no_with_batch(serial_nos, item_code): diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 1763269193a..180adee0cb0 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -83,6 +83,8 @@ "actual_qty", "installed_qty", "item_tax_rate", + "column_break_atna", + "received_qty", "accounting_details_section", "expense_account", "allow_zero_valuation_rate", @@ -832,13 +834,27 @@ "fieldname": "material_request_item", "fieldtype": "Data", "label": "Material Request Item" + }, + { + "fieldname": "column_break_atna", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: parent.is_internal_customer", + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-20 14:24:10.406746", + "modified": "2023-04-06 09:28:29.182053", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 423b9defc19..8a25be58618 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -350,10 +350,15 @@ class Item(Document): check_list = [] for d in self.get("taxes"): if d.item_tax_template: - if d.item_tax_template in check_list: - frappe.throw(_("{0} entered twice in Item Tax").format(d.item_tax_template)) + if (d.item_tax_template, d.tax_category) in check_list: + frappe.throw( + _("{0} entered twice {1} in Item Taxes").format( + frappe.bold(d.item_tax_template), + "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "", + ) + ) else: - check_list.append(d.item_tax_template) + check_list.append((d.item_tax_template, d.tax_category)) def validate_barcode(self): from stdnum import ean diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c1abd31bcc1..d268cc11963 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -65,6 +65,16 @@ class PurchaseReceipt(BuyingController): "percent_join_field": "purchase_invoice", "overflow_type": "receipt", }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Delivery Note Item", + "join_field": "delivery_note_item", + "source_field": "received_qty", + "target_field": "received_qty", + "target_parent_dt": "Delivery Note", + "target_ref_field": "qty", + "overflow_type": "receipt", + }, ] if cint(self.is_return): diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b6341466f87..7567cfe98c5 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1544,6 +1544,72 @@ class TestPurchaseReceipt(FrappeTestCase): res = get_item_details(args) self.assertEqual(res.get("last_purchase_rate"), 100) + def test_validate_received_qty_for_internal_pr(self): + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + + # Step 1: Create Item + item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}) + + # Step 2: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + make_stock_entry( + purpose="Material Receipt", + item_code=item.name, + qty=15, + company=company, + to_warehouse=from_warehouse, + ) + + # Step 3: Create Delivery Note with Internal Customer + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + dn = create_delivery_note( + item_code=item.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=100, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + # Step 4: Create Internal Purchase Receipt + from erpnext.controllers.status_updater import OverAllowanceError + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + pr = make_inter_company_purchase_receipt(dn.name) + pr.items[0].qty = 15 + pr.items[0].from_warehouse = target_warehouse + pr.items[0].warehouse = to_warehouse + pr.items[0].rejected_warehouse = from_warehouse + pr.save() + + self.assertRaises(OverAllowanceError, pr.submit) + + # Step 5: Test Over Receipt Allowance + frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50) + + make_stock_entry( + purpose="Material Transfer", + item_code=item.name, + qty=5, + company=company, + from_warehouse=from_warehouse, + to_warehouse=target_warehouse, + ) + + pr.submit() + + frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 9c0f1fc03f4..bc5533fd2de 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -27,7 +27,6 @@ "set_posting_time", "inspection_required", "apply_putaway_rule", - "items_tab", "bom_info_section", "from_bom", "use_multi_level_bom", @@ -256,7 +255,7 @@ "description": "As per Stock UOM", "fieldname": "fg_completed_qty", "fieldtype": "Float", - "label": "For Quantity", + "label": "Finished Good Quantity ", "oldfieldname": "fg_completed_qty", "oldfieldtype": "Currency", "print_hide": 1 @@ -612,11 +611,7 @@ "read_only": 1 }, { - "fieldname": "items_tab", - "fieldtype": "Tab Break", - "label": "Items" - }, - { + "collapsible": 1, "fieldname": "bom_info_section", "fieldtype": "Section Break", "label": "BOM Info" @@ -644,8 +639,10 @@ "oldfieldtype": "Section Break" }, { + "collapsible": 1, "fieldname": "section_break_7qsm", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Process Loss" }, { "depends_on": "process_loss_percentage", @@ -677,7 +674,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-01-03 16:02:50.741816", + "modified": "2023-04-06 12:42:56.673180", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 482b103d1e4..e304bd18193 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -5,7 +5,7 @@ from typing import Optional import frappe from frappe import _, bold, msgprint -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import cint, cstr, flt import erpnext @@ -575,7 +575,9 @@ class StockReconciliation(StockController): if not (row.item_code == item_code and row.batch_no == batch_no): continue - row.current_qty = get_batch_qty_for_stock_reco(item_code, row.warehouse, batch_no) + row.current_qty = get_batch_qty_for_stock_reco( + item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name + ) qty, val_rate = get_stock_balance( item_code, @@ -596,7 +598,9 @@ class StockReconciliation(StockController): ) -def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no): +def get_batch_qty_for_stock_reco( + item_code, warehouse, batch_no, posting_date, posting_time, voucher_no +): ledger = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -610,6 +614,12 @@ def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no): & (ledger.docstatus == 1) & (ledger.is_cancelled == 0) & (ledger.batch_no == batch_no) + & (ledger.posting_date <= posting_date) + & ( + CombineDatetime(ledger.posting_date, ledger.posting_time) + <= CombineDatetime(posting_date, posting_time) + ) + & (ledger.voucher_no != voucher_no) ) .groupby(ledger.batch_no) ) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index eaea301432e..7d59441d8b7 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -676,6 +676,79 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(flt(sl_entry.actual_qty), 1.0) self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0) + def test_backdated_stock_reco_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test New Batch Item ABCV", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + # Added 100 Qty, Balace Qty 100 + se1 = make_stock_entry( + item_code=item_code, posting_time="09:00:00", target=warehouse, qty=100, basic_rate=700 + ) + + # Removed 50 Qty, Balace Qty 50 + se2 = make_stock_entry( + item_code=item_code, + batch_no=se1.items[0].batch_no, + posting_time="10:00:00", + source=warehouse, + qty=50, + basic_rate=700, + ) + + # Stock Reco for 100, Balace Qty 100 + stock_reco = create_stock_reconciliation( + item_code=item_code, + posting_time="11:00:00", + warehouse=warehouse, + batch_no=se1.items[0].batch_no, + qty=100, + rate=100, + ) + + # Removed 50 Qty, Balace Qty 50 + make_stock_entry( + item_code=item_code, + batch_no=se1.items[0].batch_no, + posting_time="12:00:00", + source=warehouse, + qty=50, + basic_rate=700, + ) + + self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name})) + + # Cancel the backdated Stock Entry se2, + # Since Stock Reco entry in the future the Balace Qty should remain as it's (50) + + se2.cancel() + + self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name})) + + self.assertEqual( + frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"), + "Completed", + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0}, + fields=["qty_after_transaction"], + order_by="posting_time desc, creation desc", + ) + + self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0)) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 2df39c81832..2cf3797a36b 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -637,7 +637,9 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity.append(tax) if taxes_with_validity: - taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from, reverse=True) + taxes = sorted( + taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True + ) else: taxes = taxes_with_no_validity diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c954befdc29..b0a093def49 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1375,6 +1375,7 @@ def regenerate_sle_for_batch_stock_reco(detail): doc.recalculate_current_qty(detail.item_code, detail.batch_no) doc.docstatus = 1 doc.update_stock_ledger() + doc.repost_future_sle_and_gle() def get_stock_reco_qty_shift(args): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 95dbc83bf80..4f8e045d706 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -245,17 +245,17 @@ class SubcontractingReceipt(SubcontractingController): item.expense_account = expense_account def update_status(self, status=None, update_modified=False): - if self.docstatus >= 1 and not status: - if self.docstatus == 1: + if not status: + if self.docstatus == 0: + status = "Draft" + elif self.docstatus == 1: + status = "Completed" if self.is_return: status = "Return" return_against = frappe.get_doc("Subcontracting Receipt", self.return_against) return_against.run_method("update_status") - else: - if self.per_returned == 100: - status = "Return Issued" - elif self.status == "Draft": - status = "Completed" + elif self.per_returned == 100: + status = "Return Issued" elif self.docstatus == 2: status = "Cancelled" diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 8472d04f1ca..ac6ccf99c6e 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1875,6 +1875,7 @@ Parents Teacher Meeting Attendance,Eltern Lehrer Treffen Teilnahme, Part-time,Teilzeit, Partially Depreciated,Teilweise abgeschrieben, Partially Received,Teilweise erhalten, +Partly Paid,Teilweise bezahlt, Party,Partei, Party Name,Name der Partei, Party Type,Partei-Typ,