diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 95fbd68f381..0eecf8198d9 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -212,7 +212,7 @@ def get_opening_balance( ignore_is_opening=0, ): closing_balance = frappe.qb.DocType(doctype) - account = frappe.qb.DocType("Account") + accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name") opening_balance = ( frappe.qb.from_(closing_balance) @@ -224,14 +224,7 @@ def get_opening_balance( Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"), Sum(closing_balance.credit_in_account_currency).as_("credit_in_account_currency"), ) - .where( - (closing_balance.company == filters.company) - & ( - closing_balance.account.isin( - frappe.qb.from_(account).select("name").where(account.report_type == report_type) - ) - ) - ) + .where((closing_balance.company == filters.company) & (closing_balance.account.isin(accounts))) .groupby(closing_balance.account) ) @@ -286,21 +279,24 @@ def get_opening_balance( if filters.project: opening_balance = opening_balance.where(closing_balance.project == filters.project) - if filters.get("include_default_book_entries"): - company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") + if frappe.db.count("Finance Book"): + if filters.get("include_default_book_entries"): + company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") - if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): - frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'")) + if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): + frappe.throw( + _("To use a different finance book, please uncheck 'Include Default FB Entries'") + ) - opening_balance = opening_balance.where( - (closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) - | (closing_balance.finance_book.isnull()) - ) - else: - opening_balance = opening_balance.where( - (closing_balance.finance_book.isin([cstr(filters.finance_book), ""])) - | (closing_balance.finance_book.isnull()) - ) + opening_balance = opening_balance.where( + (closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) + | (closing_balance.finance_book.isnull()) + ) + else: + opening_balance = opening_balance.where( + (closing_balance.finance_book.isin([cstr(filters.finance_book), ""])) + | (closing_balance.finance_book.isnull()) + ) if accounting_dimensions: for dimension in accounting_dimensions: diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 84e557bb5dc..b4ff9d9b885 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -13,7 +13,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime import erpnext from erpnext.stock.serial_batch_bundle import get_batches_from_bundle from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle -from erpnext.stock.utils import get_incoming_rate, get_valuation_method +from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method class StockOverReturnError(frappe.ValidationError): @@ -1082,8 +1082,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f "batches": data.get("batches"), "serial_nos_valuation": data.get("serial_nos_valuation"), "batches_valuation": data.get("batches_valuation"), - "posting_date": parent_doc.posting_date, - "posting_time": parent_doc.posting_time, + "posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time), "voucher_type": parent_doc.doctype, "voucher_no": parent_doc.name, "voucher_detail_no": child_doc.name, diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 85a07dabfeb..a0c53ad0ece 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor -from erpnext.stock.utils import get_incoming_rate, get_valuation_method +from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method class SellingController(StockController): @@ -1017,8 +1017,7 @@ def get_serial_and_batch_bundle(child, parent, delivery_note_child=None): "voucher_type": parent.doctype, "voucher_no": parent.name if parent.docstatus < 2 else None, "voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name, - "posting_date": parent.posting_date, - "posting_time": parent.posting_time, + "posting_datetime": get_combine_datetime(parent.posting_date, parent.posting_time), "qty": child.qty, "type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward", "company": parent.company, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fc0fd9f3055..84d22c54f8c 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -27,6 +27,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( get_evaluated_inventory_dimension, ) from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_type_of_transaction, ) from erpnext.stock.stock_ledger import get_items_to_be_repost @@ -266,8 +267,7 @@ class StockController(AccountsController): ): bundle_details = { "item_code": row.get("rm_item_code") or row.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index ecdac114d1a..cf3782c7e4b 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -12,6 +12,9 @@ from frappe.utils import cint, flt, get_link_to_form from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, + get_auto_batch_nos, + get_available_serial_nos, get_voucher_wise_serial_batch_from_bundle, ) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -52,9 +55,42 @@ class SubcontractingController(StockController): if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]: self.validate_items() self.create_raw_materials_supplied() + self.set_valuation_rate_for_rm() else: super().validate() + def set_valuation_rate_for_rm(self): + rate_changed = False + if self.doctype == "Subcontracting Receipt": + for row in self.supplied_items: + kwargs = frappe._dict( + { + "item_code": row.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": flt(row.consumed_qty) * (-1 if not self.is_return else 1), + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "serial_and_batch_bundle": row.serial_and_batch_bundle, + "voucher_detail_no": row.name, + "batch_no": row.batch_no, + "serial_no": row.serial_no, + "use_serial_batch_fields": row.use_serial_batch_fields, + } + ) + + rate = get_incoming_rate(kwargs) + precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "rate") + if flt(rate, precision) != flt(row.rate, precision): + row.rate = rate + row.amount = flt(row.consumed_qty) * flt(rate) + rate_changed = True + + if rate_changed: + self.calculate_items_qty_and_amount() + def validate_rejected_warehouse(self): for item in self.get("items"): if flt(item.rejected_qty) and not item.rejected_warehouse: @@ -537,8 +573,7 @@ class SubcontractingController(StockController): "qty": qty, "serial_nos": serial_nos, "batches": batches, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "voucher_type": "Subcontracting Receipt", "do_not_submit": True, "type_of_transaction": "Outward" if qty > 0 else "Inward", @@ -616,6 +651,64 @@ class SubcontractingController(StockController): self.set_rate_for_supplied_items(rm_obj, item_row) elif self.backflush_based_on == "BOM": self.update_rate_for_supplied_items() + self.set_batch_for_supplied_items() + + def set_batch_for_supplied_items(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward + from erpnext.stock.get_item_details import get_filtered_serial_nos + + for row in self.supplied_items: + item_details = frappe.get_cached_value( + "Item", row.rm_item_code, ["has_batch_no", "has_serial_no"], as_dict=1 + ) + + if not item_details.has_batch_no and not item_details.has_serial_no: + continue + + if not row.use_serial_batch_fields: + continue + + kwargs = frappe._dict( + { + "item_code": row.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": flt(row.consumed_qty), + } + ) + + if item_details.has_serial_no and not row.serial_and_batch_bundle and not row.serial_no: + serial_nos = get_available_serial_nos(kwargs) + if serial_nos: + serial_nos = [sn.get("serial_no") for sn in serial_nos] + serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items") + row.serial_no = "\n".join(serial_nos) + + elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no: + batches = get_auto_batch_nos(kwargs) + if batches: + consumed_qty = row.consumed_qty + for index, d in enumerate(batches): + if consumed_qty <= 0: + break + + if index == 0: + row.batch_no = d.get("batch_no") + row.consumed_qty = d.get("qty") + consumed_qty -= d.get("qty") + else: + new_row = self.append("supplied_items", {}) + new_row.update(frappe.copy_doc(row).as_dict()) + new_row.update( + { + "consumed_qty": d.get("qty"), + "batch_no": d.get("batch_no"), + "rate": row.rate, + "amount": flt(d.get("qty")) * flt(row.rate), + } + ) + consumed_qty -= d.get("qty") def update_rate_for_supplied_items(self): if self.doctype != "Subcontracting Receipt": diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 5caaba6c70b..94ec192779e 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -1308,6 +1308,7 @@ def make_subcontracted_items(): "Subcontracted Item SA7": {}, "Subcontracted Item SA8": {}, "Subcontracted Item SA9": {"stock_uom": "Litre"}, + "Subcontracted Item SA10": {}, } for item, properties in sub_contracted_items.items(): @@ -1329,6 +1330,7 @@ def make_raw_materials(): "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"}, "Subcontracted SRM Item 8": {}, "Subcontracted SRM Item 9": {"stock_uom": "Litre"}, + "Subcontracted SRM Item 10": {}, } for item, properties in raw_materials.items(): @@ -1357,6 +1359,7 @@ def make_service_items(): "Subcontracted Service Item 7": {}, "Subcontracted Service Item 8": {}, "Subcontracted Service Item 9": {}, + "Subcontracted Service Item 10": {}, } for item, properties in service_items.items(): @@ -1381,6 +1384,7 @@ def make_bom_for_subcontracted_items(): "Subcontracted Item SA6": ["Subcontracted SRM Item 3"], "Subcontracted Item SA7": ["Subcontracted SRM Item 1"], "Subcontracted Item SA8": ["Subcontracted SRM Item 8"], + "Subcontracted Item SA10": ["Subcontracted SRM Item 10"], } for item_code, raw_materials in boms.items(): diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 28da6a9e275..f9e9250b8b6 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1487,7 +1487,9 @@ def add_operating_cost_component_wise( ) for wc in workstation_cost: - expense_account = get_component_account(wc.operating_component) or op_expense_account + expense_account = ( + get_component_account(wc.operating_component, stock_entry.company) or op_expense_account + ) actual_cp_operating_cost = flt( flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0), row.precision("actual_operating_cost"), @@ -1513,8 +1515,10 @@ def add_operating_cost_component_wise( @frappe.request_cache -def get_component_account(parent): - return frappe.db.get_value("Workstation Operating Component Account", parent, "expense_account") +def get_component_account(parent, company): + return frappe.db.get_value( + "Workstation Operating Component Account", {"parent": parent, "company": company}, "expense_account" + ) def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 91dad17616d..674cca808e1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -437,3 +437,4 @@ erpnext.patches.v16_0.set_invoice_type_in_pos_settings erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter erpnext.patches.v16_0.make_workstation_operating_components #1 erpnext.patches.v16_0.set_reporting_currency +erpnext.patches.v16_0.set_posting_datetime_for_sabb_and_drop_indexes diff --git a/erpnext/patches/v16_0/set_posting_datetime_for_sabb_and_drop_indexes.py b/erpnext/patches/v16_0/set_posting_datetime_for_sabb_and_drop_indexes.py new file mode 100644 index 00000000000..c4a71791671 --- /dev/null +++ b/erpnext/patches/v16_0/set_posting_datetime_for_sabb_and_drop_indexes.py @@ -0,0 +1,31 @@ +import click +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE `tabSerial and Batch Bundle` + JOIN `tabStock Ledger Entry` + ON `tabSerial and Batch Bundle`.`name` = `tabStock Ledger Entry`.`serial_and_batch_bundle` + SET `tabSerial and Batch Bundle`.`posting_datetime` = `tabStock Ledger Entry`.`posting_datetime` + WHERE `tabStock Ledger Entry`.`is_cancelled` = 0 + """ + ) + + drop_indexes() + + +def drop_indexes(): + table = "tabSerial and Batch Bundle" + index_list = ["voucher_no_index", "item_code_index", "warehouse_index", "company_index"] + + for index in index_list: + if not frappe.db.has_index(table, index): + continue + + try: + frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") + click.echo(f"✓ dropped {index} index from {table}") + except Exception: + frappe.log_error("Failed to drop index") diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 4adc5449dba..8ef3fe93fa8 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -53,6 +53,11 @@ class DeprecatedSerialNoValuation: # get rate from serial nos within same company incoming_values = 0.0 + posting_datetime = self.sle.posting_datetime + + if not posting_datetime and self.sle.posting_date: + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + for serial_no in serial_nos: sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1) if sn_details and sn_details.purchase_rate and sn_details.company == self.sle.company: @@ -77,7 +82,7 @@ class DeprecatedSerialNoValuation: & (table.actual_qty > 0) & (table.is_cancelled == 0) & table.posting_datetime - <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + <= posting_datetime ) .orderby(table.posting_datetime, order=Order.desc) .limit(1) @@ -132,11 +137,8 @@ class DeprecatedBatchNoValuation: sle = frappe.qb.DocType("Stock Ledger Entry") timestamp_condition = None - if self.sle.posting_date: - if self.sle.posting_time is None: - self.sle.posting_time = nowtime() - - posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + if self.sle.posting_datetime: + posting_datetime = self.sle.posting_datetime if not self.sle.creation: posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) @@ -245,7 +247,11 @@ class DeprecatedBatchNoValuation: sle = frappe.qb.DocType("Stock Ledger Entry") batch = frappe.qb.DocType("Batch") - posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + posting_datetime = self.sle.posting_datetime + + if not posting_datetime and self.sle.posting_date: + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + if not self.sle.creation: posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) @@ -293,7 +299,10 @@ class DeprecatedBatchNoValuation: sle = frappe.qb.DocType("Stock Ledger Entry") - posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + posting_datetime = self.sle.posting_datetime + if not posting_datetime and self.sle.posting_date: + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + if not self.sle.creation: posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) @@ -343,19 +352,22 @@ class DeprecatedBatchNoValuation: "No known instructions.", ) def set_balance_value_from_bundle(self) -> None: + from erpnext.stock.utils import get_combine_datetime + bundle = frappe.qb.DocType("Serial and Batch Bundle") bundle_child = frappe.qb.DocType("Serial and Batch Entry") batch = frappe.qb.DocType("Batch") - timestamp_condition = CombineDatetime(bundle.posting_date, bundle.posting_time) < CombineDatetime( - self.sle.posting_date, self.sle.posting_time - ) + posting_datetime = self.sle.posting_datetime + if not posting_datetime and self.sle.posting_date: + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + + timestamp_condition = bundle.posting_datetime < posting_datetime if self.sle.creation: - timestamp_condition |= ( - CombineDatetime(bundle.posting_date, bundle.posting_time) - == CombineDatetime(self.sle.posting_date, self.sle.posting_time) - ) & (bundle.creation < self.sle.creation) + timestamp_condition |= (bundle.posting_datetime == posting_datetime) & ( + bundle.creation < self.sle.creation + ) query = ( frappe.qb.from_(bundle) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 302cef668f6..29ebb710923 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -219,6 +219,7 @@ def get_batch_qty( warehouse=None, item_code=None, creation=None, + posting_datetime=None, posting_date=None, posting_time=None, ignore_voucher_nos=None, @@ -237,6 +238,7 @@ def get_batch_qty( :param for_stock_levels: True consider expired batches""" from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_auto_batch_nos, ) @@ -246,8 +248,6 @@ def get_batch_qty( "item_code": item_code, "warehouse": warehouse, "creation": creation, - "posting_date": posting_date, - "posting_time": posting_time, "batch_no": batch_no, "ignore_voucher_nos": ignore_voucher_nos, "for_stock_levels": for_stock_levels, @@ -256,6 +256,10 @@ def get_batch_qty( } ) + kwargs["posting_datetime"] = posting_datetime + if not kwargs.get("posting_datetime") and posting_date: + kwargs["posting_datetime"] = combine_datetime(posting_date, posting_time) + batches = get_auto_batch_nos(kwargs) if not (batch_no and warehouse): @@ -337,6 +341,7 @@ def make_batch_bundle( ): from frappe.utils import nowtime, today + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import combine_datetime from erpnext.stock.serial_batch_bundle import SerialBatchCreation return ( @@ -344,8 +349,7 @@ def make_batch_bundle( { "item_code": item_code, "warehouse": warehouse, - "posting_date": today(), - "posting_time": nowtime(), + "posting_datetime": combine_datetime(today(), nowtime()), "voucher_type": "Stock Entry", "qty": qty, "type_of_transaction": type_of_transaction, @@ -456,9 +460,13 @@ def get_pos_reserved_batch_qty(filters): def get_available_batches(kwargs): from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_auto_batch_nos, ) + if kwargs.get("posting_date"): + kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) + batchwise_qty = OrderedDict() batches = get_auto_batch_nos(kwargs) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index f3601cd622d..0f1e4686b77 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -1438,7 +1438,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte @frappe.whitelist() def get_item_details(item_code, uom=None, warehouse=None, company=None): - details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1) + details = frappe.db.get_value("Item", item_code, "stock_uom", as_dict=1) details.uom = uom or details.stock_uom if uom: details.update(get_conversion_factor(item_code, uom)) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index ff2341234da..78970bb93d7 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -29,8 +29,7 @@ "voucher_no", "voucher_detail_no", "column_break_aouy", - "posting_date", - "posting_time", + "posting_datetime", "returned_against", "section_break_wzou", "is_cancelled", @@ -50,8 +49,7 @@ "in_list_view": 1, "label": "Company", "options": "Company", - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "fetch_from": "item_code.item_group", @@ -80,8 +78,7 @@ "in_standard_filter": 1, "label": "Item Code", "options": "Item", - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "fetch_from": "item_code.item_name", @@ -118,8 +115,7 @@ "in_standard_filter": 1, "label": "Voucher No", "no_copy": 1, - "options": "voucher_type", - "search_index": 1 + "options": "voucher_type" }, { "default": "0", @@ -189,8 +185,7 @@ "in_standard_filter": 1, "label": "Warehouse", "mandatory_depends_on": "eval:doc.type_of_transaction != \"Maintenance\"", - "options": "Warehouse", - "search_index": 1 + "options": "Warehouse" }, { "fieldname": "type_of_transaction", @@ -212,18 +207,6 @@ "fieldname": "section_break_wzou", "fieldtype": "Section Break" }, - { - "fieldname": "posting_date", - "fieldtype": "Date", - "label": "Posting Date", - "no_copy": 1 - }, - { - "fieldname": "posting_time", - "fieldtype": "Time", - "label": "Posting Time", - "no_copy": 1 - }, { "fieldname": "voucher_detail_no", "fieldtype": "Data", @@ -258,13 +241,18 @@ "fieldname": "is_packed", "fieldtype": "Check", "label": "Is Packed" + }, + { + "fieldname": "posting_datetime", + "fieldtype": "Datetime", + "label": "Posting Datetime" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-09-15 14:37:26.441742", + "modified": "2025-09-24 16:24:48.154853", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 0a7df3b537c..e7727361f0d 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -73,8 +73,7 @@ class SerialandBatchBundle(Document): item_group: DF.Link | None item_name: DF.Data | None naming_series: DF.Literal["", "SABB-.########"] - posting_date: DF.Date | None - posting_time: DF.Time | None + posting_datetime: DF.Datetime | None returned_against: DF.Data | None total_amount: DF.Float total_qty: DF.Float @@ -252,8 +251,7 @@ class SerialandBatchBundle(Document): kwargs.update( { "voucher_no": self.voucher_no, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": self.posting_datetime, } ) @@ -291,8 +289,7 @@ class SerialandBatchBundle(Document): kwargs = frappe._dict( { "item_code": self.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": self.posting_datetime, "serial_nos": serial_nos, "check_serial_nos": True, } @@ -560,8 +557,7 @@ class SerialandBatchBundle(Document): def get_sle_for_outward_transaction(self): sle = frappe._dict( { - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": self.posting_datetime, "item_code": self.item_code, "warehouse": self.warehouse, "serial_and_batch_bundle": self.name, @@ -662,11 +658,10 @@ class SerialandBatchBundle(Document): if not self.voucher_detail_no or self.voucher_detail_no != row.name: values_to_set["voucher_detail_no"] = row.name - if parent.get("posting_date") and (not self.posting_date or self.posting_date != parent.posting_date): - values_to_set["posting_date"] = parent.posting_date or today() - - if parent.get("posting_time") and (not self.posting_time or self.posting_time != parent.posting_time): - values_to_set["posting_time"] = parent.posting_time + if parent.get("posting_date") and parent.get("posting_time"): + posting_datetime = combine_datetime(parent.posting_date, parent.posting_time) + if not self.posting_datetime or self.posting_datetime != posting_datetime: + values_to_set["posting_datetime"] = posting_datetime if parent.doctype in [ "Delivery Note", @@ -741,9 +736,7 @@ class SerialandBatchBundle(Document): parent = frappe.qb.DocType("Serial and Batch Bundle") child = frappe.qb.DocType("Serial and Batch Entry") - timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) > CombineDatetime( - self.posting_date, self.posting_time - ) + timestamp_condition = parent.posting_datetime > self.posting_datetime future_entries = ( frappe.qb.from_(parent) @@ -1251,7 +1244,7 @@ class SerialandBatchBundle(Document): frappe.qb.update(sn_table) .set(sn_table.reference_doctype, self.voucher_type) .set(sn_table.reference_name, self.voucher_no) - .set(sn_table.posting_date, self.posting_date) + .set(sn_table.posting_date, getdate(self.posting_datetime)) .where((sn_table.name.isin(serial_nos)) & (sn_table.reference_name.isnull())) ).run() @@ -1700,6 +1693,8 @@ def create_serial_batch_no_ledgers( if parent_doc.get("doctype") == "Stock Entry": warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse + posting_datetime = combine_datetime(parent_doc.get("posting_date"), parent_doc.get("posting_time")) + doc = frappe.get_doc( { "doctype": "Serial and Batch Bundle", @@ -1708,8 +1703,7 @@ def create_serial_batch_no_ledgers( "warehouse": warehouse, "is_rejected": child_row.is_rejected, "type_of_transaction": type_of_transaction, - "posting_date": parent_doc.get("posting_date"), - "posting_time": parent_doc.get("posting_time"), + "posting_datetime": posting_datetime, "company": parent_doc.get("company"), } ) @@ -1745,6 +1739,12 @@ def create_serial_batch_no_ledgers( return doc +def combine_datetime(date, time=None): + from erpnext.stock.utils import get_combine_datetime + + return get_combine_datetime(date, time) + + def get_batch(item_code): from erpnext.stock.doctype.batch.batch import make_batch @@ -1788,8 +1788,8 @@ def get_type_of_transaction(parent_doc, child_row): def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse=None) -> object: doc = frappe.get_doc("Serial and Batch Bundle", bundle) doc.voucher_detail_no = child_row.name - doc.posting_date = parent_doc.posting_date - doc.posting_time = parent_doc.posting_time + doc.posting_datetime = combine_datetime(parent_doc.get("posting_date"), parent_doc.get("posting_time")) + doc.warehouse = warehouse or doc.warehouse doc.set("entries", []) @@ -1896,6 +1896,9 @@ def get_available_serial_nos(kwargs): elif kwargs.based_on == "Expiry": order_by = "amc_expiry_date asc" + if not kwargs.get("posting_datetime") and kwargs.get("posting_date"): + kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) + filters = {"item_code": kwargs.item_code} # ignore_warehouse is used for backdated stock transactions @@ -1941,10 +1944,7 @@ def get_available_serial_nos(kwargs): ignore_serial_nos.extend(kwargs.get("ignore_serial_nos")) ignore_serial_nos = list(set(ignore_serial_nos)) - if kwargs.get("posting_date"): - if kwargs.get("posting_time") is None: - kwargs.posting_time = nowtime() - + if kwargs.get("posting_datetime"): time_based_serial_nos = get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos) if not time_based_serial_nos: @@ -2343,11 +2343,13 @@ def get_auto_batch_nos(kwargs): kwargs.batch_no = batches kwargs.warehouse = warehouses + if not kwargs.get("posting_datetime") and kwargs.get("posting_date"): + kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) + available_batches = get_available_batches(kwargs) stock_ledgers_batches = get_stock_ledgers_batches(kwargs) pos_invoice_batches = get_reserved_batches_for_pos(kwargs) sre_reserved_batches = get_reserved_batches_for_sre(kwargs) - if kwargs.against_sales_order and only_consider_batches: kwargs.batch_no = kwargs.warehouse = None @@ -2367,7 +2369,7 @@ def get_auto_batch_nos(kwargs): if kwargs.based_on == "Expiry": available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or getdate("9999-12-31"))) - if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"): + if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_datetime"): filter_zero_near_batches(available_batches, kwargs) if not kwargs.consider_negative_batches: @@ -2404,8 +2406,7 @@ def get_batches_to_be_considered(sales_order_name): def filter_zero_near_batches(available_batches, kwargs): kwargs.batch_no = [d.batch_no for d in available_batches] - del kwargs["posting_date"] - del kwargs["posting_time"] + del kwargs["posting_datetime"] kwargs.do_not_check_future_batches = 1 available_batches_in_future = get_auto_batch_nos(kwargs) @@ -2471,8 +2472,6 @@ def update_available_batches(available_batches, *reserved_batches) -> None: def get_available_batches(kwargs): - from erpnext.stock.utils import get_combine_datetime - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_ledger = frappe.qb.DocType("Serial and Batch Entry") batch_table = frappe.qb.DocType("Batch") @@ -2497,23 +2496,15 @@ def get_available_batches(kwargs): if not kwargs.get("for_stock_levels"): query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull())) - if kwargs.get("posting_date"): - if kwargs.get("posting_time") is None: - kwargs.posting_time = nowtime() - - timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + if kwargs.get("posting_datetime"): + timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime if kwargs.get("creation"): - timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime - timestamp_condition |= ( - stock_ledger_entry.posting_datetime - == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) - ) & (stock_ledger_entry.creation < kwargs.creation) + timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & ( + stock_ledger_entry.creation < kwargs.creation + ) query = query.where(timestamp_condition) @@ -2693,15 +2684,14 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]: serial_batch_table.incoming_rate, bundle_table.voucher_detail_no, bundle_table.voucher_no, - bundle_table.posting_date, - bundle_table.posting_time, + bundle_table.posting_datetime, ) .where( (bundle_table.docstatus == 1) & (bundle_table.is_cancelled == 0) & (bundle_table.type_of_transaction.isin(["Inward", "Outward"])) ) - .orderby(bundle_table.posting_date, bundle_table.posting_time) + .orderby(bundle_table.posting_datetime) ) for key, val in kwargs.items(): @@ -2719,7 +2709,7 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]: query = query.where(bundle_table[key].isin(val)) else: query = query.where(bundle_table[key] == val) - elif key in ["posting_date", "posting_time"]: + elif key in ["posting_datetime"]: query = query.where(bundle_table[key] >= val) else: if isinstance(val, list): @@ -2731,8 +2721,6 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]: def get_stock_ledgers_for_serial_nos(kwargs): - from erpnext.stock.utils import get_combine_datetime - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -2748,23 +2736,15 @@ def get_stock_ledgers_for_serial_nos(kwargs): .orderby(stock_ledger_entry.creation) ) - if kwargs.get("posting_date"): - if kwargs.get("posting_time") is None: - kwargs.posting_time = nowtime() - - timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + if kwargs.get("posting_datetime"): + timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime if kwargs.get("creation"): - timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime - timestamp_condition |= ( - stock_ledger_entry.posting_datetime - == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) - ) & (stock_ledger_entry.creation < kwargs.creation) + timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & ( + stock_ledger_entry.creation < kwargs.creation + ) query = query.where(timestamp_condition) @@ -2784,8 +2764,6 @@ def get_stock_ledgers_for_serial_nos(kwargs): def get_stock_ledgers_batches(kwargs): - from erpnext.stock.utils import get_combine_datetime - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_table = frappe.qb.DocType("Batch") @@ -2816,23 +2794,15 @@ def get_stock_ledgers_batches(kwargs): if not kwargs.get("for_stock_levels"): query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull())) - if kwargs.get("posting_date"): - if kwargs.get("posting_time") is None: - kwargs.posting_time = nowtime() - - timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + if kwargs.get("posting_datetime"): + timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime if kwargs.get("creation"): - timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime - timestamp_condition |= ( - stock_ledger_entry.posting_datetime - == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) - ) & (stock_ledger_entry.creation < kwargs.creation) + timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & ( + stock_ledger_entry.creation < kwargs.creation + ) query = query.where(timestamp_condition) @@ -2920,3 +2890,7 @@ def get_stock_reco_details(voucher_detail_no): ], as_dict=True, ) + + +def on_doctype_update(): + frappe.db.add_index("Serial and Batch Bundle", ["item_code", "warehouse", "posting_datetime", "creation"]) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 95ef3270ee9..b0dcee3b920 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -10,6 +10,7 @@ from frappe.utils import flt, nowtime, today from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( add_serial_batch_ledgers, + combine_datetime, make_batch_nos, make_serial_nos, ) @@ -932,14 +933,17 @@ def make_serial_batch_bundle(kwargs): if kwargs.get("type_of_transaction"): type_of_transaction = kwargs.get("type_of_transaction") + posting_datetime = None + if kwargs.get("posting_date"): + posting_datetime = combine_datetime(kwargs.posting_date, kwargs.posting_time or nowtime()) + sb = SerialBatchCreation( { "item_code": kwargs.item_code, "warehouse": kwargs.warehouse, "voucher_type": kwargs.voucher_type, "voucher_no": kwargs.voucher_no, - "posting_date": kwargs.posting_date, - "posting_time": kwargs.posting_time, + "posting_datetime": posting_datetime, "qty": kwargs.qty, "avg_rate": kwargs.rate, "batches": kwargs.batches, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index c5b0bb5735d..09233013cc6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -54,7 +54,7 @@ from erpnext.stock.serial_batch_bundle import ( get_serial_or_batch_items, ) from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get_valuation_rate -from erpnext.stock.utils import get_bin, get_incoming_rate +from erpnext.stock.utils import get_bin, get_combine_datetime, get_incoming_rate class FinishedGoodError(frappe.ValidationError): @@ -1122,8 +1122,7 @@ class StockEntry(StockController): { "item_code": row.item_code, "warehouse": row.s_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": get_combine_datetime(self.posting_date, self.posting_time), "voucher_type": self.doctype, "voucher_detail_no": row.name, "qty": row.transfer_qty * -1, @@ -2263,6 +2262,9 @@ class StockEntry(StockController): # in case of BOM to_warehouse = item.get("default_warehouse") + expense_account = item.get("expense_account") + if not expense_account: + expense_account = frappe.get_cached_value("Company", self.company, "stock_adjustment_account") args = { "to_warehouse": to_warehouse, "from_warehouse": "", @@ -2270,7 +2272,7 @@ class StockEntry(StockController): "item_name": item.item_name, "description": item.description, "stock_uom": item.stock_uom, - "expense_account": item.get("expense_account"), + "expense_account": expense_account, "cost_center": item.get("buying_cost_center"), "is_finished_item": 1, } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 94769ab9c24..576d129ee2d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -5,9 +5,10 @@ from typing import TYPE_CHECKING, overload import frappe -from frappe.utils import cint, flt +from frappe.utils import cint, flt, today import erpnext +from erpnext.stock.utils import get_combine_datetime if TYPE_CHECKING: from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry @@ -76,6 +77,9 @@ def make_stock_entry(**args): if args.inspection_required: s.inspection_required = args.inspection_required + if not args.posting_date: + s.posting_date = today() + # map names if args.from_warehouse: args.source = args.from_warehouse @@ -140,6 +144,10 @@ def make_stock_entry(**args): elif args.batches: batches = args.batches + posting_datetime = None + if s.posting_date and s.posting_time: + posting_datetime = get_combine_datetime(s.posting_date, s.posting_time) + bundle_id = ( SerialBatchCreation( { @@ -151,8 +159,7 @@ def make_stock_entry(**args): "serial_nos": args.serial_no, "type_of_transaction": "Outward" if args.source else "Inward", "company": s.company, - "posting_date": s.posting_date, - "posting_time": s.posting_time, + "posting_datetime": posting_datetime, "rate": args.rate or args.basic_rate, "do_not_submit": True, } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 46cb9e1ece5..205fda5c41e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -13,6 +13,7 @@ from erpnext.controllers.stock_controller import StockController, create_repost_ from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_available_serial_nos, ) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -135,8 +136,7 @@ class StockReconciliation(StockController): { "item_code": row.item_code, "warehouse": row.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, @@ -242,8 +242,7 @@ class StockReconciliation(StockController): "doctype": "Serial and Batch Bundle", "item_code": item.item_code, "warehouse": item.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "voucher_type": self.doctype, "type_of_transaction": "Outward", } @@ -261,8 +260,7 @@ class StockReconciliation(StockController): { "item_code": item.item_code, "warehouse": item.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "ignore_warehouse": 1, } ) @@ -1154,8 +1152,7 @@ class StockReconciliation(StockController): { "item_code": doc.item_code, "warehouse": doc.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": doc.posting_datetime, "creation": sle_creation, "voucher_no": self.name, "ignore_warehouse": 1, @@ -1195,8 +1192,7 @@ class StockReconciliation(StockController): d.batch_no, doc.warehouse, creation=sle_creation, - posting_date=doc.posting_date, - posting_time=doc.posting_time, + posting_datetime=doc.posting_datetime, ignore_voucher_nos=[doc.voucher_no], for_stock_levels=True, consider_negative_batches=True, diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index e7f542a9774..a6f896ff385 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -25,7 +25,12 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.tests.test_utils import StockTestMixin -from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method +from erpnext.stock.utils import ( + get_combine_datetime, + get_incoming_rate, + get_stock_value_on, + get_valuation_method, +) class TestStockReconciliation(IntegrationTestCase, StockTestMixin): @@ -716,6 +721,13 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin): rate=100, ) + stock_reco.reload() + stock_reco_sabb = stock_reco.items[0].serial_and_batch_bundle + posting_datetime = frappe.db.get_value("Serial and Batch Bundle", stock_reco_sabb, "posting_datetime") + self.assertEqual( + posting_datetime, get_combine_datetime(stock_reco.posting_date, stock_reco.posting_time) + ) + sle = frappe.get_all( "Stock Ledger Entry", filters={"is_cancelled": 0, "voucher_no": stock_reco.name, "actual_qty": ("<", 0)}, diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 2be66e59bc9..985e51d11b8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -283,10 +283,13 @@ def filter_batches(batches, doc): del batches[row.get("batch_no")] -def get_filtered_serial_nos(serial_nos, doc): +def get_filtered_serial_nos(serial_nos, doc, table=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - for row in doc.get("items"): + if not table: + table = "items" + + for row in doc.get(table): if row.get("serial_no"): for serial_no in get_serial_nos(row.get("serial_no")): if serial_no in serial_nos: diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py index d089235dbd3..431670d5775 100644 --- a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py @@ -19,7 +19,7 @@ def get_data(filters): "Serial and Batch Bundle", fields=[ "`tabSerial and Batch Bundle`.`voucher_type`", - "`tabSerial and Batch Bundle`.`posting_date`", + "`tabSerial and Batch Bundle`.`posting_datetime` as posting_date", "`tabSerial and Batch Bundle`.`name`", "`tabSerial and Batch Bundle`.`company`", "`tabSerial and Batch Bundle`.`voucher_no`", @@ -33,7 +33,7 @@ def get_data(filters): "`tabSerial and Batch Entry`.`qty`", ], filters=filter_conditions, - order_by="posting_date", + order_by="posting_datetime", ) @@ -54,7 +54,7 @@ def get_filter_conditions(filters): filter_conditions.append( [ "Serial and Batch Bundle", - "posting_date", + "posting_datetime", "between", [filters.get("from_date"), filters.get("to_date")], ] diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 0edce832aec..e2cce6e99da 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -492,6 +492,7 @@ class FIFOSlots: bundle = frappe.qb.DocType("Serial and Batch Bundle") entry = frappe.qb.DocType("Serial and Batch Entry") + to_date = get_datetime(self.filters.get("to_date") + " 23:59:59") query = ( frappe.qb.from_(bundle) .join(entry) @@ -501,7 +502,7 @@ class FIFOSlots: (bundle.docstatus == 1) & (entry.serial_no.isnotnull()) & (bundle.company == self.filters.get("company")) - & (bundle.posting_date <= self.filters.get("to_date")) + & (bundle.posting_datetime <= to_date) ) ) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 8985515967c..c0b5c79679a 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -4,7 +4,7 @@ import frappe from frappe import _, bold from frappe.model.naming import make_autoname from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp -from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today +from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowtime, today from pypika import Order from pypika.terms import ExistsCriterion @@ -100,8 +100,7 @@ class SerialBatchBundle: { "item_code": self.item_code, "warehouse": self.warehouse, - "posting_date": self.sle.posting_date, - "posting_time": self.sle.posting_time, + "posting_datetime": self.sle.posting_datetime, "voucher_type": self.sle.voucher_type, "voucher_no": self.sle.voucher_no, "voucher_detail_no": self.sle.voucher_detail_no, @@ -463,7 +462,7 @@ class SerialBatchBundle: if status == "Delivered": warranty_period = frappe.get_cached_value("Item", sle.item_code, "warranty_period") if warranty_period: - warranty_expiry_date = add_days(sle.posting_date, cint(warranty_period)) + warranty_expiry_date = add_days(getdate(sle.posting_datetime), cint(warranty_period)) query = query.set(sn_table.warranty_expiry_date, warranty_expiry_date) query = query.set(sn_table.warranty_period, warranty_period) else: @@ -488,7 +487,7 @@ class SerialBatchBundle: sle_doctype.voucher_no, sle_doctype.is_cancelled, sle_doctype.item_code, - sle_doctype.posting_date, + sle_doctype.posting_datetime, sle_doctype.company, ) .where( @@ -644,7 +643,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation): & (bundle.item_code == self.sle.item_code) & (bundle_child.warehouse == self.sle.warehouse) ) - .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc) + .orderby(bundle.posting_datetime, order=Order.desc) .limit(1) ) @@ -652,13 +651,8 @@ class SerialNoValuation(DeprecatedSerialNoValuation): if self.sle.voucher_no: query = query.where(bundle.voucher_no != self.sle.voucher_no) - if self.sle.posting_date: - if self.sle.posting_time is None: - self.sle.posting_time = nowtime() - - timestamp_condition = CombineDatetime( - bundle.posting_date, bundle.posting_time - ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time) + if self.sle.posting_datetime: + timestamp_condition = bundle.posting_datetime <= self.sle.posting_datetime query = query.where(timestamp_condition) @@ -754,19 +748,13 @@ class BatchNoValuation(DeprecatedBatchNoValuation): child = frappe.qb.DocType("Serial and Batch Entry") timestamp_condition = "" - if self.sle.posting_date: - if self.sle.posting_time is None: - self.sle.posting_time = nowtime() - - timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime( - self.sle.posting_date, self.sle.posting_time - ) + if self.sle.posting_datetime: + timestamp_condition = parent.posting_datetime < self.sle.posting_datetime if self.sle.creation: - timestamp_condition |= ( - CombineDatetime(parent.posting_date, parent.posting_time) - == CombineDatetime(self.sle.posting_date, self.sle.posting_time) - ) & (parent.creation < self.sle.creation) + timestamp_condition |= (parent.posting_datetime == self.sle.posting_datetime) & ( + parent.creation < self.sle.creation + ) query = ( frappe.qb.from_(parent) @@ -989,9 +977,9 @@ class SerialBatchCreation: self.__dict__.update(item_details) def set_other_details(self): - if not self.get("posting_date"): - self.posting_date = today() - self.__dict__["posting_date"] = self.posting_date + if not self.get("posting_datetime"): + self.posting_datetime = now() + self.__dict__["posting_datetime"] = self.posting_datetime if not self.get("actual_qty"): qty = self.get("qty") or self.get("total_qty") @@ -1016,8 +1004,7 @@ class SerialBatchCreation: new_package.docstatus = 0 new_package.warehouse = self.warehouse new_package.voucher_no = "" - new_package.posting_date = self.posting_date if hasattr(self, "posting_date") else today() - new_package.posting_time = self.posting_time if hasattr(self, "posting_time") else nowtime() + new_package.posting_datetime = self.posting_datetime if hasattr(self, "posting_datetime") else now() new_package.type_of_transaction = self.type_of_transaction new_package.returned_against = self.get("returned_against") @@ -1144,9 +1131,8 @@ class SerialBatchCreation: elif self.has_serial_no and not self.get("serial_nos"): self.serial_nos = get_serial_nos_for_outward(kwargs) elif not self.has_serial_no and self.has_batch_no and not self.get("batches"): - if self.get("posting_date"): - kwargs["posting_date"] = self.get("posting_date") - kwargs["posting_time"] = self.get("posting_time") + if self.get("posting_datetime"): + kwargs["posting_datetime"] = self.get("posting_datetime") self.batches = get_available_batches(kwargs) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 166133e3f28..10477638a03 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -729,32 +729,16 @@ class update_entries_after: self.distinct_item_warehouses[key] = val self.new_items_found = True else: - # Check if the dependent voucher is reposted - # If not, then do not add it to the list - if not self.is_dependent_voucher_reposted(dependant_sle): - return - - existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") - - dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key) - if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): - if dependent_voucher_detail_nos and dependant_sle.voucher_detail_no in set( - dependent_voucher_detail_nos - ): - return - - val.sle_changed = True - dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no) - val.dependent_voucher_detail_nos = dependent_voucher_detail_nos + existing_sle = self.distinct_item_warehouses[key].get("sle", {}) + if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date): self.distinct_item_warehouses[key] = val self.new_items_found = True - elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos): - # Future dependent voucher needs to be repost to get the correct stock value - # If dependent voucher has not reposted, then add it to the list - dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no) - self.new_items_found = True - val.dependent_voucher_detail_nos = dependent_voucher_detail_nos + elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry( + dependant_sle.voucher_no + ): + print(dependant_sle.voucher_no) self.distinct_item_warehouses[key] = val + self.new_items_found = True def is_dependent_voucher_reposted(self, dependant_sle) -> bool: # Return False if the dependent voucher is not reposted @@ -1785,6 +1769,8 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): "posting_time", "voucher_detail_no", "posting_datetime as timestamp", + "voucher_type", + "voucher_no", ], as_dict=1, ) @@ -1863,8 +1849,7 @@ def get_valuation_rate( "warehouse": warehouse, "actual_qty": -1, "serial_and_batch_bundle": serial_and_batch_bundle, - "posting_date": sabb.posting_date, - "posting_time": sabb.posting_time, + "posting_datetime": get_combine_datetime(sabb.posting_date, sabb.posting_time), } ) ) @@ -2208,8 +2193,7 @@ def validate_reserved_batch_nos(item_code, warehouse, batch_nos): { "item_code": item_code, "warehouse": warehouse, - "posting_date": nowdate(), - "posting_time": nowtime(), + "posting_datetime": get_combine_datetime(nowdate(), nowtime()), } ) ) @@ -2293,3 +2277,10 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, difference_amount = query.run() return flt(difference_amount[0][0]) if difference_amount else 0 + + +@frappe.request_cache +def is_transfer_stock_entry(voucher_no): + purpose = frappe.get_cached_value("Stock Entry", voucher_no, "purpose") + + return purpose in ["Material Transfer", "Material Transfer for Manufacture", "Send to Subcontractor"] diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 0656cd6bb41..bee190e13a1 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -10,9 +10,7 @@ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime import erpnext -from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( - get_available_serial_nos, -) +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_available_serial_nos from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation from erpnext.stock.valuation import FIFOValuation, LIFOValuation @@ -139,8 +137,7 @@ def get_stock_balance( { "item_code": item_code, "warehouse": warehouse, - "posting_date": posting_date, - "posting_time": posting_time, + "posting_datetime": get_combine_datetime(posting_date, posting_time), "ignore_warehouse": 1, } ) @@ -247,6 +244,9 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): if isinstance(args, str): args = json.loads(args) + if not args.get("posting_datetime") and args.get("posting_date"): + args["posting_datetime"] = get_combine_datetime(args.get("posting_date"), args.get("posting_time")) + in_rate = None item_details = frappe.get_cached_value( diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 949d1850053..e7555194672 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -117,7 +117,13 @@ class SubcontractingReceipt(SubcontractingController): self.validate_items_qty() self.set_items_bom() self.set_items_cost_center() - self.set_items_expense_account() + + if self.company: + default_expense_account = self.get_company_default( + "default_expense_account", ignore_validation=True + ) + self.set_service_expense_account(default_expense_account) + self.set_expense_account_for_subcontracted_items(default_expense_account) def validate(self): self.reset_supplied_items() @@ -205,6 +211,39 @@ class SubcontractingReceipt(SubcontractingController): doc = frappe.get_doc("Job Card", row.job_card) doc.set_manufactured_qty() + def set_service_expense_account(self, default_expense_account): + for row in self.get("items"): + if not row.service_expense_account and row.purchase_order_item: + service_item = frappe.db.get_value( + "Purchase Order Item", row.purchase_order_item, "item_code" + ) + + if service_item: + if default := ( + get_item_defaults(service_item, self.company) + or get_item_group_defaults(service_item, self.company) + or get_brand_defaults(service_item, self.company) + ): + if service_expense_account := default.get("expense_account"): + row.service_expense_account = service_expense_account + + if not row.service_expense_account: + row.service_expense_account = default_expense_account + + def set_expense_account_for_subcontracted_items(self, default_expense_account): + for row in self.get("items"): + if not row.expense_account: + if default := ( + get_item_defaults(row.item_code, self.company) + or get_item_group_defaults(row.item_code, self.company) + or get_brand_defaults(row.item_code, self.company) + ): + if expense_account := default.get("expense_account"): + row.expense_account = expense_account + + if not row.expense_account: + row.expense_account = default_expense_account + def get_manufactured_qty(self, job_card): table = frappe.qb.DocType("Subcontracting Receipt Item") query = ( @@ -262,14 +301,6 @@ class SubcontractingReceipt(SubcontractingController): self.company, ) - def set_items_expense_account(self): - if self.company: - expense_account = self.get_company_default("default_expense_account", ignore_validation=True) - - for item in self.items: - if not item.expense_account: - item.expense_account = expense_account - def set_supplied_items_expense_account(self): for item in self.supplied_items: if not item.expense_account: @@ -625,13 +656,17 @@ class SubcontractingReceipt(SubcontractingController): project=item.project, item=item, ) + + service_cost = flt( + item.service_cost_per_qty, item.precision("service_cost_per_qty") + ) * flt(item.qty, item.precision("qty")) # Expense Account (Credit) self.add_gl_entry( gl_entries=gl_entries, account=item.expense_account, cost_center=item.cost_center, debit=0.0, - credit=stock_value_diff, + credit=flt(stock_value_diff) - service_cost, remarks=remarks, against_account=accepted_warehouse_account, account_currency=get_account_currency(item.expense_account), @@ -639,6 +674,21 @@ class SubcontractingReceipt(SubcontractingController): item=item, ) + service_account = item.service_expense_account or item.expense_account + # Expense Account (Credit) + self.add_gl_entry( + gl_entries=gl_entries, + account=service_account, + cost_center=item.cost_center, + debit=0.0, + credit=service_cost, + remarks=remarks, + against_account=accepted_warehouse_account, + account_currency=get_account_currency(service_account), + project=item.project, + item=item, + ) + if flt(item.rm_supp_cost) and supplier_warehouse_account: for rm_item in supplied_items_details.get(item.name): # Supplier Warehouse Account (Credit) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index da3920aaa41..46b11dc29fa 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -421,6 +421,79 @@ class TestSubcontractingReceipt(IntegrationTestCase): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) + def test_subcontracting_receipt_for_service_expense_account(self): + service_expense_account = ( + frappe.get_doc( + { + "doctype": "Account", + "account_name": "_Test Service Expense", + "account_type": "Expense Account", + "company": "_Test Company with perpetual inventory", + "is_group": 0, + "parent_account": "Indirect Expenses - TCP1", + } + ) + .insert(ignore_if_duplicate=True) + .name + ) + + service_item_doc = frappe.get_doc("Item", "Subcontracted Service Item 10") + service_item_doc.append( + "item_defaults", + { + "company": "_Test Company with perpetual inventory", + "expense_account": service_expense_account, + "default_warehouse": "Stores - TCP1", + }, + ) + + service_item_doc.save() + + service_items = [ + { + "warehouse": "Stores - TCP1", + "item_code": "Subcontracted Service Item 10", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA10", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + service_items=service_items, + ) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + scr.submit() + + for item in scr.items: + self.assertEqual(item.service_expense_account, service_expense_account) + + gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) + self.assertTrue(gl_entries) + + fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse) + expense_account = scr.items[0].expense_account + expected_values = { + fg_warehouse_ac: [2000, 1000], + expense_account: [1000, 1000], + service_expense_account: [0, 1000], + } + + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.debit) + self.assertEqual(expected_values[gle.account][1], gle.credit) + @IntegrationTestCase.change_settings("Stock Settings", {"use_serial_batch_fields": 0}) def test_subcontracting_receipt_with_zero_service_cost(self): warehouse = "Stores - TCP1" @@ -739,13 +812,13 @@ class TestSubcontractingReceipt(IntegrationTestCase): for row in scr.supplied_items: self.assertEqual(row.rate, 300.00) self.assertTrue(row.serial_and_batch_bundle) - auto_created_serial_batch = frappe.db.get_value( + serial_and_batch_bundle = frappe.db.get_value( "Stock Ledger Entry", {"voucher_no": scr.name, "voucher_detail_no": row.name}, - "auto_created_serial_and_batch_bundle", + "serial_and_batch_bundle", ) - self.assertTrue(auto_created_serial_batch) + self.assertTrue(serial_and_batch_bundle) self.assertEqual(scr.items[0].rm_cost_per_qty, 900) self.assertEqual(scr.items[0].service_cost_per_qty, 100) 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 466ad8b5e9c..9c1f8e60946 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -66,6 +66,8 @@ "manufacturer_part_no", "accounting_details_section", "expense_account", + "column_break_exht", + "service_expense_account", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -597,13 +599,23 @@ "label": "Landed Cost Voucher Amount", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "column_break_exht", + "fieldtype": "Column Break" + }, + { + "fieldname": "service_expense_account", + "fieldtype": "Link", + "label": "Service Expense Account", + "options": "Account" } ], "grid_page_length": 50, "idx": 1, "istable": 1, "links": [], - "modified": "2025-06-11 08:45:18.903036", + "modified": "2025-09-26 12:00:38.877638", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py index 875a7d5477e..e916a90462f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py @@ -56,6 +56,7 @@ class SubcontractingReceiptItem(Document): serial_and_batch_bundle: DF.Link | None serial_no: DF.SmallText | None service_cost_per_qty: DF.Currency + service_expense_account: DF.Link | None stock_uom: DF.Link subcontracting_order: DF.Link | None subcontracting_order_item: DF.Data | None