Merge pull request #40917 from frappe/l10n_develop
fix: sync translations from crowdin
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index c494eec..d74224c 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -182,6 +182,7 @@
and self.company == dimension.company
and dimension.mandatory_for_pl
and not dimension.disabled
+ and not self.is_cancelled
):
if not self.get(dimension.fieldname):
frappe.throw(
@@ -195,6 +196,7 @@
and self.company == dimension.company
and dimension.mandatory_for_bs
and not dimension.disabled
+ and not self.is_cancelled
):
if not self.get(dimension.fieldname):
frappe.throw(
diff --git a/erpnext/accounts/doctype/ledger_health/__init__.py b/erpnext/accounts/doctype/ledger_health/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health/__init__.py
diff --git a/erpnext/accounts/doctype/ledger_health/ledger_health.js b/erpnext/accounts/doctype/ledger_health/ledger_health.js
new file mode 100644
index 0000000..e207dae
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health/ledger_health.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Ledger Health", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/erpnext/accounts/doctype/ledger_health/ledger_health.json b/erpnext/accounts/doctype/ledger_health/ledger_health.json
new file mode 100644
index 0000000..fb2da3d
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health/ledger_health.json
@@ -0,0 +1,70 @@
+{
+ "actions": [],
+ "autoname": "autoincrement",
+ "creation": "2024-03-26 17:01:47.443986",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "voucher_type",
+ "voucher_no",
+ "checked_on",
+ "debit_credit_mismatch",
+ "general_and_payment_ledger_mismatch"
+ ],
+ "fields": [
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Data",
+ "label": "Voucher Type"
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Data",
+ "label": "Voucher No"
+ },
+ {
+ "default": "0",
+ "fieldname": "debit_credit_mismatch",
+ "fieldtype": "Check",
+ "label": "Debit-Credit mismatch"
+ },
+ {
+ "fieldname": "checked_on",
+ "fieldtype": "Datetime",
+ "label": "Checked On"
+ },
+ {
+ "default": "0",
+ "fieldname": "general_and_payment_ledger_mismatch",
+ "fieldtype": "Check",
+ "label": "General and Payment Ledger mismatch"
+ }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2024-04-09 11:16:07.044484",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Ledger Health",
+ "naming_rule": "Autoincrement",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "read_only": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/ledger_health/ledger_health.py b/erpnext/accounts/doctype/ledger_health/ledger_health.py
new file mode 100644
index 0000000..590ff80
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health/ledger_health.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class LedgerHealth(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ checked_on: DF.Datetime | None
+ debit_credit_mismatch: DF.Check
+ general_and_payment_ledger_mismatch: DF.Check
+ name: DF.Int | None
+ voucher_no: DF.Data | None
+ voucher_type: DF.Data | None
+ # end: auto-generated types
+
+ pass
diff --git a/erpnext/accounts/doctype/ledger_health/test_ledger_health.py b/erpnext/accounts/doctype/ledger_health/test_ledger_health.py
new file mode 100644
index 0000000..d35b39d
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health/test_ledger_health.py
@@ -0,0 +1,109 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import nowdate
+
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+from erpnext.accounts.utils import run_ledger_health_checks
+
+
+class TestLedgerHealth(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.configure_monitoring_tool()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def configure_monitoring_tool(self):
+ monitor_settings = frappe.get_doc("Ledger Health Monitor")
+ monitor_settings.enable_health_monitor = True
+ monitor_settings.enable_for_last_x_days = 60
+ monitor_settings.debit_credit_mismatch = True
+ monitor_settings.general_and_payment_ledger_mismatch = True
+ exists = [x for x in monitor_settings.companies if x.company == self.company]
+ if not exists:
+ monitor_settings.append("companies", {"company": self.company})
+ monitor_settings.save()
+
+ def clear_old_entries(self):
+ super().clear_old_entries()
+ lh = qb.DocType("Ledger Health")
+ qb.from_(lh).delete().run()
+
+ def create_journal(self):
+ je = frappe.new_doc("Journal Entry")
+ je.company = self.company
+ je.voucher_type = "Journal Entry"
+ je.posting_date = nowdate()
+ je.append(
+ "accounts",
+ {
+ "account": self.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "debit_in_account_currency": 10000,
+ },
+ )
+ je.append("accounts", {"account": self.income_account, "credit_in_account_currency": 10000})
+ je.save().submit()
+ self.je = je
+
+ def test_debit_credit_mismatch(self):
+ self.create_journal()
+
+ # manually cause debit-credit mismatch
+ gle = frappe.db.get_all(
+ "GL Entry", filters={"voucher_no": self.je.name, "account": self.income_account}
+ )[0]
+ frappe.db.set_value("GL Entry", gle.name, "credit", 8000)
+
+ run_ledger_health_checks()
+ expected = {
+ "voucher_type": self.je.doctype,
+ "voucher_no": self.je.name,
+ "debit_credit_mismatch": True,
+ "general_and_payment_ledger_mismatch": False,
+ }
+ actual = frappe.db.get_all(
+ "Ledger Health",
+ fields=[
+ "voucher_type",
+ "voucher_no",
+ "debit_credit_mismatch",
+ "general_and_payment_ledger_mismatch",
+ ],
+ )
+ self.assertEqual(len(actual), 1)
+ self.assertEqual(expected, actual[0])
+
+ def test_gl_and_pl_mismatch(self):
+ self.create_journal()
+
+ # manually cause GL and PL discrepancy
+ ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": self.je.name})[0]
+ frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", 11000)
+
+ run_ledger_health_checks()
+ expected = {
+ "voucher_type": self.je.doctype,
+ "voucher_no": self.je.name,
+ "debit_credit_mismatch": False,
+ "general_and_payment_ledger_mismatch": True,
+ }
+ actual = frappe.db.get_all(
+ "Ledger Health",
+ fields=[
+ "voucher_type",
+ "voucher_no",
+ "debit_credit_mismatch",
+ "general_and_payment_ledger_mismatch",
+ ],
+ )
+ self.assertEqual(len(actual), 1)
+ self.assertEqual(expected, actual[0])
diff --git a/erpnext/accounts/doctype/ledger_health_monitor/__init__.py b/erpnext/accounts/doctype/ledger_health_monitor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health_monitor/__init__.py
diff --git a/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.js b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.js
new file mode 100644
index 0000000..cf11276
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Ledger Health Monitor", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.json b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.json
new file mode 100644
index 0000000..6e68833
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.json
@@ -0,0 +1,104 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2024-03-27 09:38:07.427997",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "enable_health_monitor",
+ "monitor_section",
+ "monitor_for_last_x_days",
+ "debit_credit_mismatch",
+ "general_and_payment_ledger_mismatch",
+ "section_break_xdsp",
+ "companies"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "enable_health_monitor",
+ "fieldtype": "Check",
+ "label": "Enable Health Monitor"
+ },
+ {
+ "fieldname": "monitor_section",
+ "fieldtype": "Section Break",
+ "label": "Configuration"
+ },
+ {
+ "default": "0",
+ "fieldname": "debit_credit_mismatch",
+ "fieldtype": "Check",
+ "label": "Debit-Credit Mismatch"
+ },
+ {
+ "default": "0",
+ "fieldname": "general_and_payment_ledger_mismatch",
+ "fieldtype": "Check",
+ "label": "Discrepancy between General and Payment Ledger"
+ },
+ {
+ "default": "60",
+ "fieldname": "monitor_for_last_x_days",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Monitor for Last 'X' days",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_xdsp",
+ "fieldtype": "Section Break",
+ "label": "Companies"
+ },
+ {
+ "fieldname": "companies",
+ "fieldtype": "Table",
+ "options": "Ledger Health Monitor Company"
+ }
+ ],
+ "hide_toolbar": 1,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2024-03-27 10:14:16.511681",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Ledger Health Monitor",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.py b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.py
new file mode 100644
index 0000000..9f7c569
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health_monitor/ledger_health_monitor.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class LedgerHealthMonitor(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ from erpnext.accounts.doctype.ledger_health_monitor_company.ledger_health_monitor_company import (
+ LedgerHealthMonitorCompany,
+ )
+
+ companies: DF.Table[LedgerHealthMonitorCompany]
+ debit_credit_mismatch: DF.Check
+ enable_health_monitor: DF.Check
+ general_and_payment_ledger_mismatch: DF.Check
+ monitor_for_last_x_days: DF.Int
+ # end: auto-generated types
+
+ pass
diff --git a/erpnext/accounts/doctype/ledger_health_monitor/test_ledger_health_monitor.py b/erpnext/accounts/doctype/ledger_health_monitor/test_ledger_health_monitor.py
new file mode 100644
index 0000000..e0ba443
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health_monitor/test_ledger_health_monitor.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestLedgerHealthMonitor(FrappeTestCase):
+ pass
diff --git a/erpnext/accounts/doctype/ledger_health_monitor_company/__init__.py b/erpnext/accounts/doctype/ledger_health_monitor_company/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health_monitor_company/__init__.py
diff --git a/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.json b/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.json
new file mode 100644
index 0000000..87fa3e3
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.json
@@ -0,0 +1,32 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2024-03-27 10:04:45.727054",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2024-03-27 10:06:22.806155",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Ledger Health Monitor Company",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.py b/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.py
new file mode 100644
index 0000000..5890410
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class LedgerHealthMonitorCompany(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ company: DF.Link | None
+ parent: DF.Data
+ parentfield: DF.Data
+ parenttype: DF.Data
+ # end: auto-generated types
+
+ pass
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
index 87f666b..c1d6935 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
@@ -16,6 +16,7 @@
"col_break1",
"account_head",
"description",
+ "is_tax_withholding_account",
"section_break_10",
"rate",
"accounting_dimensions_section",
@@ -225,15 +226,23 @@
"label": "Account Currency",
"options": "Currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_tax_withholding_account",
+ "fieldtype": "Check",
+ "label": "Is Tax Withholding Account",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:10:26.775139",
+ "modified": "2024-04-08 19:51:36.678551",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py
index d6c0292..585d5e6 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py
@@ -33,6 +33,7 @@
description: DF.SmallText
included_in_paid_amount: DF.Check
included_in_print_rate: DF.Check
+ is_tax_withholding_account: DF.Check
item_wise_tax_detail: DF.Code | None
parent: DF.Data
parentfield: DF.Data
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index b043f9a..74e54dc 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -146,7 +146,12 @@
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
cost_center = get_cost_center(inv)
- tax_row.update({"cost_center": cost_center})
+ tax_row.update(
+ {
+ "cost_center": cost_center,
+ "is_tax_withholding_account": 1,
+ }
+ )
if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances, voucher_wise_amount
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html
index 2d5ca49..3c4e1a0 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.html
+++ b/erpnext/accounts/report/general_ledger/general_ledger.html
@@ -55,10 +55,10 @@
</span>
</td>
<td style="text-align: right">
- {%= format_currency(data[i].debit, filters.presentation_currency) %}
+ {%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
</td>
<td style="text-align: right">
- {%= format_currency(data[i].credit, filters.presentation_currency) %}
+ {%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
</td>
{% } else { %}
<td></td>
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 888e040..ade17be 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -347,7 +347,7 @@
# acc
if acc_dict.entries:
# opening
- data.append({})
+ data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
if filters.get("group_by") != "Group by Voucher":
data.append(acc_dict.totals.opening)
@@ -359,7 +359,8 @@
# closing
if filters.get("group_by") != "Group by Voucher":
data.append(acc_dict.totals.closing)
- data.append({})
+
+ data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
else:
data += entries
@@ -380,6 +381,8 @@
credit=0.0,
debit_in_account_currency=0.0,
credit_in_account_currency=0.0,
+ debit_in_transaction_currency=None,
+ credit_in_transaction_currency=None,
)
return _dict(
@@ -424,6 +427,10 @@
data[key].debit_in_account_currency += gle.debit_in_account_currency
data[key].credit_in_account_currency += gle.credit_in_account_currency
+ if filters.get("add_values_in_transaction_currency") and key not in ["opening", "closing", "total"]:
+ data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
+ data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
+
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
"Receivable",
"Payable",
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 869fd42..65ba9af 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -13,11 +13,13 @@
from frappe.query_builder.functions import Round, Sum
from frappe.query_builder.utils import DocType
from frappe.utils import (
+ add_days,
cint,
create_batch,
cstr,
flt,
formatdate,
+ get_datetime,
get_number_format_info,
getdate,
now,
@@ -1721,6 +1723,7 @@
)
ref_doc.set_status(update=True)
+ ref_doc.notify_update()
def delink_original_entry(pl_entry, partial_cancel=False):
@@ -2071,3 +2074,44 @@
def get_party_types_from_account_type(account_type):
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
+
+
+def run_ledger_health_checks():
+ health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
+ if health_monitor_settings.enable_health_monitor:
+ period_end = getdate()
+ period_start = add_days(period_end, -abs(health_monitor_settings.monitor_for_last_x_days))
+
+ run_date = get_datetime()
+
+ # Debit-Credit mismatch report
+ if health_monitor_settings.debit_credit_mismatch:
+ for x in health_monitor_settings.companies:
+ filters = {"company": x.company, "from_date": period_start, "to_date": period_end}
+ voucher_wise = frappe.get_doc("Report", "Voucher-wise Balance")
+ res = voucher_wise.execute_script_report(filters=filters)
+ for x in res[1]:
+ doc = frappe.new_doc("Ledger Health")
+ doc.voucher_type = x.voucher_type
+ doc.voucher_no = x.voucher_no
+ doc.debit_credit_mismatch = True
+ doc.checked_on = run_date
+ doc.save()
+
+ # General Ledger and Payment Ledger discrepancy
+ if health_monitor_settings.general_and_payment_ledger_mismatch:
+ for x in health_monitor_settings.companies:
+ filters = {
+ "company": x.company,
+ "period_start_date": period_start,
+ "period_end_date": period_end,
+ }
+ gl_pl_comparison = frappe.get_doc("Report", "General and Payment Ledger Comparison")
+ res = gl_pl_comparison.execute_script_report(filters=filters)
+ for x in res[1]:
+ doc = frappe.new_doc("Ledger Health")
+ doc.voucher_type = x.voucher_type
+ doc.voucher_no = x.voucher_no
+ doc.general_and_payment_ledger_mismatch = True
+ doc.checked_on = run_date
+ doc.save()
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 3ecef85..66097ce 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1032,10 +1032,10 @@
"transaction_currency": self.get("currency") or self.company_currency,
"transaction_exchange_rate": self.get("conversion_rate", 1),
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
- account_currency, args, "debit"
+ account_currency, gl_dict, "debit"
),
"credit_in_transaction_currency": self.get_value_in_transaction_currency(
- account_currency, args, "credit"
+ account_currency, gl_dict, "credit"
),
}
)
@@ -1067,11 +1067,11 @@
return "Debit Note"
return self.doctype
- def get_value_in_transaction_currency(self, account_currency, args, field):
+ def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
if account_currency == self.get("currency"):
- return args.get(field + "_in_account_currency")
+ return gl_dict.get(field + "_in_account_currency")
else:
- return flt(args.get(field, 0) / self.get("conversion_rate", 1))
+ return flt(gl_dict.get(field, 0) / self.get("conversion_rate", 1))
def validate_zero_qty_for_return_invoices_with_stock(self):
rows = []
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index d90c14a..21b9186 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -467,7 +467,16 @@
if tax.charge_type == "Actual":
# distribute the tax amount proportionally to each item row
actual = flt(tax.tax_amount, tax.precision("tax_amount"))
- current_tax_amount = item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
+
+ if tax.get("is_tax_withholding_account") and item.meta.get_field("apply_tds"):
+ if not item.get("apply_tds") or not self.doc.tax_withholding_net_total:
+ current_tax_amount = 0.0
+ else:
+ current_tax_amount = item.net_amount * actual / self.doc.tax_withholding_net_total
+ else:
+ current_tax_amount = (
+ item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
+ )
elif tax.charge_type == "On Net Total":
current_tax_amount = (tax_rate / 100.0) * item.net_amount
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 9c1521a..a31f011 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -434,6 +434,7 @@
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
+ "erpnext.accounts.utils.run_ledger_health_checks",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 4127218..8478e20 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -452,6 +452,9 @@
},
get_fiscal_year: function (date, with_dates = false, boolean = false) {
+ if (!frappe.boot.setup_complete) {
+ return;
+ }
if (!date) {
date = frappe.datetime.get_today();
}
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 9aaa08e..e3dbdb5 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -12,7 +12,7 @@
from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
-from frappe.utils import ceil, cint, floor, flt
+from frappe.utils import ceil, cint, floor, flt, get_link_to_form
from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import (
@@ -23,7 +23,11 @@
get_picked_serial_nos,
)
from erpnext.stock.get_item_details import get_conversion_factor
-from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+from erpnext.stock.serial_batch_bundle import (
+ SerialBatchCreation,
+ get_batches_from_bundle,
+ get_serial_nos_from_bundle,
+)
# TODO: Prioritize SO or WO group warehouse
@@ -201,10 +205,11 @@
row.db_set("serial_and_batch_bundle", None)
def on_update(self):
- self.linked_serial_and_batch_bundle()
+ if self.get("locations"):
+ self.linked_serial_and_batch_bundle()
def linked_serial_and_batch_bundle(self):
- for row in self.locations:
+ for row in self.get("locations"):
if row.serial_and_batch_bundle:
frappe.get_doc(
"Serial and Batch Bundle", row.serial_and_batch_bundle
@@ -513,55 +518,82 @@
def get_picked_items_details(self, items):
picked_items = frappe._dict()
- if items:
- pi = frappe.qb.DocType("Pick List")
- pi_item = frappe.qb.DocType("Pick List Item")
- query = (
- frappe.qb.from_(pi)
- .inner_join(pi_item)
- .on(pi.name == pi_item.parent)
- .select(
- pi_item.item_code,
- pi_item.warehouse,
- pi_item.batch_no,
- pi_item.serial_and_batch_bundle,
- Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
- "picked_qty"
- ),
- Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
- )
- .where(
- (pi_item.item_code.isin([x.item_code for x in items]))
- & ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
- & (pi.status != "Completed")
- & (pi.status != "Cancelled")
- & (pi_item.docstatus != 2)
- )
- .groupby(
- pi_item.item_code,
- pi_item.warehouse,
- pi_item.batch_no,
- )
- )
+ if not items:
+ return picked_items
- if self.name:
- query = query.where(pi_item.parent != self.name)
+ items_data = self._get_pick_list_items(items)
- items_data = query.run(as_dict=True)
+ for item_data in items_data:
+ key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse
+ serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None
- for item_data in items_data:
- key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse
- serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None
- data = {"picked_qty": item_data.picked_qty}
- if serial_no:
- data["serial_no"] = serial_no
- if item_data.item_code not in picked_items:
- picked_items[item_data.item_code] = {key: data}
- else:
- picked_items[item_data.item_code][key] = data
+ if item_data.serial_and_batch_bundle:
+ if not serial_no:
+ serial_no = get_serial_nos_from_bundle(item_data.serial_and_batch_bundle)
+
+ if not item_data.batch_no and not serial_no:
+ bundle_batches = get_batches_from_bundle(item_data.serial_and_batch_bundle)
+ for batch_no, batch_qty in bundle_batches.items():
+ batch_qty = abs(batch_qty)
+
+ key = (item_data.warehouse, batch_no)
+ if item_data.item_code not in picked_items:
+ picked_items[item_data.item_code] = {key: {"picked_qty": batch_qty}}
+ else:
+ picked_items[item_data.item_code][key]["picked_qty"] += batch_qty
+
+ continue
+
+ if item_data.item_code not in picked_items:
+ picked_items[item_data.item_code] = {}
+
+ if key not in picked_items[item_data.item_code]:
+ picked_items[item_data.item_code][key] = frappe._dict(
+ {
+ "picked_qty": 0,
+ "serial_no": [],
+ "batch_no": item_data.batch_no or "",
+ "warehouse": item_data.warehouse,
+ }
+ )
+
+ picked_items[item_data.item_code][key]["picked_qty"] += item_data.picked_qty
+ if serial_no:
+ picked_items[item_data.item_code][key]["serial_no"].extend(serial_no)
return picked_items
+ def _get_pick_list_items(self, items):
+ pi = frappe.qb.DocType("Pick List")
+ pi_item = frappe.qb.DocType("Pick List Item")
+ query = (
+ frappe.qb.from_(pi)
+ .inner_join(pi_item)
+ .on(pi.name == pi_item.parent)
+ .select(
+ pi_item.item_code,
+ pi_item.warehouse,
+ pi_item.batch_no,
+ pi_item.serial_and_batch_bundle,
+ pi_item.serial_no,
+ (Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
+ "picked_qty"
+ ),
+ )
+ .where(
+ (pi_item.item_code.isin([x.item_code for x in items]))
+ & ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
+ & (pi.status != "Completed")
+ & (pi.status != "Cancelled")
+ & (pi_item.docstatus != 2)
+ )
+ )
+
+ if self.name:
+ query = query.where(pi_item.parent != self.name)
+
+ return query.run(as_dict=True)
+
def _get_product_bundles(self) -> dict[str, str]:
# Dict[so_item_row: item_code]
product_bundles = {}
@@ -718,9 +750,7 @@
consider_rejected_warehouses=False,
):
locations = []
- total_picked_qty = (
- sum([v.get("picked_qty") for k, v in picked_item_details.items()]) if picked_item_details else 0
- )
+
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
@@ -730,63 +760,90 @@
from_warehouses,
required_qty,
company,
- total_picked_qty,
consider_rejected_warehouses=consider_rejected_warehouses,
)
elif has_serial_no:
locations = get_available_item_locations_for_serialized_item(
item_code,
from_warehouses,
- required_qty,
company,
- total_picked_qty,
consider_rejected_warehouses=consider_rejected_warehouses,
)
elif has_batch_no:
locations = get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
- required_qty,
- company,
- total_picked_qty,
consider_rejected_warehouses=consider_rejected_warehouses,
)
else:
locations = get_available_item_locations_for_other_item(
item_code,
from_warehouses,
- required_qty,
company,
- total_picked_qty,
consider_rejected_warehouses=consider_rejected_warehouses,
)
+ if picked_item_details:
+ locations = filter_locations_by_picked_materials(locations, picked_item_details)
+
+ if locations:
+ locations = get_locations_based_on_required_qty(locations, required_qty)
+
+ if not ignore_validation:
+ validate_picked_materials(item_code, required_qty, locations)
+
+ return locations
+
+
+def get_locations_based_on_required_qty(locations, required_qty):
+ filtered_locations = []
+
+ for location in locations:
+ if location.qty >= required_qty:
+ location.qty = required_qty
+ filtered_locations.append(location)
+ break
+
+ required_qty -= location.qty
+ filtered_locations.append(location)
+
+ return filtered_locations
+
+
+def validate_picked_materials(item_code, required_qty, locations):
+ for location in list(locations):
+ if location["qty"] < 0:
+ locations.remove(location)
+
total_qty_available = sum(location.get("qty") for location in locations)
remaining_qty = required_qty - total_qty_available
- if remaining_qty > 0 and not ignore_validation:
+ if remaining_qty > 0:
frappe.msgprint(
- _("{0} units of Item {1} is not available.").format(
- remaining_qty, frappe.get_desk_link("Item", item_code)
+ _("{0} units of Item {1} is picked in another Pick List.").format(
+ remaining_qty, get_link_to_form("Item", item_code)
),
- title=_("Insufficient Stock"),
+ title=_("Already Picked"),
)
- if picked_item_details:
- for location in list(locations):
- if location["qty"] < 0:
- locations.remove(location)
- total_qty_available = sum(location.get("qty") for location in locations)
- remaining_qty = required_qty - total_qty_available
+def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
+ for row in locations:
+ key = row.warehouse
+ if row.batch_no:
+ key = (row.warehouse, row.batch_no)
- if remaining_qty > 0 and not ignore_validation:
- frappe.msgprint(
- _("{0} units of Item {1} is picked in another Pick List.").format(
- remaining_qty, frappe.get_desk_link("Item", item_code)
- ),
- title=_("Already Picked"),
- )
+ picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
+ if not picked_qty:
+ continue
+ if picked_qty > row.qty:
+ row.qty = 0
+ picked_item_details[key]["picked_qty"] -= row.qty
+ else:
+ row.qty -= picked_qty
+ picked_item_details[key]["picked_qty"] = 0.0
+ if row.serial_nos:
+ row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no")))
return locations
@@ -796,15 +853,12 @@
from_warehouses,
required_qty,
company,
- total_picked_qty=0,
consider_rejected_warehouses=False,
):
# Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
- required_qty,
- company,
consider_rejected_warehouses=consider_rejected_warehouses,
)
@@ -824,7 +878,6 @@
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
)
.orderby(sn.creation)
- .limit(ceil(location.qty + total_picked_qty))
).run(as_dict=True)
serial_nos = [sn.name for sn in serial_nos]
@@ -837,18 +890,14 @@
def get_available_item_locations_for_serialized_item(
item_code,
from_warehouses,
- required_qty,
company,
- total_picked_qty=0,
consider_rejected_warehouses=False,
):
- picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
-
sn = frappe.qb.DocType("Serial No")
query = (
frappe.qb.from_(sn)
.select(sn.name, sn.warehouse)
- .where((sn.item_code == item_code) & (sn.company == company))
+ .where(sn.item_code == item_code)
.orderby(sn.creation)
)
@@ -856,6 +905,7 @@
query = query.where(sn.warehouse.isin(from_warehouses))
else:
query = query.where(Coalesce(sn.warehouse, "") != "")
+ query = query.where(sn.company == company)
if not consider_rejected_warehouses:
if rejected_warehouses := get_rejected_warehouses():
@@ -864,16 +914,8 @@
serial_nos = query.run(as_list=True)
warehouse_serial_nos_map = frappe._dict()
- picked_qty = required_qty
for serial_no, warehouse in serial_nos:
- if serial_no in picked_serial_nos:
- continue
-
- if picked_qty <= 0:
- break
-
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
- picked_qty -= 1
locations = []
@@ -881,12 +923,14 @@
qty = len(serial_nos)
locations.append(
- {
- "qty": qty,
- "warehouse": warehouse,
- "item_code": item_code,
- "serial_nos": serial_nos,
- }
+ frappe._dict(
+ {
+ "qty": qty,
+ "warehouse": warehouse,
+ "item_code": item_code,
+ "serial_nos": serial_nos,
+ }
+ )
)
return locations
@@ -895,9 +939,6 @@
def get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
- required_qty,
- company,
- total_picked_qty=0,
consider_rejected_warehouses=False,
):
locations = []
@@ -906,8 +947,6 @@
{
"item_code": item_code,
"warehouse": from_warehouses,
- "qty": required_qty,
- "is_pick_list": True,
}
)
)
@@ -943,9 +982,7 @@
def get_available_item_locations_for_other_item(
item_code,
from_warehouses,
- required_qty,
company,
- total_picked_qty=0,
consider_rejected_warehouses=False,
):
bin = frappe.qb.DocType("Bin")
@@ -954,7 +991,6 @@
.select(bin.warehouse, bin.actual_qty.as_("qty"))
.where((bin.item_code == item_code) & (bin.actual_qty > 0))
.orderby(bin.creation)
- .limit(cint(required_qty + total_picked_qty))
)
if from_warehouses:
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 0fe869b..87a7150 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -815,7 +815,7 @@
def test_pick_list_status(self):
warehouse = "_Test Warehouse - _TC"
- item = make_item(properties={"maintain_stock": 1}).name
+ item = make_item(properties={"is_stock_item": 1}).name
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
so = make_sales_order(item_code=item, qty=10, rate=100)
@@ -845,3 +845,135 @@
pl.cancel()
pl.reload()
self.assertEqual(pl.status, "Cancelled")
+
+ def test_pick_list_validation(self):
+ warehouse = "_Test Warehouse - _TC"
+ item = make_item("Test Non Serialized Pick List Item", properties={"is_stock_item": 1}).name
+
+ make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
+
+ so = make_sales_order(item_code=item, qty=5, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.save()
+ pl.submit()
+ self.assertEqual(pl.locations[0].qty, 5.0)
+ self.assertTrue(hasattr(pl, "locations"))
+
+ so = make_sales_order(item_code=item, qty=5, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.save()
+ self.assertEqual(pl.locations[0].qty, 5.0)
+ self.assertTrue(hasattr(pl, "locations"))
+
+ so = make_sales_order(item_code=item, qty=4, rate=100)
+ pl = create_pick_list(so.name)
+ self.assertFalse(hasattr(pl, "locations"))
+
+ def test_pick_list_validation_for_serial_no(self):
+ warehouse = "_Test Warehouse - _TC"
+ item = make_item(
+ "Test Serialized Pick List Item",
+ properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-SPLI-.####"},
+ ).name
+
+ make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
+
+ so = make_sales_order(item_code=item, qty=5, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.locations[0].qty = 5
+ pl.save()
+ pl.submit()
+ self.assertTrue(pl.locations[0].serial_no)
+ self.assertEqual(pl.locations[0].qty, 5.0)
+ self.assertTrue(hasattr(pl, "locations"))
+
+ so = make_sales_order(item_code=item, qty=5, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.save()
+ self.assertTrue(pl.locations[0].serial_no)
+ self.assertEqual(pl.locations[0].qty, 5.0)
+ self.assertTrue(hasattr(pl, "locations"))
+
+ so = make_sales_order(item_code=item, qty=4, rate=100)
+ pl = create_pick_list(so.name)
+ self.assertFalse(hasattr(pl, "locations"))
+
+ def test_pick_list_validation_for_batch_no(self):
+ warehouse = "_Test Warehouse - _TC"
+ item = make_item(
+ "Test Batch Pick List Item",
+ properties={
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "batch_number_series": "BATCH-SPLI-.####",
+ "create_new_batch": 1,
+ },
+ ).name
+
+ make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
+
+ so = make_sales_order(item_code=item, qty=5, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.locations[0].qty = 5
+ pl.save()
+ pl.submit()
+ self.assertTrue(pl.locations[0].batch_no)
+ self.assertEqual(pl.locations[0].qty, 5.0)
+ self.assertTrue(hasattr(pl, "locations"))
+
+ so = make_sales_order(item_code=item, qty=5, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.save()
+ self.assertTrue(pl.locations[0].batch_no)
+ self.assertEqual(pl.locations[0].qty, 5.0)
+ self.assertTrue(hasattr(pl, "locations"))
+
+ so = make_sales_order(item_code=item, qty=4, rate=100)
+ pl = create_pick_list(so.name)
+ self.assertFalse(hasattr(pl, "locations"))
+
+ def test_pick_list_validation_for_batch_no_and_serial_item(self):
+ warehouse = "_Test Warehouse - _TC"
+ item = make_item(
+ "Test Serialized Batch Pick List Item",
+ properties={
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "batch_number_series": "SN-BT-BATCH-SPLI-.####",
+ "create_new_batch": 1,
+ "has_serial_no": 1,
+ "serial_no_series": "SN-BT-SPLI-.####",
+ },
+ ).name
+
+ make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
+
+ so = make_sales_order(item_code=item, qty=5, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.locations[0].qty = 5
+ pl.save()
+ pl.submit()
+ self.assertTrue(pl.locations[0].batch_no)
+ self.assertTrue(pl.locations[0].serial_no)
+ self.assertEqual(pl.locations[0].qty, 5.0)
+ self.assertTrue(hasattr(pl, "locations"))
+
+ so = make_sales_order(item_code=item, qty=5, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.save()
+ self.assertTrue(pl.locations[0].batch_no)
+ self.assertTrue(pl.locations[0].serial_no)
+ self.assertEqual(pl.locations[0].qty, 5.0)
+ self.assertTrue(hasattr(pl, "locations"))
+
+ so = make_sales_order(item_code=item, qty=4, rate=100)
+ pl = create_pick_list(so.name)
+ self.assertFalse(hasattr(pl, "locations"))
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 339d508..0c460b4 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -58,6 +58,8 @@
"column_break_27",
"total",
"net_total",
+ "tax_withholding_net_total",
+ "base_tax_withholding_net_total",
"taxes_charges_section",
"tax_category",
"taxes_and_charges",
@@ -1246,13 +1248,31 @@
"label": "Subcontracting Receipt",
"options": "Subcontracting Receipt",
"search_index": 1
+ },
+ {
+ "fieldname": "tax_withholding_net_total",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Tax Withholding Net Total",
+ "no_copy": 1,
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_tax_withholding_net_total",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Base Tax Withholding Net Total",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2024-03-27 13:10:25.441066",
+ "modified": "2024-04-08 20:23:03.699201",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index ae101df..5ae0841 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -52,6 +52,7 @@
base_net_total: DF.Currency
base_rounded_total: DF.Currency
base_rounding_adjustment: DF.Currency
+ base_tax_withholding_net_total: DF.Currency
base_taxes_and_charges_added: DF.Currency
base_taxes_and_charges_deducted: DF.Currency
base_total: DF.Currency
@@ -121,6 +122,7 @@
supplier_name: DF.Data | None
supplier_warehouse: DF.Link | None
tax_category: DF.Link | None
+ tax_withholding_net_total: DF.Currency
taxes: DF.Table[PurchaseTaxesandCharges]
taxes_and_charges: DF.Link | None
taxes_and_charges_added: DF.Currency
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 7cd3799..6ba1469 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -58,6 +58,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "apply_tds",
"section_break_29",
"net_rate",
"net_amount",
@@ -1107,12 +1108,20 @@
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
+ },
+ {
+ "default": "1",
+ "fieldname": "apply_tds",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Apply TDS",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:10:25.896543",
+ "modified": "2024-04-08 20:00:16.277292",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
index 3c6dcdc..908c0a7 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
@@ -16,6 +16,7 @@
allow_zero_valuation_rate: DF.Check
amount: DF.Currency
+ apply_tds: DF.Check
asset_category: DF.Link | None
asset_location: DF.Link | None
barcode: DF.Data | None