diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py
index f7b2c377f3a..22b8d64971c 100644
--- a/erpnext/accounts/doctype/budget/budget.py
+++ b/erpnext/accounts/doctype/budget/budget.py
@@ -194,12 +194,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"]:
@@ -215,18 +221,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
@@ -243,6 +258,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
):
@@ -254,6 +271,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
@@ -269,23 +363,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
@@ -299,9 +379,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
@@ -315,7 +395,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 7cbb290947e..135f5fdcb3e 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/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
index 5ab91f2506c..ff4ac50850f 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
@@ -118,9 +118,17 @@
{
"read": 1,
"role": "Employee"
+ },
+ {
+ "read": 1,
+ "role": "Accounts Manager"
+ },
+ {
+ "read": 1,
+ "role": "Stock Manager"
}
],
"show_name_in_global_search": 1,
"sort_field": "name",
"sort_order": "DESC"
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js
index 9d4988c19c9..a041f290639 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_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 3291bcb2612..583735b1cc6 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -37,7 +37,7 @@ class PaymentRequest(Document):
self.status = "Draft"
self.validate_reference_document()
self.validate_payment_request_amount()
- self.validate_currency()
+ # self.validate_currency()
self.validate_subscription_details()
def validate_reference_document(self):
@@ -276,21 +276,17 @@ class PaymentRequest(Document):
}
)
+ if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
+ amount = payment_entry.base_paid_amount
+ else:
+ amount = self.grand_total
+
+ payment_entry.received_amount = amount
+ payment_entry.get("references")[0].allocated_amount = amount
+
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
- if payment_entry.difference_amount:
- company_details = get_company_defaults(ref_doc.company)
-
- payment_entry.append(
- "deductions",
- {
- "account": company_details.exchange_gain_loss_account,
- "cost_center": company_details.cost_center,
- "amount": payment_entry.difference_amount,
- },
- )
-
if submit:
payment_entry.insert(ignore_permissions=True)
payment_entry.submit()
@@ -432,6 +428,12 @@ def make_payment_request(**args):
pr = frappe.get_doc("Payment Request", draft_payment_request)
else:
pr = frappe.new_doc("Payment Request")
+
+ if not args.get("payment_request_type"):
+ args["payment_request_type"] = (
+ "Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
+ )
+
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -490,9 +492,9 @@ def get_amount(ref_doc, payment_account=None):
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
- grand_total = flt(ref_doc.outstanding_amount)
+ grand_total = flt(ref_doc.grand_total)
else:
- grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
+ grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 70de886ba4d..932060895b0 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase):
pr = make_payment_request(
dt="Purchase Invoice",
dn=si_usd.name,
+ party_type="Supplier",
+ party="_Test Supplier USD",
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
@@ -98,6 +100,51 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.status, "Paid")
+ def test_multiple_payment_entry_against_purchase_invoice(self):
+ purchase_invoice = make_purchase_invoice(
+ customer="_Test Supplier USD",
+ debit_to="_Test Payable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
+
+ pr = make_payment_request(
+ dt="Purchase Invoice",
+ party_type="Supplier",
+ party="_Test Supplier USD",
+ dn=purchase_invoice.name,
+ recipient_id="user@example.com",
+ mute_email=1,
+ payment_gateway_account="_Test Gateway - USD",
+ return_doc=1,
+ )
+
+ pr.grand_total = pr.grand_total / 2
+
+ pr.submit()
+ pr.create_payment_entry()
+
+ purchase_invoice.load_from_db()
+ self.assertEqual(purchase_invoice.status, "Partly Paid")
+
+ pr = make_payment_request(
+ dt="Purchase Invoice",
+ party_type="Supplier",
+ party="_Test Supplier USD",
+ dn=purchase_invoice.name,
+ recipient_id="user@example.com",
+ mute_email=1,
+ payment_gateway_account="_Test Gateway - USD",
+ return_doc=1,
+ )
+
+ pr.save()
+ pr.submit()
+ pr.create_payment_entry()
+
+ purchase_invoice.load_from_db()
+ self.assertEqual(purchase_invoice.status, "Paid")
+
def test_payment_entry(self):
frappe.db.set_value(
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 8238af0ad16..2c74088a19e 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -452,7 +452,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2023-02-14 04:54:25.819620",
+ "modified": "2024-04-24 10:56:16.001032",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -487,6 +487,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 a52a9dbeac2..f6ba4ae9fab 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -823,11 +823,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/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/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 9ff22d384e7..2ad3f485d08 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -772,7 +772,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 0beb98563cf..a6fd929ea34 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -126,8 +126,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()
@@ -141,6 +140,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/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..0125a96fe70
--- /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",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "letter_head": "",
+ "modified": "2024-04-23 17:09:54.595566",
+ "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..cb651dd7972
--- /dev/null
+++ b/erpnext/stock/report/available_batch_report/available_batch_report.py
@@ -0,0 +1,144 @@
+# 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)
+
+ 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_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/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
index 1694abe7c08..401ebe43028 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
@@ -40,16 +40,26 @@ frappe.query_reports["Batch-Wise Balance History"] = {
};
},
},
+ {
+ fieldname: "warehouse_type",
+ label: __("Warehouse Type"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Warehouse Type",
+ },
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
get_query: function () {
- let company = frappe.query_report.get_filter_value("company");
+ let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
+ const company = frappe.query_report.get_filter_value("company");
+
return {
filters: {
- company: company,
+ ...(warehouse_type && { warehouse_type }),
+ ...(company && { company }),
},
};
},
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index 50b5efd6a08..c8c26fd66cb 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -113,6 +113,16 @@ def get_stock_ledger_entries(filters):
)
query = apply_warehouse_filter(query, sle, filters)
+ if filters.warehouse_type and not filters.warehouse:
+ warehouses = frappe.get_all(
+ "Warehouse",
+ filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
+ pluck="name",
+ )
+
+ if warehouses:
+ query = query.where(sle.warehouse.isin(warehouses))
+
for field in ["item_code", "batch_no", "company"]:
if filters.get(field):
query = query.where(sle[field] == filters.get(field))
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.js b/erpnext/stock/report/stock_ageing/stock_ageing.js
index 641084149ab..726b507663d 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.js
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.js
@@ -18,15 +18,25 @@ frappe.query_reports["Stock Ageing"] = {
default: frappe.datetime.get_today(),
reqd: 1,
},
+ {
+ fieldname: "warehouse_type",
+ label: __("Warehouse Type"),
+ fieldtype: "Link",
+ width: "80",
+ options: "Warehouse Type",
+ },
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
get_query: () => {
+ let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
const company = frappe.query_report.get_filter_value("company");
+
return {
filters: {
+ ...(warehouse_type && { warehouse_type }),
...(company && { company }),
},
};
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index a1aee987cb6..26bf99e1ed7 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -227,25 +227,30 @@ class FIFOSlots:
consumed/updated and maintained via FIFO. **
}
"""
- if self.sle is None:
- self.sle = self.__get_stock_ledger_entries()
+ stock_ledger_entries = self.sle
- for d in self.sle:
- key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
+ with frappe.db.unbuffered_cursor():
+ if self.sle is None:
+ self.sle = self.__get_stock_ledger_entries()
- if d.voucher_type == "Stock Reconciliation":
- # get difference in qty shift as actual qty
- prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
- d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
+ for d in self.sle:
+ key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
- serial_nos = get_serial_nos(d.serial_no) if d.serial_no else []
+ if d.voucher_type == "Stock Reconciliation":
+ # get difference in qty shift as actual qty
+ prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
+ d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
- if d.actual_qty > 0:
- self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
- else:
- self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
+ serial_nos = get_serial_nos(d.serial_no) if d.serial_no else []
- self.__update_balances(d, key)
+ if d.actual_qty > 0:
+ self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
+ else:
+ self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
+
+ self.__update_balances(d, key)
+
+ del stock_ledger_entries
if not self.filters.get("show_warehouse_wise_stock"):
# (Item 1, WH 1), (Item 1, WH 2) => (Item 1)
@@ -412,10 +417,19 @@ class FIFOSlots:
if self.filters.get("warehouse"):
sle_query = self.__get_warehouse_conditions(sle, sle_query)
+ elif self.filters.get("warehouse_type"):
+ warehouses = frappe.get_all(
+ "Warehouse",
+ filters={"warehouse_type": self.filters.get("warehouse_type"), "is_group": 0},
+ pluck="name",
+ )
+
+ if warehouses:
+ sle_query = sle_query.where(sle.warehouse.isin(warehouses))
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)
- return sle_query.run(as_dict=True)
+ return sle_query.run(as_dict=True, as_iterator=True)
def __get_item_query(self) -> str:
item_table = frappe.qb.DocType("Item")
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 127e6eec882..c0235f9b85c 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -329,7 +329,9 @@ class SubcontractingReceipt(SubcontractingController):
)
accepted_warehouse_account = warehouse_account[item.warehouse]["account"]
- supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
+ supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
+ "account"
+ )
remarks = self.get("remarks") or _("Accounting Entry for Stock")
# Accepted Warehouse Account (Debit)
@@ -401,7 +403,9 @@ class SubcontractingReceipt(SubcontractingController):
)
if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")):
- loss_account = self.get_company_default("stock_adjustment_account", ignore_validation=True)
+ loss_account = self.get_company_default(
+ "stock_adjustment_account", ignore_validation=True
+ )
# Loss Account (Credit)
self.add_gl_entry(
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)),