diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
index 5d94a08f2f..04dab4c28a 100644
--- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
+++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
@@ -112,7 +112,8 @@ class AutoMatchbyPartyNameDescription:
for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
- names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
+ field = party.lower() + "_name"
+ names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
for field in ["bank_party_name", "description"]:
if not self.get(field):
@@ -131,7 +132,11 @@ class AutoMatchbyPartyNameDescription:
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
skip = False
- result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
+ result = process.extract(
+ query=self.get(field),
+ choices={row.get("name"): row.get("party_name") for row in names},
+ scorer=fuzz.token_set_ratio,
+ )
party_name, skip = self.process_fuzzy_result(result)
if not party_name:
@@ -149,14 +154,14 @@ class AutoMatchbyPartyNameDescription:
Returns: Result, Skip (whether or not to discontinue matching)
"""
- PARTY, SCORE, CUTOFF = 0, 1, 80
+ SCORE, PARTY_ID, CUTOFF = 1, 2, 80
if not result or not len(result):
return None, False
first_result = result[0]
if len(result) == 1:
- return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
+ return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
second_result = result[1]
if first_result[SCORE] > CUTOFF:
@@ -165,7 +170,7 @@ class AutoMatchbyPartyNameDescription:
if first_result[SCORE] == second_result[SCORE]:
return None, True
- return first_result[PARTY], True
+ return first_result[PARTY_ID], True
else:
return None, False
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 0203c45058..b3ae627c6e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -154,7 +154,7 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
- if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
+ if((frm.doc.references) && (frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0}))) {
frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
}, __('Actions'));
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
index 9cf2ac6c2a..28c9529995 100644
--- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
@@ -30,7 +30,8 @@
{
"fieldname": "posting_date",
"fieldtype": "Date",
- "label": "Posting Date"
+ "label": "Posting Date",
+ "search_index": 1
},
{
"fieldname": "account_type",
@@ -64,7 +65,8 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Voucher Type",
- "options": "DocType"
+ "options": "DocType",
+ "search_index": 1
},
{
"fieldname": "voucher_no",
@@ -72,14 +74,16 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
- "options": "voucher_type"
+ "options": "voucher_type",
+ "search_index": 1
},
{
"fieldname": "against_voucher_type",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Against Voucher Type",
- "options": "DocType"
+ "options": "DocType",
+ "search_index": 1
},
{
"fieldname": "against_voucher_no",
@@ -87,7 +91,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Against Voucher No",
- "options": "against_voucher_type"
+ "options": "against_voucher_type",
+ "search_index": 1
},
{
"fieldname": "amount",
@@ -147,13 +152,14 @@
{
"fieldname": "voucher_detail_no",
"fieldtype": "Data",
- "label": "Voucher Detail No"
+ "label": "Voucher Detail No",
+ "search_index": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-06-29 12:24:20.500632",
+ "modified": "2023-11-03 16:39:58.904113",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Ledger Entry",
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 1626f25f3e..43167be15a 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -109,6 +109,8 @@ class PaymentReconciliation(Document):
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
)
+ limit = f"limit {self.payment_limit}" if self.payment_limit else " "
+
# nosemgrep
journal_entries = frappe.db.sql(
"""
@@ -132,11 +134,13 @@ class PaymentReconciliation(Document):
ELSE {bank_account_condition}
END)
order by t1.posting_date
+ {limit}
""".format(
**{
"dr_or_cr": dr_or_cr,
"bank_account_condition": bank_account_condition,
"condition": condition,
+ "limit": limit,
}
),
{
@@ -162,7 +166,7 @@ class PaymentReconciliation(Document):
if self.payment_name:
conditions.append(doc.name.like(f"%{self.payment_name}%"))
- self.return_invoices = (
+ self.return_invoices_query = (
qb.from_(doc)
.select(
ConstantColumn(voucher_type).as_("voucher_type"),
@@ -170,8 +174,11 @@ class PaymentReconciliation(Document):
doc.return_against,
)
.where(Criterion.all(conditions))
- .run(as_dict=True)
)
+ if self.payment_limit:
+ self.return_invoices_query = self.return_invoices_query.limit(self.payment_limit)
+
+ self.return_invoices = self.return_invoices_query.run(as_dict=True)
def get_dr_or_cr_notes(self):
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
index 1131a0fca6..b4ac9812cb 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
@@ -110,7 +110,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-04-21 17:36:26.642617",
+ "modified": "2023-11-02 11:32:12.254018",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log",
@@ -125,7 +125,19 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "System Manager",
+ "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
}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 2d1f4451b6..09bffff6da 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -384,7 +384,8 @@
"label": "Supplier Invoice No",
"oldfieldname": "bill_no",
"oldfieldtype": "Data",
- "print_hide": 1
+ "print_hide": 1,
+ "search_index": 1
},
{
"fieldname": "column_break_15",
@@ -407,7 +408,8 @@
"no_copy": 1,
"options": "Purchase Invoice",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "section_addresses",
@@ -1602,7 +1604,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2023-10-16 16:24:51.886231",
+ "modified": "2023-11-03 15:47:30.319200",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1665,4 +1667,4 @@
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 13593bcf9b..171cc0ccdf 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1783,9 +1783,14 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
set_advance_flag(company="_Test Company", flag=0, default_account="")
def test_gl_entries_for_standalone_debit_note(self):
- make_purchase_invoice(qty=5, rate=500, update_stock=True)
+ from erpnext.stock.doctype.item.test_item import make_item
- returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
+ item_code = make_item(properties={"is_stock_item": 1})
+ make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True)
+
+ returned_inv = make_purchase_invoice(
+ item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True
+ )
# override the rate with valuation rate
sle = frappe.get_all(
@@ -1795,7 +1800,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
)[0]
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
- self.assertAlmostEqual(returned_inv.items[0].rate, rate)
+ self.assertAlmostEqual(rate, 500)
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
@@ -1898,6 +1903,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
disable_dimension()
def test_repost_accounting_entries(self):
+ # update repost settings
+ settings = frappe.get_doc("Repost Accounting Ledger Settings")
+ if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]:
+ settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True})
+ settings.save()
+
pi = make_purchase_invoice(
rate=1000,
price_list_rate=1000,
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
index 3a87a380d1..c7b7a148cf 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
@@ -5,9 +5,7 @@ frappe.ui.form.on("Repost Accounting Ledger", {
setup: function(frm) {
frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
return {
- filters: {
- name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
- }
+ query: "erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.get_repost_allowed_types"
}
}
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
index dbb0971fde..69cfe9fcd7 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
@@ -10,9 +10,12 @@ from frappe.utils.data import comma_and
class RepostAccountingLedger(Document):
def __init__(self, *args, **kwargs):
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
- self._allowed_types = set(
- ["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
- )
+ self._allowed_types = [
+ x.document_type
+ for x in frappe.db.get_all(
+ "Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
+ )
+ ]
def validate(self):
self.validate_vouchers()
@@ -157,7 +160,7 @@ def start_repost(account_repost_doc=str) -> None:
doc.docstatus = 1
doc.make_gl_entries()
- elif doc.doctype in ["Payment Entry", "Journal Entry"]:
+ elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
if not repost_doc.delete_cancelled_entries:
doc.make_gl_entries(1)
doc.make_gl_entries()
@@ -186,3 +189,18 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]))
)
)
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
+ filters = {"allowed": True}
+
+ if txt:
+ filters.update({"document_type": ("like", f"%{txt}%")})
+
+ if allowed_types := frappe.db.get_all(
+ "Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1
+ ):
+ return allowed_types
+ return []
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
index 0e75dd2e3e..dda0ec778f 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
@@ -20,10 +20,18 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.create_company()
self.create_customer()
self.create_item()
+ self.update_repost_settings()
def teadDown(self):
frappe.db.rollback()
+ def update_repost_settings(self):
+ allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
+ repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
+ for x in allowed_types:
+ repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
+ repost_settings.save()
+
def test_01_basic_functions(self):
si = create_sales_invoice(
item=self.item,
diff --git a/erpnext/projects/report/employee_billing_summary/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/__init__.py
similarity index 100%
rename from erpnext/projects/report/employee_billing_summary/__init__.py
rename to erpnext/accounts/doctype/repost_accounting_ledger_settings/__init__.py
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js
new file mode 100644
index 0000000000..8c83ca5043
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Repost Accounting Ledger Settings", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json
new file mode 100644
index 0000000000..8aa0a840c7
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json
@@ -0,0 +1,46 @@
+{
+ "actions": [],
+ "creation": "2023-11-07 09:57:20.619939",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "allowed_types"
+ ],
+ "fields": [
+ {
+ "fieldname": "allowed_types",
+ "fieldtype": "Table",
+ "label": "Allowed Doctypes",
+ "options": "Repost Allowed Types"
+ }
+ ],
+ "in_create": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2023-11-07 14:24:13.321522",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Accounting Ledger Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "System Manager",
+ "select": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py
new file mode 100644
index 0000000000..2b8230df86
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class RepostAccountingLedgerSettings(Document):
+ pass
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py
new file mode 100644
index 0000000000..ec4e87ffc0
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestRepostAccountingLedgerSettings(FrappeTestCase):
+ pass
diff --git a/erpnext/projects/report/project_billing_summary/__init__.py b/erpnext/accounts/doctype/repost_allowed_types/__init__.py
similarity index 100%
rename from erpnext/projects/report/project_billing_summary/__init__.py
rename to erpnext/accounts/doctype/repost_allowed_types/__init__.py
diff --git a/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json
new file mode 100644
index 0000000000..ede12fbc18
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json
@@ -0,0 +1,45 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-11-07 09:58:03.595382",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "column_break_sfzb",
+ "allowed"
+ ],
+ "fields": [
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Doctype",
+ "options": "DocType"
+ },
+ {
+ "default": "0",
+ "fieldname": "allowed",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Allowed"
+ },
+ {
+ "fieldname": "column_break_sfzb",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-11-07 10:01:39.217861",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Allowed Types",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py
new file mode 100644
index 0000000000..0e4883b0c9
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class RepostAllowedTypes(Document):
+ pass
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index e5adeae501..cd725b9862 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -26,6 +26,7 @@
"is_return",
"return_against",
"update_billed_amount_in_sales_order",
+ "update_billed_amount_in_delivery_note",
"is_debit_note",
"amended_from",
"accounting_dimensions_section",
@@ -2153,6 +2154,13 @@
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company default Cost Center for Round off"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.is_return",
+ "fieldname": "update_billed_amount_in_delivery_note",
+ "fieldtype": "Check",
+ "label": "Update Billed Amount in Delivery Note"
}
],
"icon": "fa fa-file-text",
@@ -2165,7 +2173,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2023-07-25 16:02:18.988799",
+ "modified": "2023-11-03 14:39:38.012346",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f6d9c93261..fa95ccdc57 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -253,6 +253,7 @@ class SalesInvoice(SellingController):
self.update_status_updater_args()
self.update_prevdoc_status()
+
self.update_billing_status_in_dn()
self.clear_unallocated_mode_of_payments()
@@ -1019,7 +1020,7 @@ class SalesInvoice(SellingController):
def make_customer_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total
- # because rounded_total had value even before introcution of posting GLE based on rounded total
+ # because rounded_total had value even before introduction of posting GLE based on rounded total
grand_total = (
self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
)
@@ -1267,7 +1268,7 @@ class SalesInvoice(SellingController):
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= flt(self.change_amount)
- if payment_mode.amount:
+ if payment_mode.base_amount:
# POS, make payment entries
gl_entries.append(
self.get_gl_dict(
@@ -1429,6 +1430,8 @@ class SalesInvoice(SellingController):
)
def update_billing_status_in_dn(self, update_modified=True):
+ if self.is_return and not self.update_billed_amount_in_delivery_note:
+ return
updated_delivery_notes = []
for d in self.get("items"):
if d.dn_detail:
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 310e41208f..16e73ea52f 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -5,7 +5,7 @@
from typing import Optional
import frappe
-from frappe import _, msgprint, scrub
+from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
@@ -480,11 +480,19 @@ def get_party_account_currency(party_type, party, company):
def get_party_gle_currency(party_type, party, company):
def generator():
- existing_gle_currency = frappe.db.sql(
- """select account_currency from `tabGL Entry`
- where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
- limit 1""",
- {"company": company, "party_type": party_type, "party": party},
+ gl = qb.DocType("GL Entry")
+ existing_gle_currency = (
+ qb.from_(gl)
+ .select(gl.account_currency)
+ .where(
+ (gl.docstatus == 1)
+ & (gl.company == company)
+ & (gl.party_type == party_type)
+ & (gl.party == party)
+ & (gl.is_cancelled == 0)
+ )
+ .limit(1)
+ .run()
)
return existing_gle_currency[0][0] if existing_gle_currency else None
diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js
index 8fa81cd9dc..10362dba3d 100644
--- a/erpnext/accounts/report/accounts_payable/accounts_payable.js
+++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js
@@ -148,7 +148,13 @@ frappe.query_reports["Accounts Payable"] = {
"fieldname": "in_party_currency",
"label": __("In Party Currency"),
"fieldtype": "Check",
+ },
+ {
+ "fieldname": "ignore_accounts",
+ "label": __("Group by Voucher"),
+ "fieldtype": "Check",
}
+
],
"formatter": function(value, row, column, data, default_formatter) {
@@ -180,4 +186,4 @@ function get_party_type_options() {
});
});
return options;
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
index c49b67a727..d06b259474 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
@@ -177,7 +177,13 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldname": "in_party_currency",
"label": __("In Party Currency"),
"fieldtype": "Check",
+ },
+ {
+ "fieldname": "ignore_accounts",
+ "label": __("Group by Voucher"),
+ "fieldtype": "Check",
}
+
],
"formatter": function(value, row, column, data, default_formatter) {
@@ -210,4 +216,4 @@ function get_party_type_options() {
});
});
return options;
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index e8cf91507b..e42d325bf1 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -119,7 +119,12 @@ class ReceivablePayableReport(object):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
# get the balance object for voucher_type
- key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
+
+ if self.filters.get("ignore_accounts"):
+ key = (ple.voucher_type, ple.voucher_no, ple.party)
+ else:
+ key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
+
if not key in self.voucher_balance:
self.voucher_balance[key] = frappe._dict(
voucher_type=ple.voucher_type,
@@ -186,7 +191,10 @@ class ReceivablePayableReport(object):
):
return
- key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
+ if self.filters.get("ignore_accounts"):
+ key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
+ else:
+ key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
# If payment is made against credit note
# and credit note is made against a Sales Invoice
@@ -195,13 +203,19 @@ class ReceivablePayableReport(object):
if ple.against_voucher_no in self.return_entries:
return_against = self.return_entries.get(ple.against_voucher_no)
if return_against:
- key = (ple.account, ple.against_voucher_type, return_against, ple.party)
+ if self.filters.get("ignore_accounts"):
+ key = (ple.against_voucher_type, return_against, ple.party)
+ else:
+ key = (ple.account, ple.against_voucher_type, return_against, ple.party)
row = self.voucher_balance.get(key)
if not row:
# no invoice, this is an invoice / stand-alone payment / credit note
- row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
+ if self.filters.get("ignore_accounts"):
+ row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
+ else:
+ row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
row.party_type = ple.party_type
return row
@@ -722,6 +736,7 @@ class ReceivablePayableReport(object):
query = (
qb.from_(ple)
.select(
+ ple.name,
ple.account,
ple.voucher_type,
ple.voucher_no,
@@ -735,13 +750,15 @@ class ReceivablePayableReport(object):
ple.account_currency,
ple.amount,
ple.amount_in_account_currency,
- ple.remarks,
)
.where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter))
.where(Criterion.any(self.or_filters))
)
+ if self.filters.get("show_remarks"):
+ query = query.select(ple.remarks)
+
if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date)
else:
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py
index 099884a48e..696a03b0a7 100644
--- a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py
@@ -79,7 +79,9 @@ class General_Payment_Ledger_Comparison(object):
.select(
gle.company,
gle.account,
+ gle.voucher_type,
gle.voucher_no,
+ gle.party_type,
gle.party,
outstanding,
)
@@ -89,7 +91,9 @@ class General_Payment_Ledger_Comparison(object):
& (gle.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
- .groupby(gle.company, gle.account, gle.voucher_no, gle.party)
+ .groupby(
+ gle.company, gle.account, gle.voucher_type, gle.voucher_no, gle.party_type, gle.party
+ )
.run()
)
@@ -112,7 +116,13 @@ class General_Payment_Ledger_Comparison(object):
self.account_types[acc_type].ple = (
qb.from_(ple)
.select(
- ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
+ ple.company,
+ ple.account,
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.party_type,
+ ple.party,
+ Sum(ple.amount).as_("outstanding"),
)
.where(
(ple.company == self.filters.company)
@@ -120,7 +130,9 @@ class General_Payment_Ledger_Comparison(object):
& (ple.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
- .groupby(ple.company, ple.account, ple.voucher_no, ple.party)
+ .groupby(
+ ple.company, ple.account, ple.voucher_type, ple.voucher_no, ple.party_type, ple.party
+ )
.run()
)
@@ -138,12 +150,12 @@ class General_Payment_Ledger_Comparison(object):
self.diff = frappe._dict({})
for x in self.variation_in_payment_ledger:
- self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
+ self.diff[(x[0], x[1], x[2], x[3], x[4], x[5])] = frappe._dict({"gl_balance": x[6]})
for x in self.variation_in_general_ledger:
- self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update(
- frappe._dict({"pl_balance": x[4]})
- )
+ self.diff.setdefault(
+ (x[0], x[1], x[2], x[3], x[4], x[5]), frappe._dict({"gl_balance": 0.0})
+ ).update(frappe._dict({"pl_balance": x[6]}))
def generate_data(self):
self.data = []
@@ -151,8 +163,12 @@ class General_Payment_Ledger_Comparison(object):
self.data.append(
frappe._dict(
{
- "voucher_no": key[2],
- "party": key[3],
+ "company": key[0],
+ "account": key[1],
+ "voucher_type": key[2],
+ "voucher_no": key[3],
+ "party_type": key[4],
+ "party": key[5],
"gl_balance": val.gl_balance,
"pl_balance": val.pl_balance,
}
@@ -162,12 +178,52 @@ class General_Payment_Ledger_Comparison(object):
def get_columns(self):
self.columns = []
options = None
+ self.columns.append(
+ dict(
+ label=_("Company"),
+ fieldname="company",
+ fieldtype="Link",
+ options="Company",
+ width="100",
+ )
+ )
+
+ self.columns.append(
+ dict(
+ label=_("Account"),
+ fieldname="account",
+ fieldtype="Link",
+ options="Account",
+ width="100",
+ )
+ )
+
+ self.columns.append(
+ dict(
+ label=_("Voucher Type"),
+ fieldname="voucher_type",
+ fieldtype="Link",
+ options="DocType",
+ width="100",
+ )
+ )
+
self.columns.append(
dict(
label=_("Voucher No"),
fieldname="voucher_no",
- fieldtype="Data",
- options=options,
+ fieldtype="Dynamic Link",
+ options="voucher_type",
+ width="100",
+ )
+ )
+
+ self.columns.append(
+ dict(
+ label=_("Party Type"),
+ fieldname="party_type",
+ fieldtype="Link",
+ options="DocType",
width="100",
)
)
@@ -176,8 +232,8 @@ class General_Payment_Ledger_Comparison(object):
dict(
label=_("Party"),
fieldname="party",
- fieldtype="Data",
- options=options,
+ fieldtype="Dynamic Link",
+ options="party_type",
width="100",
)
)
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py
index 4b0e99d712..59e906ba33 100644
--- a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py
@@ -50,7 +50,11 @@ class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
self.assertEqual(len(data), 1)
expected = {
+ "company": sinv.company,
+ "account": sinv.debit_to,
+ "voucher_type": sinv.doctype,
"voucher_no": sinv.name,
+ "party_type": "Customer",
"party": sinv.customer,
"gl_balance": sinv.grand_total,
"pl_balance": sinv.grand_total - 1,
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 37d0659acf..c0b4f59579 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -193,7 +193,13 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "add_values_in_transaction_currency",
"label": __("Add Columns in Transaction Currency"),
"fieldtype": "Check"
+ },
+ {
+ "fieldname": "show_remarks",
+ "label": __("Show Remarks"),
+ "fieldtype": "Check"
}
+
]
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 79bfd7833a..5e484cf558 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -163,6 +163,9 @@ def get_gl_entries(filters, accounting_dimensions):
select_fields = """, debit, credit, debit_in_account_currency,
credit_in_account_currency """
+ if filters.get("show_remarks"):
+ select_fields += """,remarks"""
+
order_by_statement = "order by posting_date, account, creation"
if filters.get("include_dimensions"):
@@ -195,7 +198,7 @@ def get_gl_entries(filters, accounting_dimensions):
voucher_type, voucher_no, {dimension_fields}
cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
- remarks, against, is_opening, creation {select_fields}
+ against, is_opening, creation {select_fields}
from `tabGL Entry`
where company=%(company)s {conditions}
{order_by_statement}
@@ -631,8 +634,10 @@ def get_columns(filters):
"width": 100,
},
{"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
- {"label": _("Remarks"), "fieldname": "remarks", "width": 400},
]
)
+ if filters.get("show_remarks"):
+ columns.extend([{"label": _("Remarks"), "fieldname": "remarks", "width": 400}])
+
return columns
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
index 4a3d9bb479..b6bbd979ed 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
@@ -32,13 +32,6 @@ frappe.query_reports["Profitability Analysis"] = {
"label": __("Accounting Dimension"),
"fieldtype": "Link",
"options": "Accounting Dimension",
- "get_query": () =>{
- return {
- filters: {
- "disabled": 0
- }
- }
- }
},
{
"fieldname": "fiscal_year",
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index eac5426b8b..e842d2e8dc 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -47,6 +47,7 @@ def get_result(
out = []
for name, details in gle_map.items():
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
+ bill_no, bill_date = "", ""
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
@@ -70,7 +71,10 @@ def get_result(
if net_total_map.get(name):
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
- total_amount = grand_total = base_total = tax_amount / (rate / 100)
+ if rate:
+ total_amount = grand_total = base_total = tax_amount / (rate / 100)
+ elif voucher_type == "Purchase Invoice":
+ total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name)
else:
total_amount, grand_total, base_total = net_total_map.get(name)
else:
@@ -96,7 +100,7 @@ def get_result(
row.update(
{
- "section_code": tax_withholding_category,
+ "section_code": tax_withholding_category or "",
"entity_type": party_map.get(party, {}).get(party_type),
"rate": rate,
"total_amount": total_amount,
@@ -106,10 +110,14 @@ def get_result(
"transaction_date": posting_date,
"transaction_type": voucher_type,
"ref_no": name,
+ "supplier_invoice_no": bill_no,
+ "supplier_invoice_date": bill_date,
}
)
out.append(row)
+ out.sort(key=lambda x: x["section_code"])
+
return out
@@ -157,14 +165,14 @@ def get_gle_map(documents):
def get_columns(filters):
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
- {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
{
- "label": _(filters.get("party_type")),
- "fieldname": "party",
- "fieldtype": "Dynamic Link",
- "options": "party_type",
- "width": 180,
+ "label": _("Section Code"),
+ "options": "Tax Withholding Category",
+ "fieldname": "section_code",
+ "fieldtype": "Link",
+ "width": 90,
},
+ {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
]
if filters.naming_series == "Naming Series":
@@ -179,51 +187,60 @@ def get_columns(filters):
columns.extend(
[
- {
- "label": _("Date of Transaction"),
- "fieldname": "transaction_date",
- "fieldtype": "Date",
- "width": 100,
- },
- {
- "label": _("Section Code"),
- "options": "Tax Withholding Category",
- "fieldname": "section_code",
- "fieldtype": "Link",
- "width": 90,
- },
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
- {
- "label": _("Total Amount"),
- "fieldname": "total_amount",
- "fieldtype": "Float",
- "width": 90,
- },
+ ]
+ )
+ if filters.party_type == "Supplier":
+ columns.extend(
+ [
+ {
+ "label": _("Supplier Invoice No"),
+ "fieldname": "supplier_invoice_no",
+ "fieldtype": "Data",
+ "width": 120,
+ },
+ {
+ "label": _("Supplier Invoice Date"),
+ "fieldname": "supplier_invoice_date",
+ "fieldtype": "Date",
+ "width": 120,
+ },
+ ]
+ )
+
+ columns.extend(
+ [
{
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
- "width": 90,
+ "width": 60,
},
{
- "label": _("Tax Amount"),
- "fieldname": "tax_amount",
+ "label": _("Total Amount"),
+ "fieldname": "total_amount",
"fieldtype": "Float",
- "width": 90,
- },
- {
- "label": _("Grand Total"),
- "fieldname": "grand_total",
- "fieldtype": "Float",
- "width": 90,
+ "width": 120,
},
{
"label": _("Base Total"),
"fieldname": "base_total",
"fieldtype": "Float",
- "width": 90,
+ "width": 120,
},
- {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
+ {
+ "label": _("Tax Amount"),
+ "fieldname": "tax_amount",
+ "fieldtype": "Float",
+ "width": 120,
+ },
+ {
+ "label": _("Grand Total"),
+ "fieldname": "grand_total",
+ "fieldtype": "Float",
+ "width": 120,
+ },
+ {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
{
"label": _("Reference No."),
"fieldname": "ref_no",
@@ -231,6 +248,12 @@ def get_columns(filters):
"options": "transaction_type",
"width": 180,
},
+ {
+ "label": _("Date of Transaction"),
+ "fieldname": "transaction_date",
+ "fieldtype": "Date",
+ "width": 100,
+ },
]
)
@@ -253,27 +276,7 @@ def get_tds_docs(filters):
"Tax Withholding Account", {"company": filters.get("company")}, pluck="account"
)
- query_filters = {
- "account": ("in", tds_accounts),
- "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
- "is_cancelled": 0,
- "against": ("not in", bank_accounts),
- }
-
- party = frappe.get_all(filters.get("party_type"), pluck="name")
- or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"})
-
- if filters.get("party"):
- del query_filters["account"]
- del query_filters["against"]
- or_filters = {"against": filters.get("party"), "party": filters.get("party")}
-
- tds_docs = frappe.get_all(
- "GL Entry",
- filters=query_filters,
- or_filters=or_filters,
- fields=["voucher_no", "voucher_type", "against", "party"],
- )
+ tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True)
for d in tds_docs:
if d.voucher_type == "Purchase Invoice":
@@ -309,6 +312,47 @@ def get_tds_docs(filters):
)
+def get_tds_docs_query(filters, bank_accounts, tds_accounts):
+ if not tds_accounts:
+ frappe.throw(
+ _("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")),
+ title="Accounts Missing Error",
+ )
+ gle = frappe.qb.DocType("GL Entry")
+ query = (
+ frappe.qb.from_(gle)
+ .select("voucher_no", "voucher_type", "against", "party")
+ .where((gle.is_cancelled == 0))
+ )
+
+ if filters.get("from_date"):
+ query = query.where(gle.posting_date >= filters.get("from_date"))
+ if filters.get("to_date"):
+ query = query.where(gle.posting_date <= filters.get("to_date"))
+
+ if bank_accounts:
+ query = query.where(gle.against.notin(bank_accounts))
+
+ if filters.get("party"):
+ party = [filters.get("party")]
+ query = query.where(
+ ((gle.account.isin(tds_accounts) & gle.against.isin(party)))
+ | ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
+ | gle.party.isin(party)
+ )
+ else:
+ party = frappe.get_all(filters.get("party_type"), pluck="name")
+ query = query.where(
+ ((gle.account.isin(tds_accounts) & gle.against.isin(party)))
+ | (
+ (gle.voucher_type == "Journal Entry")
+ & ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
+ )
+ | gle.party.isin(party)
+ )
+ return query
+
+
def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
@@ -335,6 +379,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
"base_tax_withholding_net_total",
"grand_total",
"base_total",
+ "bill_no",
+ "bill_date",
],
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
"Payment Entry": [
@@ -353,7 +399,13 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
for entry in entries:
tax_category_map.update({entry.name: entry.tax_withholding_category})
if doctype == "Purchase Invoice":
- value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]
+ value = [
+ entry.base_tax_withholding_net_total,
+ entry.grand_total,
+ entry.base_total,
+ entry.bill_no,
+ entry.bill_date,
+ ]
elif doctype == "Sales Invoice":
value = [entry.base_net_total, entry.grand_total, entry.base_total]
elif doctype == "Payment Entry":
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 1c7052f8ff..e0adac412b 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -10,7 +10,7 @@ import frappe.defaults
from frappe import _, qb, throw
from frappe.model.meta import get_field_precision
from frappe.query_builder import AliasedQuery, Criterion, Table
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import Round, Sum
from frappe.query_builder.utils import DocType
from frappe.utils import (
cint,
@@ -536,6 +536,8 @@ def check_if_advance_entry_modified(args):
)
else:
+ precision = frappe.get_precision("Payment Entry", "unallocated_amount")
+
payment_entry = frappe.qb.DocType("Payment Entry")
payment_ref = frappe.qb.DocType("Payment Entry Reference")
@@ -557,7 +559,10 @@ def check_if_advance_entry_modified(args):
.where(payment_ref.allocated_amount == args.get("unreconciled_amount"))
)
else:
- q = q.where(payment_entry.unallocated_amount == args.get("unreconciled_amount"))
+ q = q.where(
+ Round(payment_entry.unallocated_amount, precision)
+ == Round(args.get("unreconciled_amount"), precision)
+ )
ret = q.run(as_dict=True)
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index f0e4c82048..d378fbd26a 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -9,7 +9,6 @@ frappe.ui.form.on('Asset', {
frm.set_query("item_code", function() {
return {
"filters": {
- "disabled": 0,
"is_fixed_asset": 1,
"is_stock_item": 0
}
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 9d35634933..3c570d1af0 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -818,7 +818,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
"depreciation_method": d.depreciation_method,
"total_number_of_depreciations": d.total_number_of_depreciations,
"frequency_of_depreciation": d.frequency_of_depreciation,
- "daily_depreciation": d.daily_depreciation,
+ "daily_prorata_based": d.daily_prorata_based,
"salvage_value_percentage": d.salvage_value_percentage,
"expected_value_after_useful_life": flt(gross_purchase_amount)
* flt(d.salvage_value_percentage / 100),
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index e2a4b2909a..84a428ca54 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -780,6 +780,15 @@ def get_disposal_account_and_cost_center(company):
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
asset_doc = frappe.get_doc("Asset", asset)
+ if asset_doc.available_for_use_date > getdate(disposal_date):
+ frappe.throw(
+ "Disposal date {0} cannot be before available for use date {1} of the asset.".format(
+ disposal_date, asset_doc.available_for_use_date
+ )
+ )
+ elif asset_doc.available_for_use_date == getdate(disposal_date):
+ return flt(asset_doc.gross_purchase_amount - asset_doc.opening_accumulated_depreciation)
+
if not asset_doc.calculate_depreciation:
return flt(asset_doc.value_after_depreciation)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index d69f5ef0b7..9e3ec6faa8 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -755,7 +755,9 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(schedules, expected_schedules)
- def test_schedule_for_straight_line_method_with_daily_depreciation(self):
+ def test_schedule_for_straight_line_method_with_daily_prorata_based(
+ self,
+ ):
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2023-01-01",
@@ -764,7 +766,7 @@ class TestDepreciationMethods(AssetSetup):
depreciation_start_date="2023-01-31",
total_number_of_depreciations=12,
frequency_of_depreciation=1,
- daily_depreciation=1,
+ daily_prorata_based=1,
)
expected_schedules = [
@@ -1760,7 +1762,7 @@ def create_asset(**args):
"total_number_of_depreciations": args.total_number_of_depreciations or 5,
"expected_value_after_useful_life": args.expected_value_after_useful_life or 0,
"depreciation_start_date": args.depreciation_start_date,
- "daily_depreciation": args.daily_depreciation or 0,
+ "daily_prorata_based": args.daily_prorata_based or 0,
},
)
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
index 3772ef4d68..8d8b46321f 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -19,7 +19,7 @@
"depreciation_method",
"total_number_of_depreciations",
"rate_of_depreciation",
- "daily_depreciation",
+ "daily_prorata_based",
"column_break_8",
"frequency_of_depreciation",
"expected_value_after_useful_life",
@@ -179,9 +179,9 @@
{
"default": "0",
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
- "fieldname": "daily_depreciation",
+ "fieldname": "daily_prorata_based",
"fieldtype": "Check",
- "label": "Daily Depreciation",
+ "label": "Depreciate based on daily pro-rata",
"print_hide": 1,
"read_only": 1
}
@@ -189,7 +189,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-08-10 22:22:09.722968",
+ "modified": "2023-11-03 21:32:15.021796",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 7a88ffc5b7..7305691f97 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -153,7 +153,7 @@ class AssetDepreciationSchedule(Document):
self.frequency_of_depreciation = row.frequency_of_depreciation
self.rate_of_depreciation = row.rate_of_depreciation
self.expected_value_after_useful_life = row.expected_value_after_useful_life
- self.daily_depreciation = row.daily_depreciation
+ self.daily_prorata_based = row.daily_prorata_based
self.status = "Draft"
def make_depr_schedule(
@@ -573,7 +573,7 @@ def get_straight_line_or_manual_depr_amount(
)
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
- if row.daily_depreciation:
+ if row.daily_prorata_based:
daily_depr_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / date_diff(
@@ -618,7 +618,7 @@ def get_straight_line_or_manual_depr_amount(
) / number_of_pending_depreciations
# if the Depreciation Schedule is being prepared for the first time
else:
- if row.daily_depreciation:
+ if row.daily_prorata_based:
daily_depr_amount = (
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index 2c27dc9aca..e597d5fe31 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -8,7 +8,7 @@
"finance_book",
"depreciation_method",
"total_number_of_depreciations",
- "daily_depreciation",
+ "daily_prorata_based",
"column_break_5",
"frequency_of_depreciation",
"depreciation_start_date",
@@ -86,23 +86,23 @@
"fieldtype": "Percent",
"label": "Rate of Depreciation"
},
- {
- "default": "0",
- "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
- "fieldname": "daily_depreciation",
- "fieldtype": "Check",
- "label": "Daily Depreciation"
- },
{
"fieldname": "salvage_value_percentage",
"fieldtype": "Percent",
"label": "Salvage Value Percentage"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
+ "fieldname": "daily_prorata_based",
+ "fieldtype": "Check",
+ "label": "Depreciate based on daily pro-rata"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-09-29 15:39:52.740594",
+ "modified": "2023-11-03 21:30:24.266601",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
diff --git a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py
index 23088c9ccf..a33acfd833 100644
--- a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py
+++ b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py
@@ -13,25 +13,22 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
class TestAssetMaintenance(unittest.TestCase):
def setUp(self):
set_depreciation_settings_in_company()
- create_asset_data()
- create_maintenance_team()
-
- def test_create_asset_maintenance(self):
- pr = make_purchase_receipt(
+ self.pr = make_purchase_receipt(
item_code="Photocopier", qty=1, rate=100000.0, location="Test Location"
)
+ self.asset_name = frappe.db.get_value("Asset", {"purchase_receipt": self.pr.name}, "name")
+ self.asset_doc = frappe.get_doc("Asset", self.asset_name)
- asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
- asset_doc = frappe.get_doc("Asset", asset_name)
+ def test_create_asset_maintenance_with_log(self):
month_end_date = get_last_day(nowdate())
purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
- asset_doc.available_for_use_date = purchase_date
- asset_doc.purchase_date = purchase_date
+ self.asset_doc.available_for_use_date = purchase_date
+ self.asset_doc.purchase_date = purchase_date
- asset_doc.calculate_depreciation = 1
- asset_doc.append(
+ self.asset_doc.calculate_depreciation = 1
+ self.asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 200,
@@ -42,97 +39,40 @@ class TestAssetMaintenance(unittest.TestCase):
},
)
- asset_doc.save()
+ self.asset_doc.save()
- if not frappe.db.exists("Asset Maintenance", "Photocopier"):
- asset_maintenance = frappe.get_doc(
- {
- "doctype": "Asset Maintenance",
- "asset_name": "Photocopier",
- "maintenance_team": "Team Awesome",
- "company": "_Test Company",
- "asset_maintenance_tasks": get_maintenance_tasks(),
- }
- ).insert()
+ asset_maintenance = frappe.get_doc(
+ {
+ "doctype": "Asset Maintenance",
+ "asset_name": self.asset_name,
+ "maintenance_team": "Team Awesome",
+ "company": "_Test Company",
+ "asset_maintenance_tasks": get_maintenance_tasks(),
+ }
+ ).insert()
- next_due_date = calculate_next_due_date(nowdate(), "Monthly")
- self.assertEqual(asset_maintenance.asset_maintenance_tasks[0].next_due_date, next_due_date)
-
- def test_create_asset_maintenance_log(self):
- if not frappe.db.exists("Asset Maintenance Log", "Photocopier"):
- asset_maintenance_log = frappe.get_doc(
- {
- "doctype": "Asset Maintenance Log",
- "asset_maintenance": "Photocopier",
- "task": "Change Oil",
- "completion_date": add_days(nowdate(), 2),
- "maintenance_status": "Completed",
- }
- ).insert()
- asset_maintenance = frappe.get_doc("Asset Maintenance", "Photocopier")
- next_due_date = calculate_next_due_date(asset_maintenance_log.completion_date, "Monthly")
+ next_due_date = calculate_next_due_date(nowdate(), "Monthly")
self.assertEqual(asset_maintenance.asset_maintenance_tasks[0].next_due_date, next_due_date)
+ asset_maintenance_log = frappe.db.get_value(
+ "Asset Maintenance Log",
+ {"asset_maintenance": asset_maintenance.name, "task_name": "Change Oil"},
+ "name",
+ )
-def create_asset_data():
- if not frappe.db.exists("Asset Category", "Equipment"):
- create_asset_category()
-
- if not frappe.db.exists("Location", "Test Location"):
- frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
-
- if not frappe.db.exists("Item", "Photocopier"):
- meta = frappe.get_meta("Asset")
- naming_series = meta.get_field("naming_series").options
- frappe.get_doc(
+ asset_maintenance_log_doc = frappe.get_doc("Asset Maintenance Log", asset_maintenance_log)
+ asset_maintenance_log_doc.update(
{
- "doctype": "Item",
- "item_code": "Photocopier",
- "item_name": "Photocopier",
- "item_group": "All Item Groups",
- "company": "_Test Company",
- "is_fixed_asset": 1,
- "is_stock_item": 0,
- "asset_category": "Equipment",
- "auto_create_assets": 1,
- "asset_naming_series": naming_series,
+ "completion_date": add_days(nowdate(), 2),
+ "maintenance_status": "Completed",
}
- ).insert()
+ )
+ asset_maintenance_log_doc.save()
+ next_due_date = calculate_next_due_date(asset_maintenance_log_doc.completion_date, "Monthly")
-def create_maintenance_team():
- user_list = ["marcus@abc.com", "thalia@abc.com", "mathias@abc.com"]
- if not frappe.db.exists("Role", "Technician"):
- frappe.get_doc({"doctype": "Role", "role_name": "Technician"}).insert()
- for user in user_list:
- if not frappe.db.get_value("User", user):
- frappe.get_doc(
- {
- "doctype": "User",
- "email": user,
- "first_name": user,
- "new_password": "password",
- "roles": [{"doctype": "Has Role", "role": "Technician"}],
- }
- ).insert()
-
- if not frappe.db.exists("Asset Maintenance Team", "Team Awesome"):
- frappe.get_doc(
- {
- "doctype": "Asset Maintenance Team",
- "maintenance_manager": "marcus@abc.com",
- "maintenance_team_name": "Team Awesome",
- "company": "_Test Company",
- "maintenance_team_members": get_maintenance_team(user_list),
- }
- ).insert()
-
-
-def get_maintenance_team(user_list):
- return [
- {"team_member": user, "full_name": user, "maintenance_role": "Technician"}
- for user in user_list[1:]
- ]
+ asset_maintenance.reload()
+ self.assertEqual(asset_maintenance.asset_maintenance_tasks[0].next_due_date, next_due_date)
def get_maintenance_tasks():
@@ -156,23 +96,6 @@ def get_maintenance_tasks():
]
-def create_asset_category():
- asset_category = frappe.new_doc("Asset Category")
- asset_category.asset_category_name = "Equipment"
- asset_category.total_number_of_depreciations = 3
- asset_category.frequency_of_depreciation = 3
- asset_category.append(
- "accounts",
- {
- "company_name": "_Test Company",
- "fixed_asset_account": "_Test Fixed Asset - _TC",
- "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
- "depreciation_expense_account": "_Test Depreciations - _TC",
- },
- )
- asset_category.insert()
-
-
def set_depreciation_settings_in_company():
company = frappe.get_doc("Company", "_Test Company")
company.accumulated_depreciation_account = "_Test Accumulated Depreciations - _TC"
diff --git a/erpnext/assets/doctype/asset_maintenance/test_records.json b/erpnext/assets/doctype/asset_maintenance/test_records.json
new file mode 100644
index 0000000000..8306fad6cb
--- /dev/null
+++ b/erpnext/assets/doctype/asset_maintenance/test_records.json
@@ -0,0 +1,68 @@
+[
+ {
+ "doctype": "Asset Category",
+ "asset_category_name": "Equipment",
+ "total_number_of_depreciations": 3,
+ "frequency_of_depreciation": 3,
+ "accounts": [
+ {
+ "company_name": "_Test Company",
+ "fixed_asset_account": "_Test Fixed Asset - _TC",
+ "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
+ "depreciation_expense_account": "_Test Depreciations - _TC"
+ }
+ ]
+ },
+ {
+ "doctype": "Location",
+ "location_name": "Test Location"
+ },
+ {
+ "doctype": "Role",
+ "role_name": "Technician"
+ },
+ {
+ "doctype": "User",
+ "email": "marcus@abc.com",
+ "first_name": "marcus@abc.com",
+ "new_password": "password",
+ "roles": [{"doctype": "Has Role", "role": "Technician"}]
+ },
+ {
+ "doctype": "User",
+ "email": "thalia@abc.com",
+ "first_name": "thalia@abc.com",
+ "new_password": "password",
+ "roles": [{"doctype": "Has Role", "role": "Technician"}]
+ },
+ {
+ "doctype": "User",
+ "email": "mathias@abc.com",
+ "first_name": "mathias@abc.com",
+ "new_password": "password",
+ "roles": [{"doctype": "Has Role", "role": "Technician"}]
+ },
+ {
+ "doctype": "Asset Maintenance Team",
+ "maintenance_manager": "marcus@abc.com",
+ "maintenance_team_name": "Team Awesome",
+ "company": "_Test Company",
+ "maintenance_team_members": [
+ {"team_member": "marcus@abc.com", "full_name": "marcus@abc.com", "maintenance_role": "Technician"},
+ {"team_member": "thalia@abc.com", "full_name": "thalia@abc.com", "maintenance_role": "Technician"},
+ {"team_member": "mathias@abc.com", "full_name": "mathias@abc.com", "maintenance_role": "Technician"}
+ ]
+ },
+ {
+ "doctype": "Item",
+ "item_code": "Photocopier",
+ "item_name": "Photocopier",
+ "item_group": "All Item Groups",
+ "company": "_Test Company",
+ "is_fixed_asset": 1,
+ "is_stock_item": 0,
+ "asset_category": "Equipment",
+ "auto_create_assets": 1,
+ "asset_naming_series": "ABC.###"
+ }
+]
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index b1da97d634..2b6ffb752f 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -470,6 +470,7 @@
"fieldname": "material_request",
"fieldtype": "Link",
"label": "Material Request",
+ "mandatory_depends_on": "eval: doc.material_request_item",
"no_copy": 1,
"oldfieldname": "prevdoc_docname",
"oldfieldtype": "Link",
@@ -485,6 +486,7 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Material Request Item",
+ "mandatory_depends_on": "eval: doc.material_request",
"no_copy": 1,
"oldfieldname": "prevdoc_detail_docname",
"oldfieldtype": "Data",
@@ -914,7 +916,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-10-27 15:50:42.655573",
+ "modified": "2023-11-06 11:00:53.596417",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index f37db5f115..60dd54c238 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -174,7 +174,7 @@
"fieldname": "supplier_type",
"fieldtype": "Select",
"label": "Supplier Type",
- "options": "Company\nIndividual",
+ "options": "Company\nIndividual\nProprietorship\nPartnership",
"reqd": 1
},
{
@@ -485,7 +485,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-09-25 12:48:21.869563",
+ "modified": "2023-10-19 16:55:15.148325",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
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 fd73b870c5..579c0a65ad 100644
--- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js
+++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js
@@ -44,11 +44,6 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
}
}
}
- else {
- return {
- filters: { "disabled": 0 }
- }
- }
}
},
{
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 6efe631a29..38c1e820d2 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2269,6 +2269,7 @@ class AccountsController(TransactionBase):
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
repost_ledger.company = self.company
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
+ repost_ledger.flags.ignore_permissions = True
repost_ledger.insert()
repost_ledger.submit()
self.db_set("repost_required", 0)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index a76abe2154..ece08d83f9 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -4,7 +4,7 @@
import frappe
from frappe import ValidationError, _, msgprint
-from frappe.contacts.doctype.address.address import get_address_display
+from frappe.contacts.doctype.address.address import render_address
from frappe.utils import cint, flt, getdate
from frappe.utils.data import nowtime
@@ -105,26 +105,26 @@ class BuyingController(SubcontractingController):
def set_rate_for_standalone_debit_note(self):
if self.get("is_return") and self.get("update_stock") and not self.return_against:
for row in self.items:
+ if row.rate <= 0:
+ # override the rate with valuation rate
+ row.rate = get_incoming_rate(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "posting_date": self.get("posting_date"),
+ "posting_time": self.get("posting_time"),
+ "qty": row.qty,
+ "serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
+ "company": self.company,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ },
+ raise_error_if_no_rate=False,
+ )
- # override the rate with valuation rate
- row.rate = get_incoming_rate(
- {
- "item_code": row.item_code,
- "warehouse": row.warehouse,
- "posting_date": self.get("posting_date"),
- "posting_time": self.get("posting_time"),
- "qty": row.qty,
- "serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
- "company": self.company,
- "voucher_type": self.doctype,
- "voucher_no": self.name,
- },
- raise_error_if_no_rate=False,
- )
-
- row.discount_percentage = 0.0
- row.discount_amount = 0.0
- row.margin_rate_or_amount = 0.0
+ row.discount_percentage = 0.0
+ row.discount_amount = 0.0
+ row.margin_rate_or_amount = 0.0
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@@ -246,7 +246,9 @@ class BuyingController(SubcontractingController):
for address_field, address_display_field in address_dict.items():
if self.get(address_field):
- self.set(address_display_field, get_address_display(self.get(address_field)))
+ self.set(
+ address_display_field, render_address(self.get(address_field), check_permissions=False)
+ )
def set_total_in_words(self):
from frappe.utils import money_in_words
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 73a248fb53..d09001c8fc 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -47,15 +47,15 @@ status_map = {
],
[
"To Bill",
- "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1",
+ "eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1",
],
[
"To Deliver",
- "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1 and not self.skip_delivery_note",
+ "eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note",
],
[
"Completed",
- "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1",
+ "eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1",
],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index a7330ec63c..fc45c7ad52 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -1210,8 +1210,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
repost_entry = frappe.new_doc("Repost Item Valuation")
repost_entry.based_on = "Item and Warehouse"
- repost_entry.voucher_type = voucher_type
- repost_entry.voucher_no = voucher_no
repost_entry.item_code = sle.item_code
repost_entry.warehouse = sle.warehouse
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
index 510317f5c2..dfef223c43 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -192,26 +192,6 @@
"onboard": 0,
"type": "Card Break"
},
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "GoCardless Settings",
- "link_count": 0,
- "link_to": "GoCardless Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Mpesa Settings",
- "link_count": 0,
- "link_to": "Mpesa Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
@@ -223,7 +203,7 @@
"type": "Link"
}
],
- "modified": "2023-08-29 15:48:59.010704",
+ "modified": "2023-10-31 19:57:32.748726",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
index f4877fdca0..9e32085351 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
@@ -10,8 +10,8 @@ frappe.views.calendar["Job Card"] = {
},
gantt: {
field_map: {
- "start": "started_time",
- "end": "started_time",
+ "start": "expected_start_date",
+ "end": "expected_end_date",
"id": "name",
"title": "subject",
"color": "color",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js
index 5d883bf9fa..99fca9570f 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_list.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js
@@ -1,6 +1,6 @@
frappe.listview_settings['Job Card'] = {
has_indicator_for_draft: true,
-
+ add_fields: ["expected_start_date", "expected_end_date"],
get_indicator: function(doc) {
const status_colors = {
"Work In Progress": "orange",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 4a0041662b..49386c4ebc 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -36,6 +36,7 @@
"prod_plan_references",
"section_break_24",
"combine_sub_items",
+ "sub_assembly_warehouse",
"section_break_ucc4",
"skip_available_sub_assembly_item",
"column_break_igxl",
@@ -416,13 +417,19 @@
{
"fieldname": "column_break_igxl",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "sub_assembly_warehouse",
+ "fieldtype": "Link",
+ "label": "Sub Assembly Warehouse",
+ "options": "Warehouse"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-09-29 11:41:03.246059",
+ "modified": "2023-11-03 14:08:11.928027",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index ddd9375211..6b12a29b50 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -490,6 +490,12 @@ class ProductionPlan(Document):
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_production_plan()
+ for d in self.sub_assembly_items:
+ if d.fg_warehouse and d.type_of_manufacturing == "In House":
+ bin_name = get_or_make_bin(d.production_item, d.fg_warehouse)
+ bin = frappe.get_doc("Bin", bin_name, for_update=True)
+ bin.update_reserved_qty_for_for_sub_assembly()
+
def delete_draft_work_order(self):
for d in frappe.get_all(
"Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)}
@@ -809,7 +815,11 @@ class ProductionPlan(Document):
bom_data = []
- warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
+ warehouse = (
+ (self.sub_assembly_warehouse or row.warehouse)
+ if self.skip_available_sub_assembly_item
+ else None
+ )
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
sub_assembly_items_store.extend(bom_data)
@@ -831,7 +841,7 @@ class ProductionPlan(Document):
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
- data.fg_warehouse = row.warehouse
+ data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
data.schedule_date = row.planned_start_date
data.type_of_manufacturing = manufacturing_type or (
"Subcontract" if data.is_sub_contracted_item else "In House"
@@ -1637,8 +1647,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
query = query.run()
- if not query:
- return 0.0
+ if not query or query[0][0] is None:
+ return None
reserved_qty_for_production_plan = flt(query[0][0])
@@ -1735,7 +1745,10 @@ def get_raw_materials_of_sub_assembly_items(
if not item.conversion_factor and item.purchase_uom:
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
- item_details.setdefault(item.get("item_code"), item)
+ if details := item_details.get(item.get("item_code")):
+ details.qty += item.get("qty")
+ else:
+ item_details.setdefault(item.get("item_code"), item)
return item_details
@@ -1777,3 +1790,29 @@ def sales_order_query(
query = query.offset(start)
return query.run()
+
+
+def get_reserved_qty_for_sub_assembly(item_code, warehouse):
+ table = frappe.qb.DocType("Production Plan")
+ child = frappe.qb.DocType("Production Plan Sub Assembly Item")
+
+ query = (
+ frappe.qb.from_(table)
+ .inner_join(child)
+ .on(table.name == child.parent)
+ .select(Sum(child.qty - IfNull(child.wo_produced_qty, 0)))
+ .where(
+ (table.docstatus == 1)
+ & (child.production_item == item_code)
+ & (child.fg_warehouse == warehouse)
+ & (table.status.notin(["Completed", "Closed"]))
+ )
+ )
+
+ query = query.run()
+
+ if not query or query[0][0] is None:
+ return None
+
+ qty = flt(query[0][0])
+ return qty if qty > 0 else 0.0
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 6ab9232788..e9c6ee3af2 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1042,13 +1042,14 @@ class TestProductionPlan(FrappeTestCase):
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty - before_qty, 1)
-
pln = frappe.get_doc("Production Plan", pln.name)
pln.cancel()
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
+ pln.reload()
+ self.assertEqual(pln.docstatus, 2)
self.assertEqual(after_qty, before_qty)
def test_resered_qty_for_production_plan_for_work_order(self):
@@ -1332,6 +1333,120 @@ class TestProductionPlan(FrappeTestCase):
self.assertTrue(row.warehouse == mrp_warhouse)
self.assertEqual(row.quantity, 12)
+ def test_mr_qty_for_same_rm_with_different_sub_assemblies(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ bom_tree = {
+ "Fininshed Goods2 For SUB Test": {
+ "SubAssembly2 For SUB Test": {"ChildPart2 For SUB Test": {}},
+ "SubAssembly3 For SUB Test": {"ChildPart2 For SUB Test": {}},
+ }
+ }
+
+ parent_bom = create_nested_bom(bom_tree, prefix="")
+ plan = create_production_plan(
+ item_code=parent_bom.item,
+ planned_qty=1,
+ ignore_existing_ordered_qty=1,
+ do_not_submit=1,
+ skip_available_sub_assembly_item=1,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ plan.get_sub_assembly_items()
+ plan.make_material_request()
+
+ for row in plan.mr_items:
+ if row.item_code == "ChildPart2 For SUB Test":
+ self.assertEqual(row.quantity, 2)
+
+ def test_reserve_sub_assembly_items(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ bom_tree = {
+ "Fininshed Goods Bicycle": {
+ "Frame Assembly": {"Frame": {}},
+ "Chain Assembly": {"Chain": {}},
+ }
+ }
+ parent_bom = create_nested_bom(bom_tree, prefix="")
+
+ warehouse = "_Test Warehouse - _TC"
+ company = "_Test Company"
+
+ sub_assembly_warehouse = create_warehouse("SUB ASSEMBLY WH", company=company)
+
+ for item_code in ["Frame", "Chain"]:
+ make_stock_entry(item_code=item_code, target=warehouse, qty=2, basic_rate=100)
+
+ before_qty = flt(
+ frappe.db.get_value(
+ "Bin",
+ {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+ "reserved_qty_for_production_plan",
+ )
+ )
+
+ plan = create_production_plan(
+ item_code=parent_bom.item,
+ planned_qty=2,
+ ignore_existing_ordered_qty=1,
+ do_not_submit=1,
+ skip_available_sub_assembly_item=1,
+ warehouse=warehouse,
+ sub_assembly_warehouse=sub_assembly_warehouse,
+ )
+
+ plan.get_sub_assembly_items()
+ plan.submit()
+
+ after_qty = flt(
+ frappe.db.get_value(
+ "Bin",
+ {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+ "reserved_qty_for_production_plan",
+ )
+ )
+
+ self.assertEqual(after_qty, before_qty + 2)
+
+ plan.make_work_order()
+ work_orders = frappe.get_all(
+ "Work Order",
+ fields=["name", "production_item"],
+ filters={"production_plan": plan.name},
+ order_by="creation desc",
+ )
+
+ for d in work_orders:
+ wo_doc = frappe.get_doc("Work Order", d.name)
+ wo_doc.skip_transfer = 1
+ wo_doc.from_wip_warehouse = 1
+
+ wo_doc.wip_warehouse = (
+ warehouse
+ if d.production_item in ["Frame Assembly", "Chain Assembly"]
+ else sub_assembly_warehouse
+ )
+
+ wo_doc.submit()
+
+ if d.production_item == "Frame Assembly":
+ self.assertEqual(wo_doc.fg_warehouse, sub_assembly_warehouse)
+ se_doc = frappe.get_doc(make_se_from_wo(wo_doc.name, "Manufacture", 2))
+ se_doc.submit()
+
+ after_qty = flt(
+ frappe.db.get_value(
+ "Bin",
+ {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+ "reserved_qty_for_production_plan",
+ )
+ )
+
+ self.assertEqual(after_qty, before_qty)
+
def create_production_plan(**args):
"""
@@ -1352,6 +1467,7 @@ def create_production_plan(**args):
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
"get_items_from": "Sales Order",
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
+ "sub_assembly_warehouse": args.sub_assembly_warehouse,
}
)
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
index fde0404c01..aff740b732 100644
--- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
@@ -17,11 +17,10 @@
"type_of_manufacturing",
"supplier",
"work_order_details_section",
- "work_order",
+ "wo_produced_qty",
"purchase_order",
"production_plan_item",
"column_break_7",
- "produced_qty",
"received_qty",
"indent",
"section_break_19",
@@ -52,13 +51,6 @@
"fieldtype": "Section Break",
"label": "Reference"
},
- {
- "fieldname": "work_order",
- "fieldtype": "Link",
- "label": "Work Order",
- "options": "Work Order",
- "read_only": 1
- },
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
@@ -81,7 +73,8 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
- "label": "Received Qty"
+ "label": "Received Qty",
+ "read_only": 1
},
{
"fieldname": "bom_no",
@@ -161,12 +154,6 @@
"label": "Target Warehouse",
"options": "Warehouse"
},
- {
- "fieldname": "produced_qty",
- "fieldtype": "Data",
- "label": "Produced Quantity",
- "read_only": 1
- },
{
"default": "In House",
"fieldname": "type_of_manufacturing",
@@ -209,12 +196,18 @@
"label": "Projected Qty",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "wo_produced_qty",
+ "fieldtype": "Float",
+ "label": "Produced Qty",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-05-22 17:52:34.708879",
+ "modified": "2023-11-03 13:33:42.959387",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index c828c878eb..0ae7657c42 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -494,6 +494,7 @@ class TestWorkOrder(FrappeTestCase):
"from_time": row.from_time,
"to_time": row.to_time,
"time_in_mins": row.time_in_mins,
+ "completed_qty": 0,
},
)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 58945bba77..d9cc212e8c 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -710,7 +710,7 @@ erpnext.work_order = {
return new Promise((resolve, reject) => {
frappe.prompt({
fieldtype: 'Float',
- label: __('Qty for {0}', [purpose]),
+ label: __('Qty for {0}', [__(purpose)]),
fieldname: 'qty',
description: __('Max: {0}', [max]),
default: max
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index f9fddcbb5e..36a0cae5cc 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -293,6 +293,7 @@ class WorkOrder(Document):
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
if self.production_plan:
+ self.set_produced_qty_for_sub_assembly_item()
self.update_production_plan_status()
def get_transferred_or_manufactured_qty(self, purpose):
@@ -569,16 +570,49 @@ class WorkOrder(Document):
)
def update_planned_qty(self):
+ from erpnext.manufacturing.doctype.production_plan.production_plan import (
+ get_reserved_qty_for_sub_assembly,
+ )
+
+ qty_dict = {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}
+
+ if self.production_plan_sub_assembly_item and self.production_plan:
+ qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly(
+ self.production_item, self.fg_warehouse
+ )
+
update_bin_qty(
self.production_item,
self.fg_warehouse,
- {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)},
+ qty_dict,
)
if self.material_request:
mr_obj = frappe.get_doc("Material Request", self.material_request)
mr_obj.update_requested_qty([self.material_request_item])
+ def set_produced_qty_for_sub_assembly_item(self):
+ table = frappe.qb.DocType("Work Order")
+
+ query = (
+ frappe.qb.from_(table)
+ .select(Sum(table.produced_qty))
+ .where(
+ (table.production_plan == self.production_plan)
+ & (table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item)
+ & (table.docstatus == 1)
+ )
+ ).run()
+
+ produced_qty = flt(query[0][0]) if query else 0
+
+ frappe.db.set_value(
+ "Production Plan Sub Assembly Item",
+ self.production_plan_sub_assembly_item,
+ "wo_produced_qty",
+ produced_qty,
+ )
+
def update_ordered_qty(self):
if (
self.production_plan
diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
index 34edb9d538..8729775dc2 100644
--- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
+++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
@@ -12,7 +12,7 @@ frappe.query_reports["BOM Operations Time"] = {
"options": "Item",
"get_query": () =>{
return {
- filters: { "disabled": 0, "is_stock_item": 1 }
+ filters: { "is_stock_item": 1 }
}
}
},
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d7f33adeea..d394db6d96 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -343,5 +343,11 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
erpnext.patches.v15_0.update_sre_from_voucher_details
erpnext.patches.v14_0.rename_over_order_allowance_field
erpnext.patches.v14_0.migrate_delivery_stop_lock_field
+execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50)
+execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50)
+erpnext.patches.v14_0.add_default_for_repost_settings
+erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month
+erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based
+erpnext.patches.v15_0.set_reserved_stock_in_bin
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v14_0/add_default_for_repost_settings.py b/erpnext/patches/v14_0/add_default_for_repost_settings.py
new file mode 100644
index 0000000000..6cafc66aab
--- /dev/null
+++ b/erpnext/patches/v14_0/add_default_for_repost_settings.py
@@ -0,0 +1,12 @@
+import frappe
+
+
+def execute():
+ """
+ Update Repost Accounting Ledger Settings with default values
+ """
+ allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
+ repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
+ for x in allowed_types:
+ repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
+ repost_settings.save()
diff --git a/erpnext/patches/v15_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py b/erpnext/patches/v15_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py
new file mode 100644
index 0000000000..63dc0e09bc
--- /dev/null
+++ b/erpnext/patches/v15_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ try:
+ rename_field(
+ "Asset Finance Book", "daily_depreciation", "depreciation_amount_based_on_num_days_in_month"
+ )
+ rename_field(
+ "Asset Depreciation Schedule",
+ "daily_depreciation",
+ "depreciation_amount_based_on_num_days_in_month",
+ )
+
+ except Exception as e:
+ if e.args[0] != 1054:
+ raise
diff --git a/erpnext/patches/v15_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py b/erpnext/patches/v15_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py
new file mode 100644
index 0000000000..2c03c23d87
--- /dev/null
+++ b/erpnext/patches/v15_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ try:
+ rename_field(
+ "Asset Finance Book", "depreciation_amount_based_on_num_days_in_month", "daily_prorata_based"
+ )
+ rename_field(
+ "Asset Depreciation Schedule",
+ "depreciation_amount_based_on_num_days_in_month",
+ "daily_prorata_based",
+ )
+
+ except Exception as e:
+ if e.args[0] != 1054:
+ raise
diff --git a/erpnext/patches/v15_0/set_reserved_stock_in_bin.py b/erpnext/patches/v15_0/set_reserved_stock_in_bin.py
new file mode 100644
index 0000000000..fd0a23333e
--- /dev/null
+++ b/erpnext/patches/v15_0/set_reserved_stock_in_bin.py
@@ -0,0 +1,24 @@
+import frappe
+from frappe.query_builder.functions import Sum
+
+
+def execute():
+ sre = frappe.qb.DocType("Stock Reservation Entry")
+ query = (
+ frappe.qb.from_(sre)
+ .select(
+ sre.item_code,
+ sre.warehouse,
+ Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_stock"),
+ )
+ .where((sre.docstatus == 1) & (sre.status.notin(["Delivered", "Cancelled"])))
+ .groupby(sre.item_code, sre.warehouse)
+ )
+
+ for d in query.run(as_dict=True):
+ frappe.db.set_value(
+ "Bin",
+ {"item_code": d.item_code, "warehouse": d.warehouse},
+ "reserved_stock",
+ d.reserved_stock,
+ )
diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py
deleted file mode 100644
index ac1524a49d..0000000000
--- a/erpnext/projects/report/billing_summary.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.utils import flt, time_diff_in_hours
-
-
-def get_columns():
- return [
- {
- "label": _("Employee ID"),
- "fieldtype": "Link",
- "fieldname": "employee",
- "options": "Employee",
- "width": 300,
- },
- {
- "label": _("Employee Name"),
- "fieldtype": "data",
- "fieldname": "employee_name",
- "hidden": 1,
- "width": 200,
- },
- {
- "label": _("Timesheet"),
- "fieldtype": "Link",
- "fieldname": "timesheet",
- "options": "Timesheet",
- "width": 150,
- },
- {"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "total_hours", "width": 150},
- {
- "label": _("Billable Hours"),
- "fieldtype": "Float",
- "fieldname": "total_billable_hours",
- "width": 150,
- },
- {"label": _("Billing Amount"), "fieldtype": "Currency", "fieldname": "amount", "width": 150},
- ]
-
-
-def get_data(filters):
- data = []
- if filters.from_date > filters.to_date:
- frappe.msgprint(_("From Date can not be greater than To Date"))
- return data
-
- timesheets = get_timesheets(filters)
-
- filters.from_date = frappe.utils.get_datetime(filters.from_date)
- filters.to_date = frappe.utils.add_to_date(
- frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1
- )
-
- timesheet_details = get_timesheet_details(filters, timesheets.keys())
-
- for ts, ts_details in timesheet_details.items():
- total_hours = 0
- total_billing_hours = 0
- total_amount = 0
-
- for row in ts_details:
- from_time, to_time = filters.from_date, filters.to_date
-
- if row.to_time < from_time or row.from_time > to_time:
- continue
-
- if row.from_time > from_time:
- from_time = row.from_time
-
- if row.to_time < to_time:
- to_time = row.to_time
-
- activity_duration, billing_duration = get_billable_and_total_duration(row, from_time, to_time)
-
- total_hours += activity_duration
- total_billing_hours += billing_duration
- total_amount += billing_duration * flt(row.billing_rate)
-
- if total_hours:
- data.append(
- {
- "employee": timesheets.get(ts).employee,
- "employee_name": timesheets.get(ts).employee_name,
- "timesheet": ts,
- "total_billable_hours": total_billing_hours,
- "total_hours": total_hours,
- "amount": total_amount,
- }
- )
-
- return data
-
-
-def get_timesheets(filters):
- record_filters = [
- ["start_date", "<=", filters.to_date],
- ["end_date", ">=", filters.from_date],
- ]
- if not filters.get("include_draft_timesheets"):
- record_filters.append(["docstatus", "=", 1])
- else:
- record_filters.append(["docstatus", "!=", 2])
- if "employee" in filters:
- record_filters.append(["employee", "=", filters.employee])
-
- timesheets = frappe.get_all(
- "Timesheet", filters=record_filters, fields=["employee", "employee_name", "name"]
- )
- timesheet_map = frappe._dict()
- for d in timesheets:
- timesheet_map.setdefault(d.name, d)
-
- return timesheet_map
-
-
-def get_timesheet_details(filters, timesheet_list):
- timesheet_details_filter = {"parent": ["in", timesheet_list]}
-
- if "project" in filters:
- timesheet_details_filter["project"] = filters.project
-
- timesheet_details = frappe.get_all(
- "Timesheet Detail",
- filters=timesheet_details_filter,
- fields=[
- "from_time",
- "to_time",
- "hours",
- "is_billable",
- "billing_hours",
- "billing_rate",
- "parent",
- ],
- )
-
- timesheet_details_map = frappe._dict()
- for d in timesheet_details:
- timesheet_details_map.setdefault(d.parent, []).append(d)
-
- return timesheet_details_map
-
-
-def get_billable_and_total_duration(activity, start_time, end_time):
- precision = frappe.get_precision("Timesheet Detail", "hours")
- activity_duration = time_diff_in_hours(end_time, start_time)
- billing_duration = 0.0
- if activity.is_billable:
- billing_duration = activity.billing_hours
- if activity_duration != activity.billing_hours:
- billing_duration = activity_duration * activity.billing_hours / activity.hours
-
- return flt(activity_duration, precision), flt(billing_duration, precision)
diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
deleted file mode 100644
index 2c25465a61..0000000000
--- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-
-frappe.query_reports["Employee Billing Summary"] = {
- "filters": [
- {
- fieldname: "employee",
- label: __("Employee"),
- fieldtype: "Link",
- options: "Employee",
- reqd: 1
- },
- {
- fieldname:"from_date",
- label: __("From Date"),
- fieldtype: "Date",
- default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
- reqd: 1
- },
- {
- fieldname:"to_date",
- label: __("To Date"),
- fieldtype: "Date",
- default: frappe.datetime.add_days(frappe.datetime.month_start(), -1),
- reqd: 1
- },
- {
- fieldname:"include_draft_timesheets",
- label: __("Include Timesheets in Draft Status"),
- fieldtype: "Check",
- },
- ]
-}
diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.json b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.json
deleted file mode 100644
index e5626a0206..0000000000
--- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "add_total_row": 1,
- "creation": "2019-03-08 15:08:19.929728",
- "disable_prepared_report": 0,
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 0,
- "is_standard": "Yes",
- "modified": "2019-06-13 15:54:49.213973",
- "modified_by": "Administrator",
- "module": "Projects",
- "name": "Employee Billing Summary",
- "owner": "Administrator",
- "prepared_report": 0,
- "ref_doctype": "Timesheet",
- "report_name": "Employee Billing Summary",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "Projects User"
- },
- {
- "role": "HR User"
- },
- {
- "role": "Manufacturing User"
- },
- {
- "role": "Employee"
- },
- {
- "role": "Accounts User"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py
deleted file mode 100644
index a2f7378d1b..0000000000
--- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-
-from erpnext.projects.report.billing_summary import get_columns, get_data
-
-
-def execute(filters=None):
- filters = frappe._dict(filters or {})
- columns = get_columns()
-
- data = get_data(filters)
- return columns, data
diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.js b/erpnext/projects/report/project_billing_summary/project_billing_summary.js
deleted file mode 100644
index fce0c68f11..0000000000
--- a/erpnext/projects/report/project_billing_summary/project_billing_summary.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-
-frappe.query_reports["Project Billing Summary"] = {
- "filters": [
- {
- fieldname: "project",
- label: __("Project"),
- fieldtype: "Link",
- options: "Project",
- reqd: 1
- },
- {
- fieldname:"from_date",
- label: __("From Date"),
- fieldtype: "Date",
- default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
- reqd: 1
- },
- {
- fieldname:"to_date",
- label: __("To Date"),
- fieldtype: "Date",
- default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
- reqd: 1
- },
- {
- fieldname:"include_draft_timesheets",
- label: __("Include Timesheets in Draft Status"),
- fieldtype: "Check",
- },
- ]
-}
diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.py b/erpnext/projects/report/project_billing_summary/project_billing_summary.py
deleted file mode 100644
index a2f7378d1b..0000000000
--- a/erpnext/projects/report/project_billing_summary/project_billing_summary.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-
-from erpnext.projects.report.billing_summary import get_columns, get_data
-
-
-def execute(filters=None):
- filters = frappe._dict(filters or {})
- columns = get_columns()
-
- data = get_data(filters)
- return columns, data
diff --git a/erpnext/projects/report/timesheet_billing_summary/__init__.py b/erpnext/projects/report/timesheet_billing_summary/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.js b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.js
new file mode 100644
index 0000000000..1efd0c6733
--- /dev/null
+++ b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.js
@@ -0,0 +1,67 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.query_reports["Timesheet Billing Summary"] = {
+ tree: true,
+ initial_depth: 0,
+ filters: [
+ {
+ fieldname: "employee",
+ label: __("Employee"),
+ fieldtype: "Link",
+ options: "Employee",
+ on_change: function (report) {
+ unset_group_by(report, "employee");
+ },
+ },
+ {
+ fieldname: "project",
+ label: __("Project"),
+ fieldtype: "Link",
+ options: "Project",
+ on_change: function (report) {
+ unset_group_by(report, "project");
+ },
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.add_months(
+ frappe.datetime.month_start(),
+ -1
+ ),
+ },
+ {
+ fieldname: "to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.add_days(
+ frappe.datetime.month_start(),
+ -1
+ ),
+ },
+ { // NOTE: `update_group_by_options` expects this filter to be the fifth in the list
+ fieldname: "group_by",
+ label: __("Group By"),
+ fieldtype: "Select",
+ options: [
+ "",
+ { value: "employee", label: __("Employee") },
+ { value: "project", label: __("Project") },
+ { value: "date", label: __("Start Date") },
+ ],
+ },
+ {
+ fieldname: "include_draft_timesheets",
+ label: __("Include Timesheets in Draft Status"),
+ fieldtype: "Check",
+ },
+ ],
+};
+
+function unset_group_by(report, fieldname) {
+ if (report.get_filter_value(fieldname) && report.get_filter_value("group_by") == fieldname) {
+ report.set_filter_value("group_by", "");
+ }
+}
diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.json b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.json
similarity index 61%
rename from erpnext/projects/report/project_billing_summary/project_billing_summary.json
rename to erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.json
index 817d0cdb66..0f070cb457 100644
--- a/erpnext/projects/report/project_billing_summary/project_billing_summary.json
+++ b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.json
@@ -1,36 +1,42 @@
{
"add_total_row": 1,
- "creation": "2019-03-11 16:22:39.460524",
- "disable_prepared_report": 0,
+ "columns": [],
+ "creation": "2023-10-10 23:53:43.692067",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
+ "filters": [],
"idx": 0,
"is_standard": "Yes",
- "modified": "2019-06-13 15:54:55.255947",
+ "letter_head": "ALYF GmbH",
+ "letterhead": null,
+ "modified": "2023-10-11 00:58:30.639078",
"modified_by": "Administrator",
"module": "Projects",
- "name": "Project Billing Summary",
+ "name": "Timesheet Billing Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Timesheet",
- "report_name": "Project Billing Summary",
+ "report_name": "Timesheet Billing Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Projects User"
},
{
- "role": "HR User"
+ "role": "Employee"
+ },
+ {
+ "role": "Accounts User"
},
{
"role": "Manufacturing User"
},
{
- "role": "Employee"
+ "role": "HR User"
},
{
- "role": "Accounts User"
+ "role": "Employee Self Service"
}
]
}
\ No newline at end of file
diff --git a/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py
new file mode 100644
index 0000000000..a6e7150e41
--- /dev/null
+++ b/erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py
@@ -0,0 +1,146 @@
+import frappe
+from frappe import _
+from frappe.model.docstatus import DocStatus
+
+
+def execute(filters=None):
+ group_fieldname = filters.pop("group_by", None)
+
+ filters = frappe._dict(filters or {})
+ columns = get_columns(filters, group_fieldname)
+
+ data = get_data(filters, group_fieldname)
+ return columns, data
+
+
+def get_columns(filters, group_fieldname=None):
+ group_columns = {
+ "date": {
+ "label": _("Date"),
+ "fieldtype": "Date",
+ "fieldname": "date",
+ "width": 150,
+ },
+ "project": {
+ "label": _("Project"),
+ "fieldtype": "Link",
+ "fieldname": "project",
+ "options": "Project",
+ "width": 200,
+ "hidden": int(bool(filters.get("project"))),
+ },
+ "employee": {
+ "label": _("Employee ID"),
+ "fieldtype": "Link",
+ "fieldname": "employee",
+ "options": "Employee",
+ "width": 200,
+ "hidden": int(bool(filters.get("employee"))),
+ },
+ }
+ columns = []
+ if group_fieldname:
+ columns.append(group_columns.get(group_fieldname))
+ columns.extend(
+ column for column in group_columns.values() if column.get("fieldname") != group_fieldname
+ )
+ else:
+ columns.extend(group_columns.values())
+
+ columns.extend(
+ [
+ {
+ "label": _("Employee Name"),
+ "fieldtype": "data",
+ "fieldname": "employee_name",
+ "hidden": 1,
+ },
+ {
+ "label": _("Timesheet"),
+ "fieldtype": "Link",
+ "fieldname": "timesheet",
+ "options": "Timesheet",
+ "width": 150,
+ },
+ {"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "hours", "width": 150},
+ {
+ "label": _("Billing Hours"),
+ "fieldtype": "Float",
+ "fieldname": "billing_hours",
+ "width": 150,
+ },
+ {
+ "label": _("Billing Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "billing_amount",
+ "width": 150,
+ },
+ ]
+ )
+
+ return columns
+
+
+def get_data(filters, group_fieldname=None):
+ _filters = []
+ if filters.get("employee"):
+ _filters.append(("employee", "=", filters.get("employee")))
+ if filters.get("project"):
+ _filters.append(("Timesheet Detail", "project", "=", filters.get("project")))
+ if filters.get("from_date"):
+ _filters.append(("Timesheet Detail", "from_time", ">=", filters.get("from_date")))
+ if filters.get("to_date"):
+ _filters.append(("Timesheet Detail", "to_time", "<=", filters.get("to_date")))
+ if not filters.get("include_draft_timesheets"):
+ _filters.append(("docstatus", "=", DocStatus.submitted()))
+ else:
+ _filters.append(("docstatus", "in", (DocStatus.submitted(), DocStatus.draft())))
+
+ data = frappe.get_list(
+ "Timesheet",
+ fields=[
+ "name as timesheet",
+ "`tabTimesheet`.employee",
+ "`tabTimesheet`.employee_name",
+ "`tabTimesheet Detail`.from_time as date",
+ "`tabTimesheet Detail`.project",
+ "`tabTimesheet Detail`.hours",
+ "`tabTimesheet Detail`.billing_hours",
+ "`tabTimesheet Detail`.billing_amount",
+ ],
+ filters=_filters,
+ order_by="`tabTimesheet Detail`.from_time",
+ )
+
+ return group_by(data, group_fieldname) if group_fieldname else data
+
+
+def group_by(data, fieldname):
+ groups = {row.get(fieldname) for row in data}
+ grouped_data = []
+ for group in sorted(groups):
+ group_row = {
+ fieldname: group,
+ "hours": sum(row.get("hours") for row in data if row.get(fieldname) == group),
+ "billing_hours": sum(row.get("billing_hours") for row in data if row.get(fieldname) == group),
+ "billing_amount": sum(row.get("billing_amount") for row in data if row.get(fieldname) == group),
+ "indent": 0,
+ "is_group": 1,
+ }
+ if fieldname == "employee":
+ group_row["employee_name"] = next(
+ row.get("employee_name") for row in data if row.get(fieldname) == group
+ )
+
+ grouped_data.append(group_row)
+ for row in data:
+ if row.get(fieldname) != group:
+ continue
+
+ _row = row.copy()
+ _row[fieldname] = None
+ _row["indent"] = 1
+ _row["is_group"] = 0
+ grouped_data.append(_row)
+
+ return grouped_data
diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json
index 94ae9c04a4..e6bead9ff4 100644
--- a/erpnext/projects/workspace/projects/projects.json
+++ b/erpnext/projects/workspace/projects/projects.json
@@ -155,9 +155,9 @@
"dependencies": "Project",
"hidden": 0,
"is_query_report": 1,
- "label": "Project Billing Summary",
+ "label": "Timesheet Billing Summary",
"link_count": 0,
- "link_to": "Project Billing Summary",
+ "link_to": "Timesheet Billing Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
@@ -192,7 +192,7 @@
"type": "Link"
}
],
- "modified": "2023-07-04 14:39:08.935853",
+ "modified": "2023-10-10 23:54:33.082108",
"modified_by": "Administrator",
"module": "Projects",
"name": "Projects",
@@ -234,8 +234,8 @@
"type": "DocType"
},
{
- "label": "Project Billing Summary",
- "link_to": "Project Billing Summary",
+ "label": "Timesheet Billing Summary",
+ "link_to": "Timesheet Billing Summary",
"type": "Report"
},
{
diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js
index 354552137b..7879173cd1 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -30,7 +30,6 @@ erpnext.accounts.taxes = {
filters: {
"account_type": account_type,
"company": doc.company,
- "disabled": 0
}
}
});
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
index fd2b6a4eaa..79fd2ebdbe 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
@@ -3,10 +3,10 @@
frappe.ui.form.on('Quality Procedure', {
refresh: function(frm) {
- frm.set_query("procedure","processes", (frm) =>{
+ frm.set_query('procedure', 'processes', (frm) =>{
return {
filters: {
- name: ["not in", [frm.parent_quality_procedure, frm.name]]
+ name: ['not in', [frm.parent_quality_procedure, frm.name]]
}
};
});
@@ -14,7 +14,8 @@ frappe.ui.form.on('Quality Procedure', {
frm.set_query('parent_quality_procedure', function(){
return {
filters: {
- is_group: 1
+ is_group: 1,
+ name: ['!=', frm.doc.name]
}
};
});
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
index e8604080fb..6834abc9d4 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
@@ -16,16 +16,13 @@ class QualityProcedure(NestedSet):
def on_update(self):
NestedSet.on_update(self)
self.set_parent()
+ self.remove_parent_from_old_child()
+ self.add_child_to_parent()
+ self.remove_child_from_old_parent()
def after_insert(self):
self.set_parent()
-
- # add child to parent if missing
- if self.parent_quality_procedure:
- parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure)
- if not [d for d in parent.processes if d.procedure == self.name]:
- parent.append("processes", {"procedure": self.name, "process_description": self.name})
- parent.save()
+ self.add_child_to_parent()
def on_trash(self):
# clear from child table (sub procedures)
@@ -36,15 +33,6 @@ class QualityProcedure(NestedSet):
)
NestedSet.on_trash(self, allow_root_deletion=True)
- def set_parent(self):
- for process in self.processes:
- # Set parent for only those children who don't have a parent
- has_parent = frappe.db.get_value(
- "Quality Procedure", process.procedure, "parent_quality_procedure"
- )
- if not has_parent and process.procedure:
- frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name)
-
def check_for_incorrect_child(self):
for process in self.processes:
if process.procedure:
@@ -61,6 +49,48 @@ class QualityProcedure(NestedSet):
title=_("Invalid Child Procedure"),
)
+ def set_parent(self):
+ """Set `Parent Procedure` in `Child Procedures`"""
+
+ for process in self.processes:
+ if process.procedure:
+ if not frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure"):
+ frappe.db.set_value(
+ "Quality Procedure", process.procedure, "parent_quality_procedure", self.name
+ )
+
+ def remove_parent_from_old_child(self):
+ """Remove `Parent Procedure` from `Old Child Procedures`"""
+
+ if old_doc := self.get_doc_before_save():
+ if old_child_procedures := set([d.procedure for d in old_doc.processes if d.procedure]):
+ current_child_procedures = set([d.procedure for d in self.processes if d.procedure])
+
+ if removed_child_procedures := list(old_child_procedures.difference(current_child_procedures)):
+ for child_procedure in removed_child_procedures:
+ frappe.db.set_value("Quality Procedure", child_procedure, "parent_quality_procedure", None)
+
+ def add_child_to_parent(self):
+ """Add `Child Procedure` to `Parent Procedure`"""
+
+ if self.parent_quality_procedure:
+ parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure)
+ if not [d for d in parent.processes if d.procedure == self.name]:
+ parent.append("processes", {"procedure": self.name, "process_description": self.name})
+ parent.save()
+
+ def remove_child_from_old_parent(self):
+ """Remove `Child Procedure` from `Old Parent Procedure`"""
+
+ if old_doc := self.get_doc_before_save():
+ if old_parent := old_doc.parent_quality_procedure:
+ if self.parent_quality_procedure != old_parent:
+ parent = frappe.get_doc("Quality Procedure", old_parent)
+ for process in parent.processes:
+ if process.procedure == self.name:
+ parent.remove(process)
+ parent.save()
+
@frappe.whitelist()
def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False):
diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
index 04e8211214..467186debd 100644
--- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
+++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
@@ -1,56 +1,107 @@
# Copyright (c) 2018, Frappe and Contributors
# See license.txt
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
from .quality_procedure import add_node
-class TestQualityProcedure(unittest.TestCase):
+class TestQualityProcedure(FrappeTestCase):
def test_add_node(self):
- try:
- procedure = frappe.get_doc(
- dict(
- doctype="Quality Procedure",
- quality_procedure_name="Test Procedure 1",
- processes=[dict(process_description="Test Step 1")],
- )
- ).insert()
-
- frappe.local.form_dict = frappe._dict(
- doctype="Quality Procedure",
- quality_procedure_name="Test Child 1",
- parent_quality_procedure=procedure.name,
- cmd="test",
- is_root="false",
- )
- node = add_node()
-
- procedure.reload()
-
- self.assertEqual(procedure.is_group, 1)
-
- # child row created
- self.assertTrue([d for d in procedure.processes if d.procedure == node.name])
-
- node.delete()
- procedure.reload()
-
- # child unset
- self.assertFalse([d for d in procedure.processes if d.name == node.name])
-
- finally:
- procedure.delete()
-
-
-def create_procedure():
- return frappe.get_doc(
- dict(
- doctype="Quality Procedure",
- quality_procedure_name="Test Procedure 1",
- is_group=1,
- processes=[dict(process_description="Test Step 1")],
+ procedure = create_procedure(
+ {
+ "quality_procedure_name": "Test Procedure 1",
+ "is_group": 1,
+ "processes": [dict(process_description="Test Step 1")],
+ }
)
- ).insert()
+
+ frappe.local.form_dict = frappe._dict(
+ doctype="Quality Procedure",
+ quality_procedure_name="Test Child 1",
+ parent_quality_procedure=procedure.name,
+ cmd="test",
+ is_root="false",
+ )
+ node = add_node()
+
+ procedure.reload()
+
+ self.assertEqual(procedure.is_group, 1)
+
+ # child row created
+ self.assertTrue([d for d in procedure.processes if d.procedure == node.name])
+
+ node.delete()
+ procedure.reload()
+
+ # child unset
+ self.assertFalse([d for d in procedure.processes if d.name == node.name])
+
+ def test_remove_parent_from_old_child(self):
+ child_qp = create_procedure(
+ {
+ "quality_procedure_name": "Test Child 1",
+ "is_group": 0,
+ }
+ )
+ group_qp = create_procedure(
+ {
+ "quality_procedure_name": "Test Group",
+ "is_group": 1,
+ "processes": [dict(procedure=child_qp.name)],
+ }
+ )
+
+ child_qp.reload()
+ self.assertEqual(child_qp.parent_quality_procedure, group_qp.name)
+
+ group_qp.reload()
+ del group_qp.processes[0]
+ group_qp.save()
+
+ child_qp.reload()
+ self.assertEqual(child_qp.parent_quality_procedure, None)
+
+ def remove_child_from_old_parent(self):
+ child_qp = create_procedure(
+ {
+ "quality_procedure_name": "Test Child 1",
+ "is_group": 0,
+ }
+ )
+ group_qp = create_procedure(
+ {
+ "quality_procedure_name": "Test Group",
+ "is_group": 1,
+ "processes": [dict(procedure=child_qp.name)],
+ }
+ )
+
+ group_qp.reload()
+ self.assertTrue([d for d in group_qp.processes if d.procedure == child_qp.name])
+
+ child_qp.reload()
+ self.assertEqual(child_qp.parent_quality_procedure, group_qp.name)
+
+ child_qp.parent_quality_procedure = None
+ child_qp.save()
+
+ group_qp.reload()
+ self.assertFalse([d for d in group_qp.processes if d.procedure == child_qp.name])
+
+
+def create_procedure(kwargs=None):
+ kwargs = frappe._dict(kwargs or {})
+
+ doc = frappe.new_doc("Quality Procedure")
+ doc.quality_procedure_name = kwargs.quality_procedure_name or "_Test Procedure"
+ doc.is_group = kwargs.is_group or 0
+
+ for process in kwargs.processes or []:
+ doc.append("processes", process)
+
+ doc.insert()
+
+ return doc
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 40cab9f330..3b97123113 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -134,7 +134,7 @@
"label": "Customer Type",
"oldfieldname": "customer_type",
"oldfieldtype": "Select",
- "options": "Company\nIndividual",
+ "options": "Company\nIndividual\nProprietorship\nPartnership",
"reqd": 1
},
{
@@ -584,7 +584,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-09-21 12:23:20.706020",
+ "modified": "2023-10-19 16:56:27.327035",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 94f9d6e37c..e4f1a28316 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -217,7 +217,15 @@ class SalesOrder(SellingController):
def validate_with_previous_doc(self):
super(SalesOrder, self).validate_with_previous_doc(
- {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}}
+ {
+ "Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]},
+ "Quotation Item": {
+ "ref_dn_field": "quotation_item",
+ "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]],
+ "is_child_table": True,
+ "allow_duplicate_prev_row_id": True,
+ },
+ }
)
if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")):
@@ -759,6 +767,8 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
if target.company_address:
target.update(get_fetch_values("Delivery Note", "company_address", target.company_address))
+ # set target items names to ensure proper linking with packed_items
+ target.set_new_name()
make_packing_list(target)
def condition(doc):
@@ -831,6 +841,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
"postprocess": update_dn_item,
}
},
+ ignore_permissions=True,
)
dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1))
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 23b93dc161..1bd469b956 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -40,7 +40,7 @@ frappe.ui.form.on("Company", {
filters:{
'warehouse_type' : 'Transit',
'is_group': 0,
- 'company': frm.doc.company
+ 'company': frm.doc.company_name
}
};
});
diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json
index a11572776a..312470d50e 100644
--- a/erpnext/stock/doctype/bin/bin.json
+++ b/erpnext/stock/doctype/bin/bin.json
@@ -5,20 +5,24 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
- "warehouse",
"item_code",
- "reserved_qty",
+ "column_break_yreo",
+ "warehouse",
+ "section_break_stag",
"actual_qty",
- "ordered_qty",
- "indented_qty",
"planned_qty",
+ "indented_qty",
+ "ordered_qty",
"projected_qty",
+ "column_break_xn5j",
+ "reserved_qty",
"reserved_qty_for_production",
"reserved_qty_for_sub_contract",
"reserved_qty_for_production_plan",
- "ma_rate",
+ "reserved_stock",
+ "section_break_pmrs",
"stock_uom",
- "fcfs_rate",
+ "column_break_0slj",
"valuation_rate",
"stock_value"
],
@@ -56,7 +60,7 @@
"fieldname": "reserved_qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Reserved Quantity",
+ "label": "Reserved Qty",
"oldfieldname": "reserved_qty",
"oldfieldtype": "Currency",
"read_only": 1
@@ -67,7 +71,7 @@
"fieldtype": "Float",
"in_filter": 1,
"in_list_view": 1,
- "label": "Actual Quantity",
+ "label": "Actual Qty",
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
"read_only": 1
@@ -77,7 +81,7 @@
"fieldname": "ordered_qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Ordered Quantity",
+ "label": "Ordered Qty",
"oldfieldname": "ordered_qty",
"oldfieldtype": "Currency",
"read_only": 1
@@ -86,7 +90,7 @@
"default": "0.00",
"fieldname": "indented_qty",
"fieldtype": "Float",
- "label": "Requested Quantity",
+ "label": "Requested Qty",
"oldfieldname": "indented_qty",
"oldfieldtype": "Currency",
"read_only": 1
@@ -116,20 +120,9 @@
{
"fieldname": "reserved_qty_for_sub_contract",
"fieldtype": "Float",
- "label": "Reserved Qty for sub contract",
+ "label": "Reserved Qty for Subcontract",
"read_only": 1
},
- {
- "fieldname": "ma_rate",
- "fieldtype": "Float",
- "hidden": 1,
- "label": "Moving Average Rate",
- "oldfieldname": "ma_rate",
- "oldfieldtype": "Currency",
- "print_hide": 1,
- "read_only": 1,
- "report_hide": 1
- },
{
"fieldname": "stock_uom",
"fieldtype": "Link",
@@ -140,17 +133,6 @@
"options": "UOM",
"read_only": 1
},
- {
- "fieldname": "fcfs_rate",
- "fieldtype": "Float",
- "hidden": 1,
- "label": "FCFS Rate",
- "oldfieldname": "fcfs_rate",
- "oldfieldtype": "Currency",
- "print_hide": 1,
- "read_only": 1,
- "report_hide": 1
- },
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
@@ -172,13 +154,40 @@
"fieldtype": "Float",
"label": "Reserved Qty for Production Plan",
"read_only": 1
+ },
+ {
+ "fieldname": "section_break_stag",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_yreo",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_xn5j",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_pmrs",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_0slj",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "reserved_stock",
+ "fieldtype": "Float",
+ "label": "Reserved Stock",
+ "read_only": 1
}
],
"hide_toolbar": 1,
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2023-05-02 23:26:21.806965",
+ "modified": "2023-11-01 16:51:17.079107",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 5abea9e69f..8b2e5cf9ec 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -34,10 +34,15 @@ class Bin(Document):
get_reserved_qty_for_production_plan,
)
- self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
+ reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
self.item_code, self.warehouse
)
+ if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
+ return
+
+ self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
+
self.db_set(
"reserved_qty_for_production_plan",
flt(self.reserved_qty_for_production_plan),
@@ -48,6 +53,29 @@ class Bin(Document):
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
+ def update_reserved_qty_for_for_sub_assembly(self):
+ from erpnext.manufacturing.doctype.production_plan.production_plan import (
+ get_reserved_qty_for_sub_assembly,
+ )
+
+ reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(
+ self.item_code, self.warehouse
+ )
+
+ if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
+ return
+
+ self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
+ self.set_projected_qty()
+
+ self.db_set(
+ {
+ "projected_qty": self.projected_qty,
+ "reserved_qty_for_production_plan": flt(self.reserved_qty_for_production_plan),
+ },
+ update_modified=True,
+ )
+
def update_reserved_qty_for_production(self):
"""Update qty reserved for production from Production Item tables
in open work orders"""
@@ -148,6 +176,17 @@ class Bin(Document):
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
+ def update_reserved_stock(self):
+ """Update `Reserved Stock` on change in Reserved Qty of Stock Reservation Entry"""
+
+ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+ get_sre_reserved_qty_for_item_and_warehouse,
+ )
+
+ reserved_stock = get_sre_reserved_qty_for_item_and_warehouse(self.item_code, self.warehouse)
+
+ self.db_set("reserved_stock", flt(reserved_stock), update_modified=True)
+
def on_doctype_update():
frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse")
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 190575eb94..66dd33a400 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -365,6 +365,9 @@ class DeliveryNote(SellingController):
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
+ # Update Reserved Stock in Bin.
+ sre_doc.update_reserved_stock_in_bin()
+
qty_to_deliver -= qty_can_be_deliver
if self._action == "cancel":
@@ -427,6 +430,9 @@ class DeliveryNote(SellingController):
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
+ # Update Reserved Stock in Bin.
+ sre_doc.update_reserved_stock_in_bin()
+
qty_to_undelivered -= qty_can_be_undelivered
def validate_against_stock_reservation_entries(self):
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 1eecf6dc2a..137c352e99 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1029,6 +1029,7 @@ class TestDeliveryNote(FrappeTestCase):
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
si1 = make_sales_invoice(dn1.name)
+ si1.update_billed_amount_in_delivery_note = True
si1.insert()
si1.submit()
dn1.reload()
@@ -1037,6 +1038,7 @@ class TestDeliveryNote(FrappeTestCase):
dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
si2 = make_sales_invoice(dn2.name)
+ si2.update_billed_amount_in_delivery_note = True
si2.insert()
si2.submit()
dn2.reload()
diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js
index ce489ff52b..8a4b4eef0a 100644
--- a/erpnext/stock/doctype/item_price/item_price.js
+++ b/erpnext/stock/doctype/item_price/item_price.js
@@ -6,7 +6,6 @@ frappe.ui.form.on("Item Price", {
frm.set_query("item_code", function() {
return {
filters: {
- "disabled": 0,
"has_variants": 0
}
};
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 7f0dc2df9f..8bbc660899 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -205,7 +205,11 @@ class LandedCostVoucher(Document):
)
docs = frappe.db.get_all(
"Asset",
- filters={receipt_document_type: item.receipt_document, "item_code": item.item_code},
+ filters={
+ receipt_document_type: item.receipt_document,
+ "item_code": item.item_code,
+ "docstatus": ["!=", 2],
+ },
fields=["name", "docstatus"],
)
if not docs or len(docs) != item.qty:
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 2a4b6f34b5..cbc1693eaa 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -600,11 +600,10 @@ class PurchaseReceipt(BuyingController):
make_rate_difference_entry(d)
make_sub_contracting_gl_entries(d)
make_divisional_loss_gl_entry(d, outgoing_amount)
- elif (
- d.warehouse not in warehouse_with_no_account
- or d.rejected_warehouse not in warehouse_with_no_account
+ elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
+ d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account
):
- warehouse_with_no_account.append(d.warehouse)
+ warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
if d.is_fixed_asset:
self.update_assets(d, d.valuation_rate)
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 f5240a6094..718f007577 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -900,7 +900,8 @@
"label": "Delivery Note Item",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"collapsible": 1,
@@ -1089,7 +1090,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-10-19 10:50:58.071735",
+ "modified": "2023-10-30 17:32:24.560337",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/quality_inspection_template/test_records.json b/erpnext/stock/doctype/quality_inspection_template/test_records.json
index 980f49a80a..2da99f616a 100644
--- a/erpnext/stock/doctype/quality_inspection_template/test_records.json
+++ b/erpnext/stock/doctype/quality_inspection_template/test_records.json
@@ -1,4 +1,8 @@
[
+ {
+ "doctype": "Quality Inspection Parameter",
+ "parameter" : "_Test Param"
+ },
{
"quality_inspection_template_name" : "_Test Quality Inspection Template",
"doctype": "Quality Inspection Template",
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index 1853f45f58..5b76e442f4 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -5,7 +5,7 @@
from unittest.mock import MagicMock, call
import frappe
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, add_to_date, now, nowdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -137,8 +137,6 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
item_code="_Test Item",
warehouse="_Test Warehouse - _TC",
based_on="Item and Warehouse",
- voucher_type="Sales Invoice",
- voucher_no="SI-1",
posting_date="2021-01-02",
posting_time="00:01:00",
)
@@ -148,8 +146,6 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
riv1.flags.dont_run_in_test = True
riv1.submit()
_assert_status(riv1, "Queued")
- self.assertEqual(riv1.voucher_type, "Sales Invoice") # traceability
- self.assertEqual(riv1.voucher_no, "SI-1")
# newer than existing duplicate - riv1
riv2 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-03"}))
@@ -200,6 +196,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
riv.set_status("Skipped")
+ @change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_prevention_of_cancelled_transaction_riv(self):
frappe.flags.dont_execute_stock_reposts = True
@@ -377,6 +374,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
accounts_settings.acc_frozen_upto = ""
accounts_settings.save()
+ @change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_create_repost_entry_for_cancelled_document(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
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 f96c184c88..f2bbf2b211 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
@@ -121,7 +121,7 @@ class SerialandBatchBundle(Document):
def throw_error_message(self, message, exception=frappe.ValidationError):
frappe.throw(_(message), exception, title=_("Error"))
- def set_incoming_rate(self, row=None, save=False):
+ def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False):
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
"Installation Note",
"Job Card",
@@ -131,7 +131,9 @@ class SerialandBatchBundle(Document):
return
if self.type_of_transaction == "Outward":
- self.set_incoming_rate_for_outward_transaction(row, save)
+ self.set_incoming_rate_for_outward_transaction(
+ row, save, allow_negative_stock=allow_negative_stock
+ )
else:
self.set_incoming_rate_for_inward_transaction(row, save)
@@ -152,7 +154,9 @@ class SerialandBatchBundle(Document):
def get_serial_nos(self):
return [d.serial_no for d in self.entries if d.serial_no]
- def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
+ def set_incoming_rate_for_outward_transaction(
+ self, row=None, save=False, allow_negative_stock=False
+ ):
sle = self.get_sle_for_outward_transaction()
if self.has_serial_no:
@@ -181,7 +185,8 @@ class SerialandBatchBundle(Document):
if self.docstatus == 1:
available_qty += flt(d.qty)
- self.validate_negative_batch(d.batch_no, available_qty)
+ if not allow_negative_stock:
+ self.validate_negative_batch(d.batch_no, available_qty)
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index c41349fcfb..b06de2e2fc 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -161,7 +161,7 @@ class StockEntry(StockController):
if self.is_enqueue_action():
frappe.msgprint(
_(
- "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage"
+ "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage"
)
)
self.queue_action("submit", timeout=2000)
@@ -172,7 +172,7 @@ class StockEntry(StockController):
if self.is_enqueue_action():
frappe.msgprint(
_(
- "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"
+ "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage"
)
)
self.queue_action("cancel", timeout=2000)
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 3e0610ef6e..b640983a09 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1467,6 +1467,7 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(se.items[0].item_name, item.item_name)
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
+ @change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_reposting_for_depedent_warehouse(self):
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 5452692a24..b3998b7c7e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -123,13 +123,6 @@ frappe.ui.form.on("Stock Reconciliation", {
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
- "get_query": function() {
- return {
- "filters": {
- "disabled": 0,
- }
- };
- }
},
{
label: __("Ignore Empty Stock"),
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 98b4ffdfcf..323ad4f57d 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -6,7 +6,7 @@ from typing import Optional
import frappe
from frappe import _, bold, msgprint
from frappe.query_builder.functions import CombineDatetime, Sum
-from frappe.utils import cint, cstr, flt
+from frappe.utils import add_to_date, cint, cstr, flt
import erpnext
from erpnext.accounts.utils import get_company_default
@@ -88,9 +88,12 @@ class StockReconciliation(StockController):
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
- def set_current_serial_and_batch_bundle(self):
+ def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
"""Set Serial and Batch Bundle for each item"""
for item in self.items:
+ if voucher_detail_no and voucher_detail_no != item.name:
+ continue
+
item_details = frappe.get_cached_value(
"Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
)
@@ -148,6 +151,7 @@ class StockReconciliation(StockController):
"warehouse": item.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
+ "ignore_voucher_nos": [self.name],
}
)
)
@@ -163,11 +167,36 @@ class StockReconciliation(StockController):
)
if not serial_and_batch_bundle.entries:
+ if voucher_detail_no:
+ return
+
continue
- item.current_serial_and_batch_bundle = serial_and_batch_bundle.save().name
+ serial_and_batch_bundle.save()
+ item.current_serial_and_batch_bundle = serial_and_batch_bundle.name
item.current_qty = abs(serial_and_batch_bundle.total_qty)
item.current_valuation_rate = abs(serial_and_batch_bundle.avg_rate)
+ if save:
+ sle_creation = frappe.db.get_value(
+ "Serial and Batch Bundle", item.serial_and_batch_bundle, "creation"
+ )
+ creation = add_to_date(sle_creation, seconds=-1)
+ item.db_set(
+ {
+ "current_serial_and_batch_bundle": item.current_serial_and_batch_bundle,
+ "current_qty": item.current_qty,
+ "current_valuation_rate": item.current_valuation_rate,
+ "creation": creation,
+ }
+ )
+
+ serial_and_batch_bundle.db_set(
+ {
+ "creation": creation,
+ "voucher_no": self.name,
+ "voucher_detail_no": voucher_detail_no,
+ }
+ )
def set_new_serial_and_batch_bundle(self):
for item in self.items:
@@ -689,56 +718,84 @@ class StockReconciliation(StockController):
else:
self._cancel()
- def recalculate_current_qty(self, item_code, batch_no):
+ def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
from erpnext.stock.stock_ledger import get_valuation_rate
sl_entries = []
+
for row in self.items:
- if (
- not (row.item_code == item_code and row.batch_no == batch_no)
- and not row.serial_and_batch_bundle
- ):
+ if voucher_detail_no != row.name:
continue
+ current_qty = 0.0
if row.current_serial_and_batch_bundle:
- self.recalculate_qty_for_serial_and_batch_bundle(row)
- continue
-
- current_qty = get_batch_qty_for_stock_reco(
- item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name
- )
+ current_qty = self.get_qty_for_serial_and_batch_bundle(row)
+ elif row.batch_no:
+ current_qty = get_batch_qty_for_stock_reco(
+ row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
+ )
precesion = row.precision("current_qty")
- if flt(current_qty, precesion) == flt(row.current_qty, precesion):
- continue
+ if flt(current_qty, precesion) != flt(row.current_qty, precesion):
+ val_rate = get_valuation_rate(
+ row.item_code,
+ row.warehouse,
+ self.doctype,
+ self.name,
+ company=self.company,
+ batch_no=row.batch_no,
+ serial_and_batch_bundle=row.current_serial_and_batch_bundle,
+ )
- val_rate = get_valuation_rate(
- item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no
- )
+ row.current_valuation_rate = val_rate
+ row.current_qty = current_qty
+ row.db_set(
+ {
+ "current_qty": row.current_qty,
+ "current_valuation_rate": row.current_valuation_rate,
+ "current_amount": flt(row.current_qty * row.current_valuation_rate),
+ }
+ )
- row.current_valuation_rate = val_rate
- if not row.current_qty and current_qty:
- sle = self.get_sle_for_items(row)
- sle.actual_qty = current_qty * -1
- sle.valuation_rate = val_rate
- sl_entries.append(sle)
+ 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 and not row.batch_no)
+ ):
+ self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
+ row.reload()
- row.current_qty = current_qty
- row.db_set(
- {
- "current_qty": row.current_qty,
- "current_valuation_rate": row.current_valuation_rate,
- "current_amount": flt(row.current_qty * row.current_valuation_rate),
- }
- )
+ 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=True)
+ 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 recalculate_qty_for_serial_and_batch_bundle(self, row):
+ def has_negative_stock_allowed(self):
+ allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+
+ if all(d.serial_and_batch_bundle and flt(d.qty) == flt(d.current_qty) for d in self.items):
+ allow_negative_stock = True
+
+ return allow_negative_stock
+
+ def get_qty_for_serial_and_batch_bundle(self, row):
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
precision = doc.entries[0].precision("qty")
+ current_qty = 0
for d in doc.entries:
qty = (
get_batch_qty(
@@ -751,10 +808,12 @@ class StockReconciliation(StockController):
or 0
) * -1
- if flt(d.qty, precision) == flt(qty, precision):
- continue
+ if flt(d.qty, precision) != flt(qty, precision):
+ d.db_set("qty", qty)
- d.db_set("qty", qty)
+ current_qty += qty
+
+ return abs(current_qty)
def get_batch_qty_for_stock_reco(
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 4817c8d8dc..1ec99bf9a5 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -674,6 +674,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(flt(sl_entry.actual_qty), 1.0)
self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0)
+ @change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_backdated_stock_reco_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -741,13 +742,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
se2.cancel()
- self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))
-
- self.assertEqual(
- frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"),
- "Completed",
- )
-
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
@@ -765,6 +759,68 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
+ def test_backdated_stock_reco_entry_with_batch(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+ 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()
+ new_batch_no = 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
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index ca19bbb96a..d9cbf95710 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -205,6 +205,7 @@
"fieldname": "current_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Current Serial / Batch Bundle",
+ "no_copy": 1,
"options": "Serial and Batch Bundle",
"read_only": 1
},
@@ -216,7 +217,7 @@
],
"istable": 1,
"links": [],
- "modified": "2023-07-26 12:54:34.011915",
+ "modified": "2023-11-02 15:47:07.929550",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
index 7c712ce225..68afd996b4 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
@@ -50,7 +50,7 @@
"label": "Limit timeslot for Stock Reposting"
},
{
- "default": "0",
+ "default": "1",
"fieldname": "item_based_reposting",
"fieldtype": "Check",
"label": "Use Item based reposting"
@@ -70,7 +70,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-05-04 16:14:29.080697",
+ "modified": "2023-11-01 16:14:29.080697",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index 6b39965f9b..09542826f3 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -9,6 +9,8 @@ from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt
+from erpnext.stock.utils import get_or_make_bin
+
class StockReservationEntry(Document):
def validate(self) -> None:
@@ -31,6 +33,7 @@ class StockReservationEntry(Document):
self.update_reserved_qty_in_voucher()
self.update_reserved_qty_in_pick_list()
self.update_status()
+ self.update_reserved_stock_in_bin()
def on_update_after_submit(self) -> None:
self.can_be_updated()
@@ -40,12 +43,14 @@ class StockReservationEntry(Document):
self.validate_reservation_based_on_serial_and_batch()
self.update_reserved_qty_in_voucher()
self.update_status()
+ self.update_reserved_stock_in_bin()
self.reload()
def on_cancel(self) -> None:
self.update_reserved_qty_in_voucher()
self.update_reserved_qty_in_pick_list()
self.update_status()
+ self.update_reserved_stock_in_bin()
def validate_amended_doc(self) -> None:
"""Raises an exception if document is amended."""
@@ -341,6 +346,13 @@ class StockReservationEntry(Document):
update_modified=update_modified,
)
+ def update_reserved_stock_in_bin(self) -> None:
+ """Updates `Reserved Stock` in Bin."""
+
+ bin_name = get_or_make_bin(self.item_code, self.warehouse)
+ bin_doc = frappe.get_cached_doc("Bin", bin_name)
+ bin_doc.update_reserved_stock()
+
def update_status(self, status: str = None, update_modified: bool = True) -> None:
"""Updates status based on Voucher Qty, Reserved Qty and Delivered Qty."""
@@ -681,6 +693,68 @@ def get_sre_reserved_qty_for_voucher_detail_no(
return flt(reserved_qty[0][0])
+def get_sre_reserved_serial_nos_details(
+ item_code: str, warehouse: str, serial_nos: list = None
+) -> dict:
+ """Returns a dict of `Serial No` reserved in Stock Reservation Entry. The dict is like {serial_no: sre_name, ...}"""
+
+ sre = frappe.qb.DocType("Stock Reservation Entry")
+ sb_entry = frappe.qb.DocType("Serial and Batch Entry")
+ query = (
+ frappe.qb.from_(sre)
+ .inner_join(sb_entry)
+ .on(sre.name == sb_entry.parent)
+ .select(sb_entry.serial_no, sre.name)
+ .where(
+ (sre.docstatus == 1)
+ & (sre.item_code == item_code)
+ & (sre.warehouse == warehouse)
+ & (sre.reserved_qty > sre.delivered_qty)
+ & (sre.status.notin(["Delivered", "Cancelled"]))
+ & (sre.reservation_based_on == "Serial and Batch")
+ )
+ .orderby(sb_entry.creation)
+ )
+
+ if serial_nos:
+ query = query.where(sb_entry.serial_no.isin(serial_nos))
+
+ return frappe._dict(query.run())
+
+
+def get_sre_reserved_batch_nos_details(
+ item_code: str, warehouse: str, batch_nos: list = None
+) -> dict:
+ """Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}"""
+
+ sre = frappe.qb.DocType("Stock Reservation Entry")
+ sb_entry = frappe.qb.DocType("Serial and Batch Entry")
+ query = (
+ frappe.qb.from_(sre)
+ .inner_join(sb_entry)
+ .on(sre.name == sb_entry.parent)
+ .select(
+ sb_entry.batch_no,
+ Sum(sb_entry.qty - sb_entry.delivered_qty),
+ )
+ .where(
+ (sre.docstatus == 1)
+ & (sre.item_code == item_code)
+ & (sre.warehouse == warehouse)
+ & ((sre.reserved_qty - sre.delivered_qty) > 0)
+ & (sre.status.notin(["Delivered", "Cancelled"]))
+ & (sre.reservation_based_on == "Serial and Batch")
+ )
+ .groupby(sb_entry.batch_no)
+ .orderby(sb_entry.creation)
+ )
+
+ if batch_nos:
+ query = query.where(sb_entry.batch_no.isin(batch_nos))
+
+ return frappe._dict(query.run())
+
+
def get_sre_details_for_voucher(voucher_type: str, voucher_no: str) -> list[dict]:
"""Returns a list of SREs for the provided voucher."""
diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
index f4c74a8aac..dd023e2080 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
@@ -286,6 +286,7 @@ class TestStockReservationEntry(FrappeTestCase):
self.assertEqual(item.stock_reserved_qty, sre_details.reserved_qty)
self.assertEqual(sre_details.status, "Partially Reserved")
+ cancel_stock_reservation_entries("Sales Order", so.name)
se.cancel()
# Test - 3: Stock should be fully Reserved if the Available Qty to Reserve is greater than the Un-reserved Qty.
@@ -493,7 +494,7 @@ class TestStockReservationEntry(FrappeTestCase):
"pick_serial_and_batch_based_on": "FIFO",
},
)
- def test_stock_reservation_from_pick_list(self):
+ def test_stock_reservation_from_pick_list(self) -> None:
items_details = create_items()
create_material_receipt(items_details, self.warehouse, qty=100)
@@ -575,7 +576,7 @@ class TestStockReservationEntry(FrappeTestCase):
"auto_reserve_stock_for_sales_order_on_purchase": 1,
},
)
- def test_stock_reservation_from_purchase_receipt(self):
+ def test_stock_reservation_from_purchase_receipt(self) -> None:
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
@@ -645,6 +646,40 @@ class TestStockReservationEntry(FrappeTestCase):
# Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos.
self.assertEqual(set(sb_details), set(reserved_sb_details))
+ @change_settings(
+ "Stock Settings",
+ {
+ "allow_negative_stock": 0,
+ "enable_stock_reservation": 1,
+ "auto_reserve_serial_and_batch": 1,
+ "pick_serial_and_batch_based_on": "FIFO",
+ },
+ )
+ def test_consider_reserved_stock_while_cancelling_an_inward_transaction(self) -> None:
+ items_details = create_items()
+ se = create_material_receipt(items_details, self.warehouse, qty=100)
+
+ item_list = []
+ for item_code, properties in items_details.items():
+ item_list.append(
+ {
+ "item_code": item_code,
+ "warehouse": self.warehouse,
+ "qty": randint(11, 100),
+ "uom": properties.stock_uom,
+ "rate": randint(10, 400),
+ }
+ )
+
+ so = make_sales_order(
+ item_list=item_list,
+ warehouse=self.warehouse,
+ )
+ so.create_stock_reservation_entries()
+
+ # Test - 1: ValidationError should be thrown as the inwarded stock is reserved.
+ self.assertRaises(frappe.ValidationError, se.cancel)
+
def tearDown(self) -> None:
cancel_all_stock_reservation_entries()
return super().tearDown()
diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js
index f00dd3e791..90b8d45342 100644
--- a/erpnext/stock/page/stock_balance/stock_balance.js
+++ b/erpnext/stock/page/stock_balance/stock_balance.js
@@ -11,6 +11,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) {
label: __('Warehouse'),
fieldtype:'Link',
options:'Warehouse',
+ default: frappe.route_options && frappe.route_options.warehouse,
change: function() {
page.item_dashboard.start = 0;
page.item_dashboard.refresh();
@@ -22,6 +23,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) {
label: __('Item'),
fieldtype:'Link',
options:'Item',
+ default: frappe.route_options && frappe.route_options.item_code,
change: function() {
page.item_dashboard.start = 0;
page.item_dashboard.refresh();
@@ -33,6 +35,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) {
label: __('Item Group'),
fieldtype:'Link',
options:'Item Group',
+ default: frappe.route_options && frappe.route_options.item_group,
change: function() {
page.item_dashboard.start = 0;
page.item_dashboard.refresh();
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index eeef39641b..e59f2fe644 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -249,6 +249,13 @@ def get_columns(filters):
"options": "Serial No",
"width": 100,
},
+ {
+ "label": _("Serial and Batch Bundle"),
+ "fieldname": "serial_and_batch_bundle",
+ "fieldtype": "Link",
+ "options": "Serial and Batch Bundle",
+ "width": 100,
+ },
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
{
"label": _("Project"),
@@ -287,6 +294,7 @@ def get_stock_ledger_entries(filters, items):
sle.voucher_type,
sle.qty_after_transaction,
sle.stock_value_difference,
+ sle.serial_and_batch_bundle,
sle.voucher_no,
sle.stock_value,
sle.batch_no,
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index ca15afe444..fb392f7e36 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -24,6 +24,7 @@ SLE_FIELDS = (
"stock_value_difference",
"valuation_rate",
"voucher_detail_no",
+ "serial_and_batch_bundle",
)
@@ -64,7 +65,11 @@ def add_invariant_check_fields(sles):
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
- if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
+ if (
+ sle.voucher_type == "Stock Reconciliation"
+ and not sle.batch_no
+ and not sle.serial_and_batch_bundle
+ ):
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
if balance_qty is None:
balance_qty = sle.qty_after_transaction
@@ -143,6 +148,12 @@ def get_columns():
"label": _("Batch"),
"options": "Batch",
},
+ {
+ "fieldname": "serial_and_batch_bundle",
+ "fieldtype": "Link",
+ "label": _("Serial and Batch Bundle"),
+ "options": "Serial and Batch Bundle",
+ },
{
"fieldname": "use_batchwise_valuation",
"fieldtype": "Check",
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index b950f18810..e9381d42b9 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -11,17 +11,22 @@ from frappe import _, scrub
from frappe.model.meta import get_field_precision
from frappe.query_builder import Case
from frappe.query_builder.functions import CombineDatetime, Sum
-from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json
+from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json
import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_available_batches,
+)
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
- get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
+ get_sre_reserved_batch_nos_details,
+ get_sre_reserved_serial_nos_details,
)
from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_or_make_bin,
+ get_stock_balance,
get_valuation_method,
)
from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
@@ -88,6 +93,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
if is_stock_item:
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+ args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
update_bin_qty(bin_name, args)
else:
@@ -114,6 +120,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
"voucher_no": args.get("voucher_no"),
"sle_id": args.get("name"),
"creation": args.get("creation"),
+ "reserved_stock": args.get("reserved_stock"),
},
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
@@ -203,6 +210,11 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
sle.allow_negative_stock = allow_negative_stock
sle.via_landed_cost_voucher = via_landed_cost_voucher
sle.submit()
+
+ # Added to handle the case when the stock ledger entry is created from the repostig
+ if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
+ sle.db_set("creation", args.get("creation_time"))
+
return sle
@@ -506,7 +518,7 @@ class update_entries_after(object):
self.new_items_found = False
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
self.affected_transactions: Set[Tuple[str, str]] = set()
- self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
+ self.reserved_stock = flt(self.args.reserved_stock)
self.data = frappe._dict()
self.initialize_previous_data(self.args)
@@ -689,9 +701,11 @@ class update_entries_after(object):
if (
sle.voucher_type == "Stock Reconciliation"
- and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle))
+ and (
+ sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
+ )
and sle.voucher_detail_no
- and sle.actual_qty < 0
+ and not self.args.get("sle_id")
):
self.reset_actual_qty_for_stock_reco(sle)
@@ -754,27 +768,22 @@ class update_entries_after(object):
self.update_outgoing_rate_on_transaction(sle)
def reset_actual_qty_for_stock_reco(self, sle):
- if sle.serial_and_batch_bundle:
- current_qty = frappe.get_cached_value(
- "Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty"
+ doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
+ doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
+
+ if sle.actual_qty < 0:
+ sle.actual_qty = (
+ flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
+ * -1
)
- if current_qty is not None:
- current_qty = abs(current_qty)
- else:
- current_qty = frappe.get_cached_value(
- "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
- )
-
- if current_qty:
- sle.actual_qty = current_qty * -1
- elif current_qty == 0:
- sle.is_cancelled = 1
+ if abs(sle.actual_qty) == 0.0:
+ sle.is_cancelled = 1
def calculate_valuation_for_serial_batch_bundle(self, sle):
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
- doc.set_incoming_rate(save=True)
+ doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
doc.calculate_qty_and_amount(save=True)
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
@@ -1461,6 +1470,7 @@ def get_valuation_rate(
currency=None,
company=None,
raise_error_if_no_rate=True,
+ batch_no=None,
serial_and_batch_bundle=None,
):
@@ -1469,6 +1479,25 @@ def get_valuation_rate(
if not company:
company = frappe.get_cached_value("Warehouse", warehouse, "company")
+ if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
+ table = frappe.qb.DocType("Stock Ledger Entry")
+ query = (
+ frappe.qb.from_(table)
+ .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
+ .where(
+ (table.item_code == item_code)
+ & (table.warehouse == warehouse)
+ & (table.batch_no == batch_no)
+ & (table.is_cancelled == 0)
+ & (table.voucher_no != voucher_no)
+ & (table.voucher_type != voucher_type)
+ )
+ )
+
+ last_valuation_rate = query.run()
+ if last_valuation_rate:
+ return flt(last_valuation_rate[0][0])
+
# Get moving average rate of a specific batch number
if warehouse and serial_and_batch_bundle:
batch_obj = BatchNoValuation(
@@ -1563,8 +1592,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
next_stock_reco_detail = get_next_stock_reco(args)
if next_stock_reco_detail:
detail = next_stock_reco_detail[0]
- if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no):
- regenerate_sle_for_batch_stock_reco(detail)
# add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition(detail)
@@ -1593,16 +1620,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
validate_negative_qty_in_future_sle(args, allow_negative_stock)
-def regenerate_sle_for_batch_stock_reco(detail):
- doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
- doc.recalculate_current_qty(detail.item_code, detail.batch_no)
-
- if not frappe.db.exists(
- "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
- ):
- doc.repost_future_sle_and_gle(force=True)
-
-
def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = 0
if args.get("is_cancelled"):
@@ -1709,22 +1726,23 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
- if not args.batch_no:
- return
+ if args.batch_no:
+ neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
+ if is_negative_with_precision(neg_batch_sle, is_batch=True):
+ message = _(
+ "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
+ ).format(
+ abs(neg_batch_sle[0]["cumulative_total"]),
+ frappe.get_desk_link("Batch", args.batch_no),
+ frappe.get_desk_link("Warehouse", args.warehouse),
+ neg_batch_sle[0]["posting_date"],
+ neg_batch_sle[0]["posting_time"],
+ frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
+ )
+ frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
- neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
- if is_negative_with_precision(neg_batch_sle, is_batch=True):
- message = _(
- "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
- ).format(
- abs(neg_batch_sle[0]["cumulative_total"]),
- frappe.get_desk_link("Batch", args.batch_no),
- frappe.get_desk_link("Warehouse", args.warehouse),
- neg_batch_sle[0]["posting_date"],
- neg_batch_sle[0]["posting_time"],
- frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
- )
- frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
+ if args.reserved_stock:
+ validate_reserved_stock(args)
def is_negative_with_precision(neg_sle, is_batch=False):
@@ -1791,6 +1809,96 @@ def get_future_sle_with_negative_batch_qty(args):
)
+def validate_reserved_stock(kwargs):
+ if kwargs.serial_no:
+ serial_nos = kwargs.serial_no.split("\n")
+ validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
+
+ elif kwargs.batch_no:
+ validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
+
+ elif kwargs.serial_and_batch_bundle:
+ sbb_entries = frappe.db.get_all(
+ "Serial and Batch Entry",
+ {
+ "parenttype": "Serial and Batch Bundle",
+ "parent": kwargs.serial_and_batch_bundle,
+ "docstatus": 1,
+ },
+ ["batch_no", "serial_no"],
+ )
+
+ if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
+ validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
+ elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
+ validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
+
+ # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
+ precision = cint(frappe.db.get_default("float_precision")) or 2
+ balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
+
+ diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
+ if diff < 0 and abs(diff) > 0.0001:
+ msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
+ abs(diff),
+ frappe.get_desk_link("Item", kwargs.item_code),
+ frappe.get_desk_link("Warehouse", kwargs.warehouse),
+ nowdate(),
+ nowtime(),
+ )
+ frappe.throw(msg, title=_("Reserved Stock"))
+
+
+def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
+ if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
+ item_code, warehouse, serial_nos
+ ):
+ if common_serial_nos := list(
+ set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
+ ):
+ msg = _(
+ "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
+ )
+ msg += "
"
+ msg += _("Example: Serial No {0} reserved in {1}.").format(
+ frappe.bold(common_serial_nos[0]),
+ frappe.get_desk_link(
+ "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
+ ),
+ )
+ frappe.throw(msg, title=_("Reserved Serial No."))
+
+
+def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
+ if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
+ available_batches = get_available_batches(
+ frappe._dict(
+ {
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "posting_date": nowdate(),
+ "posting_time": nowtime(),
+ }
+ )
+ )
+ available_batches_map = {row.batch_no: row.qty for row in available_batches}
+ precision = cint(frappe.db.get_default("float_precision")) or 2
+
+ for batch_no in batch_nos:
+ diff = flt(
+ available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
+ )
+ if diff < 0 and abs(diff) > 0.0001:
+ msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
+ abs(diff),
+ frappe.get_desk_link("Batch", batch_no),
+ frappe.get_desk_link("Warehouse", warehouse),
+ nowdate(),
+ nowtime(),
+ )
+ frappe.throw(msg, title=_("Reserved Stock for Batch"))
+
+
def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
return True
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index d4daacd4ea..f96823b290 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -1,13 +1,6 @@
frappe.ui.form.on("Issue", {
onload: function(frm) {
frm.email_field = "raised_by";
- frm.set_query("customer", function () {
- return {
- filters: {
- "disabled": 0
- }
- };
- });
frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html
index 9fe4338477..3b8698f4ab 100644
--- a/erpnext/templates/pages/projects.html
+++ b/erpnext/templates/pages/projects.html
@@ -14,18 +14,16 @@
{% block style %}
{% endblock %}
{% block page_content %}
You haven't created a {{ section_name }} yet
+{{ _("You haven't created a {0} yet").format(section_name) }}