diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
index cd883e5bf72..4e45dede1d5 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
@@ -57,9 +57,12 @@ frappe.ui.form.on("Accounting Dimension", {
}
},
+ label: function (frm) {
+ frm.set_value("fieldname", frappe.model.scrub(frm.doc.label));
+ },
+
document_type: function (frm) {
frm.set_value("label", frm.doc.document_type);
- frm.set_value("fieldname", frappe.model.scrub(frm.doc.document_type));
frappe.db.get_value(
"Accounting Dimension",
diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py
index d7545ef1520..11f78ae1763 100644
--- a/erpnext/accounts/doctype/budget/budget.py
+++ b/erpnext/accounts/doctype/budget/budget.py
@@ -219,12 +219,18 @@ def validate_expense_against_budget(args, expense_amount=0):
def validate_budget_records(args, budget_records, expense_amount):
for budget in budget_records:
if flt(budget.budget_amount):
- amount = expense_amount or get_amount(args, budget)
yearly_action, monthly_action = get_actions(args, budget)
+ args["for_material_request"] = budget.for_material_request
+ args["for_purchase_order"] = budget.for_purchase_order
if yearly_action in ("Stop", "Warn"):
compare_expense_with_budget(
- args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
+ args,
+ flt(budget.budget_amount),
+ _("Annual"),
+ yearly_action,
+ budget.budget_against,
+ expense_amount,
)
if monthly_action in ["Stop", "Warn"]:
@@ -240,18 +246,27 @@ def validate_budget_records(args, budget_records, expense_amount):
_("Accumulated Monthly"),
monthly_action,
budget.budget_against,
- amount,
+ expense_amount,
)
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
- actual_expense = get_actual_expense(args)
- total_expense = actual_expense + amount
+ args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
+ if not amount:
+ args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
+
+ if args.get("doctype") == "Material Request" and args.for_material_request:
+ amount = args.requested_amount + args.ordered_amount
+
+ elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
+ amount = args.ordered_amount
+
+ total_expense = args.actual_expense + amount
if total_expense > budget_amount:
- if actual_expense > budget_amount:
+ if args.actual_expense > budget_amount:
error_tense = _("is already")
- diff = actual_expense - budget_amount
+ diff = args.actual_expense - budget_amount
else:
error_tense = _("will be")
diff = total_expense - budget_amount
@@ -268,6 +283,8 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
frappe.bold(fmt_money(diff, currency=currency)),
)
+ msg += get_expense_breakup(args, currency, budget_against)
+
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
frappe.session.user
):
@@ -279,6 +296,83 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
+def get_expense_breakup(args, currency, budget_against):
+ msg = "
Total Expenses booked through - "
+
+ common_filters = frappe._dict(
+ {
+ args.budget_against_field: budget_against,
+ "account": args.account,
+ "company": args.company,
+ }
+ )
+
+ msg += (
+ "- "
+ + frappe.utils.get_link_to_report(
+ "General Ledger",
+ label="Actual Expenses",
+ filters=common_filters.copy().update(
+ {
+ "from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
+ "to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
+ "is_cancelled": 0,
+ }
+ ),
+ )
+ + " - "
+ + frappe.bold(fmt_money(args.actual_expense, currency=currency))
+ + "
"
+ )
+
+ msg += (
+ "- "
+ + frappe.utils.get_link_to_report(
+ "Material Request",
+ label="Material Requests",
+ report_type="Report Builder",
+ doctype="Material Request",
+ filters=common_filters.copy().update(
+ {
+ "status": [["!=", "Stopped"]],
+ "docstatus": 1,
+ "material_request_type": "Purchase",
+ "schedule_date": [["fiscal year", "2023-2024"]],
+ "item_code": args.item_code,
+ "per_ordered": [["<", 100]],
+ }
+ ),
+ )
+ + " - "
+ + frappe.bold(fmt_money(args.requested_amount, currency=currency))
+ + "
"
+ )
+
+ msg += (
+ "- "
+ + frappe.utils.get_link_to_report(
+ "Purchase Order",
+ label="Unbilled Orders",
+ report_type="Report Builder",
+ doctype="Purchase Order",
+ filters=common_filters.copy().update(
+ {
+ "status": [["!=", "Closed"]],
+ "docstatus": 1,
+ "transaction_date": [["fiscal year", "2023-2024"]],
+ "item_code": args.item_code,
+ "per_billed": [["<", 100]],
+ }
+ ),
+ )
+ + " - "
+ + frappe.bold(fmt_money(args.ordered_amount, currency=currency))
+ + "
"
+ )
+
+ return msg
+
+
def get_actions(args, budget):
yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
@@ -294,23 +388,9 @@ def get_actions(args, budget):
return yearly_action, monthly_action
-def get_amount(args, budget):
- amount = 0
-
- if args.get("doctype") == "Material Request" and budget.for_material_request:
- amount = (
- get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
- )
-
- elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
- amount = get_ordered_amount(args, budget) + get_actual_expense(args)
-
- return amount
-
-
-def get_requested_amount(args, budget):
+def get_requested_amount(args):
item_code = args.get("item_code")
- condition = get_other_condition(args, budget, "Material Request")
+ condition = get_other_condition(args, "Material Request")
data = frappe.db.sql(
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
@@ -324,9 +404,9 @@ def get_requested_amount(args, budget):
return data[0][0] if data else 0
-def get_ordered_amount(args, budget):
+def get_ordered_amount(args):
item_code = args.get("item_code")
- condition = get_other_condition(args, budget, "Purchase Order")
+ condition = get_other_condition(args, "Purchase Order")
data = frappe.db.sql(
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
@@ -340,7 +420,7 @@ def get_ordered_amount(args, budget):
return data[0][0] if data else 0
-def get_other_condition(args, budget, for_doc):
+def get_other_condition(args, for_doc):
condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field")
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json
index 2e66db7bcf3..55d4085c823 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.json
+++ b/erpnext/accounts/doctype/cost_center/cost_center.json
@@ -125,7 +125,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2022-01-31 13:22:58.916273",
+ "modified": "2024-04-24 10:55:54.083042",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@@ -163,6 +163,15 @@
{
"read": 1,
"role": "Purchase User"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "report": 1,
+ "role": "Employee",
+ "select": 1,
+ "share": 1
}
],
"search_fields": "parent_cost_center, is_group",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index fe2ef1c5a91..4de20a17389 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -6,7 +6,7 @@ import json
import frappe
from frappe import _, msgprint, scrub
-from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
+from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
import erpnext
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
@@ -146,6 +146,7 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table()
self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
+ self.validate_advance_accounts()
if self.docstatus == 0:
self.apply_tax_withholding()
@@ -153,6 +154,20 @@ class JournalEntry(AccountsController):
if not self.title:
self.title = self.get_title()
+ def validate_advance_accounts(self):
+ journal_accounts = set([x.account for x in self.accounts])
+ advance_accounts = set()
+ advance_accounts.add(
+ frappe.get_cached_value("Company", self.company, "default_advance_received_account")
+ )
+ advance_accounts.add(frappe.get_cached_value("Company", self.company, "default_advance_paid_account"))
+ if advance_accounts_used := journal_accounts & advance_accounts:
+ frappe.msgprint(
+ _(
+ "Making Journal Entries against advance accounts: {0} is not recommended. These Journals won't be available for Reconciliation."
+ ).format(frappe.bold(comma_and(advance_accounts_used)))
+ )
+
def validate_for_repost(self):
validate_docs_for_voucher_types(["Journal Entry"])
validate_docs_for_deferred_accounting([self.name], [])
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index a37a6fe5465..d6ba193aa0e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -150,6 +150,7 @@
"reqd": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
@@ -476,6 +477,7 @@
"label": "More Information"
},
{
+ "allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -776,7 +778,7 @@
"table_fieldname": "payment_entries"
}
],
- "modified": "2024-01-03 12:46:41.759121",
+ "modified": "2024-04-11 11:25:07.366347",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 0d1206b0cc7..9a4fbe10bd1 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -2028,6 +2028,8 @@ def get_negative_outstanding_invoices(
@frappe.whitelist()
def get_party_details(company, party_type, party, date, cost_center=None):
bank_account = ""
+ party_bank_account = ""
+
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
@@ -2039,8 +2041,8 @@ def get_party_details(company, party_type, party, date, cost_center=None):
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party)
+ bank_account = get_default_company_bank_account(company, party_type, party)
- bank_account = get_default_company_bank_account(company, party_type, party)
return {
"party_account": party_account,
"party_name": party_name,
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 3e24776cea7..cafdaaaa957 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -10,6 +10,7 @@ from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_outstanding_reference_documents,
+ get_party_details,
get_payment_entry,
get_reference_details,
)
@@ -1684,6 +1685,10 @@ def create_payment_entry(**args):
payment_entry.reference_no = "Test001"
payment_entry.reference_date = nowdate()
+ get_party_details(
+ payment_entry.company, payment_entry.party_type, payment_entry.party, payment_entry.posting_date
+ )
+
if args.get("save"):
payment_entry.save()
if args.get("submit"):
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js
index f009de59592..4033fc08233 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.js
+++ b/erpnext/accounts/doctype/payment_order/payment_order.js
@@ -71,6 +71,7 @@ frappe.ui.form.on("Payment Order", {
target: frm,
date_field: "posting_date",
setters: {
+ party_type: "Supplier",
party: frm.doc.supplier || "",
},
get_query_filters: {
@@ -91,6 +92,7 @@ frappe.ui.form.on("Payment Order", {
source_doctype: "Payment Request",
target: frm,
setters: {
+ party_type: "Supplier",
party: frm.doc.supplier || "",
},
get_query_filters: {
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index 666926f00eb..219d6089a57 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -195,6 +195,8 @@
},
{
"depends_on": "eval:doc.party",
+ "description": "Only 'Payment Entries' made against this advance account are supported.",
+ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
"fieldname": "default_advance_account",
"fieldtype": "Link",
"label": "Default Advance Account",
@@ -229,7 +231,7 @@
"is_virtual": 1,
"issingle": 1,
"links": [],
- "modified": "2023-12-14 13:38:16.264013",
+ "modified": "2024-04-23 12:38:29.557315",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 9e8bf78a2f1..cf08d08ce4b 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -299,6 +299,7 @@
"remember_last_selected_value": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
@@ -1367,6 +1368,7 @@
"read_only": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -1637,7 +1639,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2024-03-20 15:57:00.736868",
+ "modified": "2024-04-11 11:28:42.802211",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index aa7573e21c1..dbc9ab474d2 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -68,15 +68,11 @@ class PurchaseInvoice(BuyingController):
from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import (
PurchaseInvoiceAdvance,
)
- from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import (
- PurchaseInvoiceItem,
- )
+ from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import PurchaseInvoiceItem
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
PurchaseTaxesandCharges,
)
- from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import (
- TaxWithheldVouchers,
- )
+ from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
PurchaseReceiptItemSupplied,
)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 4a3e1758752..51da848d9da 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -289,6 +289,7 @@
"read_only": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"hide_days": 1,
@@ -354,6 +355,7 @@
"reqd": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"hide_days": 1,
@@ -2185,7 +2187,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2024-03-22 17:50:34.395602",
+ "modified": "2024-04-11 11:30:26.272441",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2240,4 +2242,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 530bea416bf..abdfcaeb512 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -55,13 +55,9 @@ class SalesInvoice(SellingController):
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
- from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
- SalesInvoiceAdvance,
- )
+ from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem
- from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
- SalesInvoicePayment,
- )
+ from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
SalesInvoiceTimesheet,
)
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 722082fe0a4..70cd8ce1f97 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -751,52 +751,6 @@ def validate_party_frozen_disabled(party_type, party_name):
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
-def get_timeline_data(doctype, name):
- """returns timeline data for the past one year"""
- from frappe.desk.form.load import get_communication_data
-
- out = {}
- after = add_years(None, -1).strftime("%Y-%m-%d")
-
- data = get_communication_data(
- doctype,
- name,
- after=after,
- group_by="group by communication_date",
- fields="C.communication_date as communication_date, count(C.name)",
- as_dict=False,
- )
-
- # fetch and append data from Activity Log
- activity_log = frappe.qb.DocType("Activity Log")
- data += (
- frappe.qb.from_(activity_log)
- .select(activity_log.communication_date, Count(activity_log.name))
- .where(
- (
- ((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name))
- | ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name))
- | (
- (activity_log.reference_doctype.isin(["Quotation", "Opportunity"]))
- & (activity_log.timeline_name == name)
- )
- )
- & (activity_log.status != "Success")
- & (activity_log.creation > after)
- )
- .groupby(activity_log.communication_date)
- .orderby(activity_log.communication_date, order=frappe.qb.desc)
- ).run()
-
- timeline_items = dict(data)
-
- for date, count in timeline_items.items():
- timestamp = get_timestamp(date)
- out.update({timestamp: count})
-
- return out
-
-
def get_dashboard_info(party_type, party, loyalty_program=None):
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 3a5acbe0322..278da1b08bf 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -305,6 +305,7 @@ def create_asset_repair(**args):
"serial_nos": args.serial_no,
"posting_date": today(),
"posting_time": nowtime(),
+ "do_not_submit": 1,
}
)
).name
diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py
index 3bd306e6591..9f424dd0c7c 100644
--- a/erpnext/buying/doctype/supplier/supplier_dashboard.py
+++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py
@@ -3,10 +3,6 @@ from frappe import _
def get_data():
return {
- "heatmap": True,
- "heatmap_message": _(
- "This is based on transactions against this Supplier. See timeline below for details"
- ),
"fieldname": "supplier",
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
"transactions": [
diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js
index f7d0d947b61..9701e147f05 100644
--- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js
+++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js
@@ -133,6 +133,13 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
return row.supplier_name;
});
+ let items = [];
+ report.data.forEach((d) => {
+ if (!items.includes(d.item_code)) {
+ items.push(d.item_code);
+ }
+ });
+
// Create a dialog window for the user to pick their supplier
let dialog = new frappe.ui.Dialog({
title: __("Select Default Supplier"),
@@ -151,20 +158,34 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
};
},
},
+ {
+ reqd: 1,
+ label: "Item",
+ fieldtype: "Link",
+ options: "Item",
+ fieldname: "item_code",
+ get_query: () => {
+ return {
+ filters: {
+ name: ["in", items],
+ },
+ };
+ },
+ },
],
});
dialog.set_primary_action(__("Set Default Supplier"), () => {
let values = dialog.get_values();
+
if (values) {
// Set the default_supplier field of the appropriate Item to the selected supplier
frappe.call({
- method: "frappe.client.set_value",
+ method: "erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison.set_default_supplier",
args: {
- doctype: "Item",
- name: item_code,
- fieldname: "default_supplier",
- value: values.supplier,
+ item_code: values.item_code,
+ supplier: values.supplier,
+ company: filters.company,
},
freeze: true,
callback: (r) => {
diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py
index 684cd3a0f9e..085f30f84d9 100644
--- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py
+++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py
@@ -292,3 +292,13 @@ def get_message():
Expires today / Already Expired
"""
+
+
+@frappe.whitelist()
+def set_default_supplier(item_code, supplier, company):
+ frappe.db.set_value(
+ "Item Default",
+ {"parent": item_code, "company": company},
+ "default_supplier",
+ supplier,
+ )
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 80b156487bd..a378d8ae606 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1896,7 +1896,7 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page
query = query.where(so_table.name.isin(filters.get("sales_orders")))
if txt:
- query = query.where(table.item_code.like(f"{txt}%"))
+ query = query.where(table.parent.like(f"%{txt}%"))
if page_len:
query = query.limit(page_len)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index e13a07b83e8..904e42a6a03 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -948,6 +948,21 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
+ if (
+ self.stock_uom
+ and frappe.get_cached_value("UOM", self.stock_uom, "must_be_whole_number")
+ and abs(cint(self.qty) - flt(self.qty, self.precision("qty"))) > 0.0000001
+ ):
+ frappe.throw(
+ _(
+ "Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}."
+ ).format(
+ flt(self.qty, self.precision("qty")),
+ frappe.bold(_("Must be Whole Number")),
+ frappe.bold(self.stock_uom),
+ ),
+ )
+
if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item:
qty_dict = frappe.db.get_value(
"Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 5917e9b5d26..464b1c9d7a8 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -454,7 +454,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2024-01-08 16:01:34.598258",
+ "modified": "2024-04-24 10:56:16.001032",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -489,6 +489,15 @@
"role": "Projects Manager",
"share": 1,
"write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "report": 1,
+ "role": "Employee",
+ "select": 1,
+ "share": 1
}
],
"quick_entry": 1,
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 42d4cc51c40..288b2f6932d 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -906,11 +906,14 @@ erpnext.utils.map_current_doc = function (opts) {
if (opts.source_doctype) {
let data_fields = [];
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
- data_fields.push({
- fieldname: "merge_taxes",
- fieldtype: "Check",
- label: __("Merge taxes from multiple documents"),
- });
+ let target_meta = frappe.get_meta(cur_frm.doc.doctype);
+ if (target_meta.fields.find((f) => f.fieldname === "taxes")) {
+ data_fields.push({
+ fieldname: "merge_taxes",
+ fieldtype: "Check",
+ label: __("Merge taxes from multiple documents"),
+ });
+ }
}
const d = new frappe.ui.form.MultiSelectDialog({
doctype: opts.source_doctype,
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
index d332b4e76bd..b94cfe673b6 100644
--- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
@@ -135,14 +135,51 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-04-18 08:25:35.302081",
+ "modified": "2024-04-18 15:25:25.808355",
"modified_by": "Administrator",
"module": "Regional",
"name": "Lower Deduction Certificate",
"naming_rule": "By fieldname",
"owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py
index 1b2296381e8..161a3ba0c50 100644
--- a/erpnext/selling/doctype/customer/customer_dashboard.py
+++ b/erpnext/selling/doctype/customer/customer_dashboard.py
@@ -3,10 +3,6 @@ from frappe import _
def get_data():
return {
- "heatmap": True,
- "heatmap_message": _(
- "This is based on transactions against this Customer. See timeline below for details"
- ),
"fieldname": "customer",
"non_standard_fieldnames": {
"Payment Entry": "party",
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 24d7da45b84..c222d6b96a7 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -707,6 +707,8 @@
},
{
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
+ "description": "Only 'Payment Entries' made against this advance account are supported.",
+ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
"fieldname": "default_advance_received_account",
"fieldtype": "Link",
"label": "Default Advance Received Account",
@@ -715,6 +717,8 @@
},
{
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
+ "description": "Only 'Payment Entries' made against this advance account are supported.",
+ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
"fieldname": "default_advance_paid_account",
"fieldtype": "Link",
"label": "Default Advance Paid Account",
@@ -782,7 +786,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2023-09-10 21:53:13.860791",
+ "modified": "2024-04-23 12:38:33.173938",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 7181e90139e..3523cac822e 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -35,7 +35,7 @@ class Company(NestedSet):
auto_exchange_rate_revaluation: DF.Check
book_advance_payments_in_separate_party_account: DF.Check
capital_work_in_progress_account: DF.Link | None
- chart_of_accounts: DF.Literal
+ chart_of_accounts: DF.Literal[None]
company_description: DF.TextEditor | None
company_logo: DF.AttachImage | None
company_name: DF.Data
diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js
index d165d429f44..7a1efa82fa0 100755
--- a/erpnext/setup/doctype/employee/employee.js
+++ b/erpnext/setup/doctype/employee/employee.js
@@ -18,18 +18,6 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo
refresh() {
erpnext.toggle_naming_series();
}
-
- salutation() {
- if (this.frm.doc.salutation) {
- this.frm.set_value(
- "gender",
- {
- Mr: "Male",
- Ms: "Female",
- }[this.frm.doc.salutation]
- );
- }
- }
};
frappe.ui.form.on("Employee", {
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 3f37b7bf098..3ef0e57c25a 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -86,6 +86,7 @@ class TestBatch(FrappeTestCase):
"batches": frappe._dict({batch_no: 20}),
"type_of_transaction": "Inward",
"company": receipt.company,
+ "do_not_submit": 1,
}
)
.make_serial_and_batch_bundle()
@@ -176,6 +177,7 @@ class TestBatch(FrappeTestCase):
"batches": frappe._dict({batch_no: batch_qty}),
"type_of_transaction": "Outward",
"company": receipt.company,
+ "do_not_submit": 1,
}
)
.make_serial_and_batch_bundle()
@@ -249,6 +251,7 @@ class TestBatch(FrappeTestCase):
"batches": frappe._dict({batch_no: batch_qty}),
"type_of_transaction": "Outward",
"company": receipt.company,
+ "do_not_submit": 1,
}
)
.make_serial_and_batch_bundle()
@@ -341,6 +344,7 @@ class TestBatch(FrappeTestCase):
"batches": frappe._dict({batch_name: 90}),
"type_of_transaction": "Inward",
"company": "_Test Company",
+ "do_not_submit": 1,
}
).make_serial_and_batch_bundle()
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 1337c4eae16..bce87b80c51 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -1099,7 +1099,7 @@ def make_delivery_trip(source_name, target_doc=None):
@frappe.whitelist()
-def make_installation_note(source_name, target_doc=None):
+def make_installation_note(source_name, target_doc=None, kwargs=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.installed_qty)
target.serial_no = obj.serial_no
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 6dc92772d8c..2e751ad5251 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -238,8 +238,7 @@ class PurchaseReceipt(BuyingController):
self.po_required()
self.validate_items_quality_inspection()
self.validate_with_previous_doc()
- self.validate_uom_is_integer("uom", ["qty", "received_qty"])
- self.validate_uom_is_integer("stock_uom", "stock_qty")
+ self.validate_uom_is_integer()
self.validate_cwip_accounts()
self.validate_provisional_expense_account()
@@ -253,6 +252,10 @@ class PurchaseReceipt(BuyingController):
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+ def validate_uom_is_integer(self):
+ super().validate_uom_is_integer("uom", ["qty", "received_qty"], "Purchase Receipt Item")
+ super().validate_uom_is_integer("stock_uom", "stock_qty", "Purchase Receipt Item")
+
def validate_cwip_accounts(self):
for item in self.get("items"):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index e67736253cd..54a695126c7 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2968,6 +2968,7 @@ def make_purchase_receipt(**args):
"serial_nos": serial_nos,
"posting_date": args.posting_date or today(),
"posting_time": args.posting_time,
+ "do_not_submit": 1,
}
)
).name
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 286a220c5dd..91951834bf9 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
@@ -596,6 +596,13 @@ class SerialandBatchBundle(Document):
serial_batches = {}
for row in self.entries:
+ if not row.qty and row.batch_no and not row.serial_no:
+ frappe.throw(
+ _("At row {0}: Qty is mandatory for the batch {1}").format(
+ bold(row.idx), bold(row.batch_no)
+ )
+ )
+
if self.has_serial_no and not row.serial_no:
frappe.throw(
_("At row {0}: Serial No is mandatory for Item {1}").format(
@@ -831,7 +838,12 @@ class SerialandBatchBundle(Document):
for batch in batches:
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
+ def validate_serial_and_batch_data(self):
+ if not self.voucher_no:
+ frappe.throw(_("Voucher No is mandatory"))
+
def before_submit(self):
+ self.validate_serial_and_batch_data()
self.validate_serial_and_batch_no_for_returned()
self.set_purchase_document_no()
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index ce50155ffd5..069192fdb16 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -1483,7 +1483,7 @@ def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list
"posting_date": dn.posting_date,
"posting_time": dn.posting_time,
"voucher_type": "Delivery Note",
- "do_not_submit": dn.name,
+ "do_not_submit": 1,
}
)
).name
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 64a6b6503cc..f92d7361f41 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -823,11 +823,9 @@ class StockReconciliation(StockController):
else:
self._cancel()
- def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
+ def recalculate_current_qty(self, voucher_detail_no):
from erpnext.stock.stock_ledger import get_valuation_rate
- sl_entries = []
-
for row in self.items:
if voucher_detail_no != row.name:
continue
@@ -881,32 +879,6 @@ class StockReconciliation(StockController):
}
)
- if (
- add_new_sle
- and not frappe.db.get_value(
- "Stock Ledger Entry",
- {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
- "name",
- )
- and (not row.current_serial_and_batch_bundle)
- ):
- self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
- row.reload()
-
- if row.current_qty > 0 and row.current_serial_and_batch_bundle:
- new_sle = self.get_sle_for_items(row)
- new_sle.actual_qty = row.current_qty * -1
- new_sle.valuation_rate = row.current_valuation_rate
- new_sle.creation_time = add_to_date(sle_creation, seconds=-1)
- new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
- new_sle.qty_after_transaction = 0.0
- sl_entries.append(new_sle)
-
- if sl_entries:
- self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed())
- if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}):
- self.repost_future_sle_and_gle(force=True)
-
def has_negative_stock_allowed(self):
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if allow_negative_stock:
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 0ffcdd55fc8..92a931036e9 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -756,66 +756,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
- def test_backdated_stock_reco_entry_with_batch(self):
- item_code = self.make_item(
- "Test New Batch Item ABCVSD",
- {
- "is_stock_item": 1,
- "has_batch_no": 1,
- "batch_number_series": "BNS9.####",
- "create_new_batch": 1,
- },
- ).name
-
- warehouse = "_Test Warehouse - _TC"
-
- # Stock Reco for 100, Balace Qty 100
- stock_reco = create_stock_reconciliation(
- item_code=item_code,
- posting_date=nowdate(),
- posting_time="11:00:00",
- warehouse=warehouse,
- qty=100,
- rate=100,
- )
-
- sles = frappe.get_all(
- "Stock Ledger Entry",
- fields=["actual_qty"],
- filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
- )
-
- self.assertEqual(len(sles), 1)
-
- stock_reco.reload()
- batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle)
-
- # Stock Reco for 100, Balace Qty 100
- stock_reco1 = create_stock_reconciliation(
- item_code=item_code,
- posting_date=add_days(nowdate(), -1),
- posting_time="11:00:00",
- batch_no=batch_no,
- warehouse=warehouse,
- qty=60,
- rate=100,
- )
-
- sles = frappe.get_all(
- "Stock Ledger Entry",
- fields=["actual_qty"],
- filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
- )
-
- stock_reco1.reload()
- get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle)
-
- self.assertEqual(len(sles), 2)
-
- for row in sles:
- if row.actual_qty < 0:
- self.assertEqual(row.actual_qty, -60)
-
def test_update_stock_reconciliation_while_reposting(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -986,6 +926,150 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code})
self.assertEqual(len(active_serial_no), 5)
+ def test_balance_qty_for_batch_with_backdated_stock_reco_and_future_entries(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+ item = self.make_item(
+ "Test Batch Item Original Test",
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TEST-BATCH-SRWFEE-.###",
+ },
+ )
+
+ warehouse = "_Test Warehouse - _TC"
+ se1 = make_stock_entry(
+ item_code=item.name,
+ target=warehouse,
+ qty=50,
+ basic_rate=100,
+ posting_date=add_days(nowdate(), -2),
+ )
+ batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
+
+ se2 = make_stock_entry(
+ item_code=item.name,
+ target=warehouse,
+ qty=50,
+ basic_rate=100,
+ posting_date=add_days(nowdate(), -2),
+ )
+ batch2 = get_batch_from_bundle(se2.items[0].serial_and_batch_bundle)
+
+ se3 = make_stock_entry(
+ item_code=item.name,
+ target=warehouse,
+ qty=100,
+ basic_rate=100,
+ posting_date=add_days(nowdate(), -2),
+ )
+ batch3 = get_batch_from_bundle(se3.items[0].serial_and_batch_bundle)
+
+ se3 = make_stock_entry(
+ item_code=item.name,
+ target=warehouse,
+ qty=100,
+ basic_rate=100,
+ posting_date=nowdate(),
+ )
+
+ sle = frappe.get_all(
+ "Stock Ledger Entry",
+ filters={
+ "item_code": item.name,
+ "warehouse": warehouse,
+ "is_cancelled": 0,
+ "voucher_no": se3.name,
+ },
+ fields=["qty_after_transaction"],
+ order_by="posting_time desc, creation desc",
+ )
+
+ self.assertEqual(flt(sle[0].qty_after_transaction), flt(300.0))
+
+ sr = create_stock_reconciliation(
+ item_code=item.name,
+ warehouse=warehouse,
+ qty=0,
+ batch_no=batch1,
+ posting_date=add_days(nowdate(), -1),
+ use_serial_batch_fields=1,
+ do_not_save=1,
+ )
+
+ for batch in [batch2, batch3]:
+ sr.append(
+ "items",
+ {
+ "item_code": item.name,
+ "warehouse": warehouse,
+ "qty": 0,
+ "batch_no": batch,
+ "use_serial_batch_fields": 1,
+ },
+ )
+
+ sr.save()
+ sr.submit()
+
+ sle = frappe.get_all(
+ "Stock Ledger Entry",
+ filters={
+ "item_code": item.name,
+ "warehouse": warehouse,
+ "is_cancelled": 0,
+ "voucher_no": se3.name,
+ },
+ fields=["qty_after_transaction"],
+ order_by="posting_time desc, creation desc",
+ )
+
+ self.assertEqual(flt(sle[0].qty_after_transaction), flt(100.0))
+
+ def test_stock_reco_and_backdated_purchase_receipt(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+ item = self.make_item(
+ "Test Batch Item Original STOCK RECO Test",
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TEST-BATCH-SRCOSRWFEE-.###",
+ },
+ )
+
+ warehouse = "_Test Warehouse - _TC"
+
+ sr = create_stock_reconciliation(
+ item_code=item.name,
+ warehouse=warehouse,
+ qty=100,
+ rate=100,
+ )
+
+ sr.reload()
+ self.assertTrue(sr.items[0].serial_and_batch_bundle)
+ self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
+ batch = get_batch_from_bundle(sr.items[0].serial_and_batch_bundle)
+
+ se1 = make_stock_entry(
+ item_code=item.name,
+ target=warehouse,
+ qty=50,
+ basic_rate=100,
+ posting_date=add_days(nowdate(), -2),
+ )
+
+ batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
+ self.assertFalse(batch1 == batch)
+
+ sr.reload()
+ self.assertTrue(sr.items[0].serial_and_batch_bundle)
+ self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
+
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
@@ -1085,7 +1169,7 @@ def create_stock_reconciliation(**args):
)
bundle_id = None
- if not args.use_serial_batch_fields and (args.batch_no or args.serial_no):
+ if not args.use_serial_batch_fields and (args.batch_no or args.serial_no) and args.qty:
batches = frappe._dict({})
if args.batch_no:
batches[args.batch_no] = args.qty
diff --git a/erpnext/stock/report/available_batch_report/__init__.py b/erpnext/stock/report/available_batch_report/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.js b/erpnext/stock/report/available_batch_report/available_batch_report.js
new file mode 100644
index 00000000000..011f7e09ca2
--- /dev/null
+++ b/erpnext/stock/report/available_batch_report/available_batch_report.js
@@ -0,0 +1,91 @@
+// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.query_reports["Available Batch Report"] = {
+ filters: [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Company",
+ default: frappe.defaults.get_default("company"),
+ },
+ {
+ fieldname: "to_date",
+ label: __("On This Date"),
+ fieldtype: "Date",
+ width: "80",
+ reqd: 1,
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ },
+ {
+ fieldname: "item_code",
+ label: __("Item"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Item",
+ get_query: () => {
+ return {
+ filters: {
+ has_batch_no: 1,
+ disabled: 0,
+ },
+ };
+ },
+ },
+ {
+ fieldname: "warehouse",
+ label: __("Warehouse"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Warehouse",
+ get_query: () => {
+ let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
+ let company = frappe.query_report.get_filter_value("company");
+
+ return {
+ filters: {
+ ...(warehouse_type && { warehouse_type }),
+ ...(company && { company }),
+ },
+ };
+ },
+ },
+ {
+ fieldname: "warehouse_type",
+ label: __("Warehouse Type"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Warehouse Type",
+ },
+ {
+ fieldname: "batch_no",
+ label: __("Batch No"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Batch",
+ get_query: () => {
+ let item = frappe.query_report.get_filter_value("item_code");
+
+ return {
+ filters: {
+ ...(item && { item }),
+ },
+ };
+ },
+ },
+ {
+ fieldname: "include_expired_batches",
+ label: __("Include Expired Batches"),
+ fieldtype: "Check",
+ width: "80",
+ },
+ {
+ fieldname: "show_item_name",
+ label: __("Show Item Name"),
+ fieldtype: "Check",
+ width: "80",
+ },
+ ],
+};
diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.json b/erpnext/stock/report/available_batch_report/available_batch_report.json
new file mode 100644
index 00000000000..ddc03120e92
--- /dev/null
+++ b/erpnext/stock/report/available_batch_report/available_batch_report.json
@@ -0,0 +1,31 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2024-04-11 17:03:32.253275",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "letter_head": "",
+ "letterhead": null,
+ "modified": "2024-04-23 17:18:19.779036",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Available Batch Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Available Batch Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Accounts Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.py b/erpnext/stock/report/available_batch_report/available_batch_report.py
new file mode 100644
index 00000000000..07fcf36c827
--- /dev/null
+++ b/erpnext/stock/report/available_batch_report/available_batch_report.py
@@ -0,0 +1,178 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from collections import defaultdict
+
+import frappe
+from frappe import _
+from frappe.query_builder.functions import Sum
+from frappe.utils import flt, today
+
+
+def execute(filters=None):
+ columns, data = [], []
+ data = get_data(filters)
+ columns = get_columns(filters)
+ return columns, data
+
+
+def get_columns(filters):
+ columns = [
+ {
+ "label": _("Item Code"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 200,
+ }
+ ]
+
+ if filters.show_item_name:
+ columns.append(
+ {
+ "label": _("Item Name"),
+ "fieldname": "item_name",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 200,
+ }
+ )
+
+ columns.extend(
+ [
+ {
+ "label": _("Warehouse"),
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "width": 200,
+ },
+ {
+ "label": _("Batch No"),
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "width": 150,
+ "options": "Batch",
+ },
+ {"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150},
+ ]
+ )
+
+ return columns
+
+
+def get_data(filters):
+ data = []
+ batchwise_data = get_batchwise_data_from_stock_ledger(filters)
+ batchwise_data = get_batchwise_data_from_serial_batch_bundle(batchwise_data, filters)
+
+ data = parse_batchwise_data(batchwise_data)
+
+ return data
+
+
+def parse_batchwise_data(batchwise_data):
+ data = []
+ for key in batchwise_data:
+ d = batchwise_data[key]
+ if d.balance_qty == 0:
+ continue
+
+ data.append(d)
+
+ return data
+
+
+def get_batchwise_data_from_stock_ledger(filters):
+ batchwise_data = frappe._dict({})
+
+ table = frappe.qb.DocType("Stock Ledger Entry")
+ batch = frappe.qb.DocType("Batch")
+
+ query = (
+ frappe.qb.from_(table)
+ .inner_join(batch)
+ .on(table.batch_no == batch.name)
+ .select(
+ table.item_code,
+ table.batch_no,
+ table.warehouse,
+ Sum(table.actual_qty).as_("balance_qty"),
+ )
+ .where(table.is_cancelled == 0)
+ .groupby(table.batch_no, table.item_code, table.warehouse)
+ )
+
+ query = get_query_based_on_filters(query, batch, table, filters)
+
+ for d in query.run(as_dict=True):
+ key = (d.item_code, d.warehouse, d.batch_no)
+ batchwise_data.setdefault(key, d)
+
+ return batchwise_data
+
+
+def get_batchwise_data_from_serial_batch_bundle(batchwise_data, filters):
+ table = frappe.qb.DocType("Stock Ledger Entry")
+ ch_table = frappe.qb.DocType("Serial and Batch Entry")
+ batch = frappe.qb.DocType("Batch")
+
+ query = (
+ frappe.qb.from_(table)
+ .inner_join(ch_table)
+ .on(table.serial_and_batch_bundle == ch_table.parent)
+ .inner_join(batch)
+ .on(ch_table.batch_no == batch.name)
+ .select(
+ table.item_code,
+ ch_table.batch_no,
+ table.warehouse,
+ Sum(ch_table.qty).as_("balance_qty"),
+ )
+ .where((table.is_cancelled == 0) & (table.docstatus == 1))
+ .groupby(ch_table.batch_no, table.item_code, ch_table.warehouse)
+ )
+
+ query = get_query_based_on_filters(query, batch, table, filters)
+
+ for d in query.run(as_dict=True):
+ key = (d.item_code, d.warehouse, d.batch_no)
+ if key in batchwise_data:
+ batchwise_data[key].balance_qty += flt(d.balance_qty)
+ else:
+ batchwise_data.setdefault(key, d)
+
+ return batchwise_data
+
+
+def get_query_based_on_filters(query, batch, table, filters):
+ if filters.item_code:
+ query = query.where(table.item_code == filters.item_code)
+
+ if filters.batch_no:
+ query = query.where(batch.name == filters.batch_no)
+
+ if not filters.include_expired_batches:
+ query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull()))
+ if filters.to_date == today():
+ query = query.where(batch.batch_qty > 0)
+
+ if filters.warehouse:
+ lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"])
+ warehouses = frappe.get_all(
+ "Warehouse", filters={"lft": (">=", lft), "rgt": ("<=", rgt), "is_group": 0}, pluck="name"
+ )
+
+ query = query.where(table.warehouse.isin(warehouses))
+
+ elif filters.warehouse_type:
+ warehouses = frappe.get_all(
+ "Warehouse", filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, pluck="name"
+ )
+
+ query = query.where(table.warehouse.isin(warehouses))
+
+ if filters.show_item_name:
+ query = query.select(batch.item_name)
+
+ return query
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 97e2f5579d7..04cd1b846b3 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -61,6 +61,8 @@ def execute(filters=None):
actual_qty += flt(sle.actual_qty, precision)
stock_value += sle.stock_value_difference
batch_balance_dict[sle.batch_no] += sle.actual_qty
+ if filters.get("segregate_serial_batch_bundle"):
+ actual_qty = batch_balance_dict[sle.batch_no]
if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty:
actual_qty = sle.qty_after_transaction
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index ec4c3682cb2..5c5fd83af2f 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -840,7 +840,7 @@ class update_entries_after:
def reset_actual_qty_for_stock_reco(self, sle):
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
- doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
+ doc.recalculate_current_qty(sle.voucher_detail_no)
if sle.actual_qty < 0:
sle.actual_qty = (
@@ -1728,6 +1728,10 @@ def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
else:
stock_reco_qty_shift = flt(args.actual_qty)
+
+ elif args.get("serial_and_batch_bundle"):
+ stock_reco_qty_shift = flt(args.actual_qty)
+
else:
# reco is being submitted
last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
@@ -1799,7 +1803,16 @@ def get_datetime_limit_condition(detail):
def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
return
- if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
+
+ if (
+ args.voucher_type == "Stock Reconciliation"
+ and args.actual_qty < 0
+ and args.get("serial_and_batch_bundle")
+ and frappe.db.get_value("Stock Reconciliation Item", args.voucher_detail_no, "qty") > 0
+ ):
+ return
+
+ if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
return
neg_sle = get_future_sle_with_negative_qty(args)
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index d89095ef3d3..3b7812f96c2 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -30,8 +30,8 @@ class TransactionBase(StatusUpdater):
except ValueError:
frappe.throw(_("Invalid Posting Time"))
- def validate_uom_is_integer(self, uom_field, qty_fields):
- validate_uom_is_integer(self, uom_field, qty_fields)
+ def validate_uom_is_integer(self, uom_field, qty_fields, child_dt=None):
+ validate_uom_is_integer(self, uom_field, qty_fields, child_dt)
def validate_with_previous_doc(self, ref):
self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else []
@@ -210,12 +210,13 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
for f in qty_fields:
qty = d.get(f)
if qty:
- if abs(cint(qty) - flt(qty, d.precision(f))) > 0.0000001:
+ precision = d.precision(f)
+ if abs(cint(qty) - flt(qty, precision)) > 0.0000001:
frappe.throw(
_(
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}."
).format(
- flt(qty, d.precision(f)),
+ flt(qty, precision),
d.idx,
frappe.bold(_("Must be Whole Number")),
frappe.bold(d.get(uom_field)),