diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index b510651e68f..4098084a802 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -4,7 +4,7 @@
import frappe
from frappe import _, throw
-from frappe.utils import cint, cstr
+from frappe.utils import add_to_date, cint, cstr, pretty_date
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
import erpnext
@@ -481,6 +481,7 @@ def get_account_autoname(account_number, account_name, company):
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
+ _ensure_idle_system()
account = frappe.get_cached_doc("Account", name)
if not account:
return
@@ -542,6 +543,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
+ _ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
@@ -595,3 +597,27 @@ def sync_update_account_number_in_child(
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
update_account_number(d["name"], account_name, account_number, from_descendant=True)
+
+
+def _ensure_idle_system():
+ # Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
+ # 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
+ # 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
+
+ if frappe.flags.in_test:
+ return
+
+ try:
+ # We also lock inserts to GL entry table with for_update here.
+ last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
+ except frappe.QueryTimeoutError:
+ # wait=False fails immediately if there's an active transaction.
+ last_gl_update = add_to_date(None, seconds=-1)
+
+ if last_gl_update > add_to_date(None, minutes=-5):
+ frappe.throw(
+ _(
+ "Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
+ ).format(pretty_date(last_gl_update)),
+ title=_("System In Use"),
+ )
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 55fab670fd8..2e13c932de4 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -258,6 +258,10 @@ frappe.ui.form.on("Payment Entry", {
frappe.flags.allocate_payment_amount = true;
},
+ validate: async function (frm) {
+ await frm.events.set_exchange_gain_loss_deduction(frm);
+ },
+
validate_company: (frm) => {
if (!frm.doc.company) {
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
@@ -1893,8 +1897,6 @@ function prompt_for_missing_account(frm, account) {
(values) => resolve(values?.[account]),
__("Please Specify Account")
);
-
- dialog.on_hide = () => resolve("");
});
}
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 967b12599bf..45462398e1c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1843,7 +1843,7 @@ class PaymentEntry(AccountsController):
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
- elif self.party_type in ("Supplier", "Employee"):
+ elif self.party_type in ("Supplier", "Customer"):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
@@ -3337,13 +3337,14 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
"""Add loss on income discount in base currency."""
precision = doc.precision("total")
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
+ positive_negative = -1 if pe.payment_type == "Pay" else 1
pe.append(
"deductions",
{
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
- "amount": flt(base_loss_on_income, precision),
+ "amount": flt(base_loss_on_income, precision) * positive_negative,
},
)
@@ -3355,6 +3356,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
tax_discount_loss = {}
base_total_tax_loss = 0
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
+ positive_negative = -1 if pe.payment_type == "Pay" else 1
# The same account head could be used more than once
for tax in doc.get("taxes", []):
@@ -3377,7 +3379,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
"account": account,
"cost_center": pe.cost_center
or frappe.get_cached_value("Company", pe.company, "cost_center"),
- "amount": flt(loss, precision),
+ "amount": flt(loss, precision) * positive_negative,
},
)
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 5883d4e2f1f..e43ba85373c 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -282,6 +282,48 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
+ def test_payment_entry_against_payment_terms_with_discount_on_pi(self):
+ pi = make_purchase_invoice(do_not_save=1)
+ create_payment_terms_template_with_discount()
+ pi.payment_terms_template = "Test Discount Template"
+
+ frappe.db.set_value("Company", pi.company, "default_discount_account", "Write Off - _TC")
+
+ pi.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18,
+ },
+ )
+ pi.save()
+ pi.submit()
+
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
+ pe_with_tax_loss = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
+
+ self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
+ self.assertEqual(pe_with_tax_loss.payment_type, "Pay")
+ self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 295.0)
+ self.assertEqual(pe_with_tax_loss.paid_amount, 265.5)
+ self.assertEqual(pe_with_tax_loss.difference_amount, 0)
+ self.assertEqual(pe_with_tax_loss.deductions[0].amount, -25.0) # Loss on Income
+ self.assertEqual(pe_with_tax_loss.deductions[1].amount, -4.5) # Loss on Tax
+ self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
+
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
+
+ self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
+ self.assertEqual(pe.payment_type, "Pay")
+ self.assertEqual(pe.references[0].allocated_amount, 295.0)
+ self.assertEqual(pe.paid_amount, 265.5)
+ self.assertEqual(pe.deductions[0].amount, -29.5)
+ self.assertEqual(pe.difference_amount, 0)
+
def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount()
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 84e44621784..4f6205a2445 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -768,29 +768,39 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
def get_existing_paid_amount(doctype, name):
- PL = frappe.qb.DocType("Payment Ledger Entry")
+ PLE = frappe.qb.DocType("Payment Ledger Entry")
PER = frappe.qb.DocType("Payment Entry Reference")
query = (
- frappe.qb.from_(PL)
+ frappe.qb.from_(PLE)
.left_join(PER)
.on(
- (PL.against_voucher_type == PER.reference_doctype)
- & (PL.against_voucher_no == PER.reference_name)
- & (PL.voucher_type == PER.parenttype)
- & (PL.voucher_no == PER.parent)
+ (PLE.against_voucher_type == PER.reference_doctype)
+ & (PLE.against_voucher_no == PER.reference_name)
+ & (PLE.voucher_type == PER.parenttype)
+ & (PLE.voucher_no == PER.parent)
+ )
+ .select(
+ Abs(Sum(PLE.amount)).as_("total_amount"),
+ Abs(Sum(frappe.qb.terms.Case().when(PER.payment_request.isnotnull(), PLE.amount).else_(0))).as_(
+ "request_paid_amount"
+ ),
+ )
+ .where(
+ (PLE.voucher_type.isin([doctype, "Journal Entry", "Payment Entry"]))
+ & (PLE.against_voucher_type == doctype)
+ & (PLE.against_voucher_no == name)
+ & (PLE.delinked == 0)
+ & (PLE.docstatus == 1)
+ & (PLE.amount < 0)
)
- .select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
- .where(PL.against_voucher_type.eq(doctype))
- .where(PL.against_voucher_no.eq(name))
- .where(PL.amount < 0)
- .where(PL.delinked == 0)
- .where(PER.docstatus == 1)
- .where(PER.payment_request.isnull())
)
- response = query.run()
- return response[0][0] if response[0] else 0
+ result = query.run()
+ ledger_amount = flt(result[0][0]) if result else 0
+ request_paid_amount = flt(result[0][1]) if result else 0
+
+ return ledger_amount - request_paid_amount
def get_gateway_details(args): # nosemgrep
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 7ce6cc0b7b7..02ecb85ac4d 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -581,6 +581,34 @@ class TestPaymentRequest(FrappeTestCase):
pi.load_from_db()
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
+ def test_consider_journal_entry_and_return_invoice(self):
+ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+
+ si = create_sales_invoice(currency="INR", qty=5, rate=500)
+
+ je = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 500, save=False)
+ je.accounts[1].party_type = "Customer"
+ je.accounts[1].party = si.customer
+ je.accounts[1].reference_type = "Sales Invoice"
+ je.accounts[1].reference_name = si.name
+ je.accounts[1].credit_in_account_currency = 500
+ je.submit()
+
+ pe = get_payment_entry("Sales Invoice", si.name)
+ pe.paid_amount = 500
+ pe.references[0].allocated_amount = 500
+ pe.save()
+ pe.submit()
+
+ cr_note = create_sales_invoice(qty=-1, rate=500, is_return=1, return_against=si.name, do_not_save=1)
+ cr_note.update_outstanding_for_self = 0
+ cr_note.save()
+ cr_note.submit()
+
+ si.load_from_db()
+ pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
+ self.assertEqual(pr.grand_total, si.outstanding_amount)
+
def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index 42861140494..c15309df294 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -1623,6 +1623,5 @@
"states": [],
"timeline_field": "customer",
"title_field": "title",
- "track_changes": 1,
- "track_seen": 1
-}
\ No newline at end of file
+ "track_changes": 1
+}
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py
index a8c4a4d8d72..2bcfb66ce73 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py
@@ -4,6 +4,7 @@
import frappe
from frappe import _, msgprint, scrub, unscrub
+from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.document import Document
from frappe.utils import get_link_to_form, now
@@ -204,17 +205,41 @@ class POSProfile(Document):
def get_item_groups(pos_profile):
item_groups = []
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
+ permitted_item_groups = get_permitted_nodes("Item Group")
if pos_profile.get("item_groups"):
# Get items based on the item groups defined in the POS profile
for data in pos_profile.get("item_groups"):
item_groups.extend(
- ["%s" % frappe.db.escape(d.name) for d in get_child_nodes("Item Group", data.item_group)]
+ [
+ "%s" % frappe.db.escape(d.name)
+ for d in get_child_nodes("Item Group", data.item_group)
+ if not permitted_item_groups or d.name in permitted_item_groups
+ ]
)
+ if not item_groups and permitted_item_groups:
+ item_groups = ["%s" % frappe.db.escape(d) for d in permitted_item_groups]
+
return list(set(item_groups))
+def get_permitted_nodes(group_type):
+ nodes = []
+ permitted_nodes = get_permitted_documents(group_type)
+
+ if not permitted_nodes:
+ return nodes
+
+ for node in permitted_nodes:
+ if frappe.db.get_value(group_type, node, "is_group"):
+ nodes.extend([d.name for d in get_child_nodes(group_type, node)])
+ else:
+ nodes.append(node)
+
+ return nodes
+
+
def get_child_nodes(group_type, root):
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
return frappe.db.sql(
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index bc28edbf396..e2386122522 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -2482,6 +2482,76 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
item.reload()
self.assertEqual(item.last_purchase_rate, 0)
+ def test_adjust_incoming_rate_from_pi_with_multi_currency_and_partial_billing(self):
+ frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
+
+ frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
+
+ pr = make_purchase_receipt(
+ qty=10, rate=10, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
+ )
+ pr.conversion_rate = 5300
+ pr.save()
+ pr.submit()
+
+ incoming_rate = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "incoming_rate",
+ )
+ self.assertEqual(incoming_rate, 53000) # Asserting to confirm if the default calculation is correct
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.qty = 1
+
+ pi.save()
+ pi.submit()
+
+ incoming_rate = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "incoming_rate",
+ )
+ # Test 1 : Incoming rate should not change as only the qty has changed and not the rate (this was not the case before)
+ self.assertEqual(incoming_rate, 53000)
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.qty = 1
+ row.rate = 9
+
+ pi.save()
+ pi.submit()
+
+ incoming_rate = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "incoming_rate",
+ )
+ # Test 2 : Rate in new PI is lower than PR, so incoming rate should also be lower
+ self.assertEqual(incoming_rate, 50350)
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.qty = 1
+ row.rate = 12
+
+ pi.save()
+ pi.submit()
+
+ incoming_rate = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "incoming_rate",
+ )
+ # Test 3 : Rate in new PI is higher than PR, so incoming rate should also be higher
+ self.assertEqual(incoming_rate, 54766.667)
+
+ frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
+
+ frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
+
def test_opening_invoice_rounding_adjustment_validation(self):
pi = make_purchase_invoice(do_not_save=1)
pi.items[0].rate = 99.98
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index ed7eb5685c1..792e1ddbdad 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -922,9 +922,25 @@ frappe.ui.form.on("Sales Invoice", {
}
const timesheets = await frm.events.get_timesheet_data(frm, kwargs);
+
+ if (kwargs.item_code) {
+ frm.events.add_timesheet_item(frm, kwargs.item_code, timesheets);
+ }
+
return frm.events.set_timesheet_data(frm, timesheets);
},
+ add_timesheet_item: function (frm, item_code, timesheets) {
+ const row = frm.add_child("items");
+ frappe.model.set_value(row.doctype, row.name, "item_code", item_code);
+ frappe.model.set_value(
+ row.doctype,
+ row.name,
+ "qty",
+ timesheets.reduce((a, b) => a + (b["billing_hours"] || 0.0), 0.0)
+ );
+ },
+
async get_timesheet_data(frm, kwargs) {
return frappe
.call({
@@ -1022,6 +1038,22 @@ frappe.ui.form.on("Sales Invoice", {
fieldtype: "Date",
reqd: 1,
},
+ {
+ label: __("Item Code"),
+ fieldname: "item_code",
+ fieldtype: "Link",
+ options: "Item",
+ get_query: () => {
+ return {
+ query: "erpnext.controllers.queries.item_query",
+ filters: {
+ is_sales_item: 1,
+ customer: frm.doc.customer,
+ has_variants: 0,
+ },
+ };
+ },
+ },
{
fieldtype: "Column Break",
fieldname: "col_break_1",
@@ -1046,6 +1078,7 @@ frappe.ui.form.on("Sales Invoice", {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
+ item_code: data.item_code,
});
d.hide();
},
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 4183cc21ab8..e2c5fd965e8 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -3,6 +3,7 @@
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2022-01-25 10:29:57.771398",
+ "default_print_format": "Sales Invoice Print",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -2177,6 +2178,7 @@
"print_hide": 1
}
],
+ "grid_page_length": 50,
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
@@ -2187,7 +2189,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2025-02-06 15:59:54.636202",
+ "modified": "2025-03-05 17:06:59.720616",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2233,6 +2235,7 @@
}
],
"quick_entry": 1,
+ "row_format": "Dynamic",
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
"show_name_in_global_search": 1,
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index c860fe41a66..9fcb1ae526c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1937,13 +1937,16 @@ def is_overdue(doc, total):
"base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount"
)
- payable_amount = sum(
- payment.get(payment_amount_field)
- for payment in doc.payment_schedule
- if getdate(payment.due_date) < today
+ payable_amount = flt(
+ sum(
+ payment.get(payment_amount_field)
+ for payment in doc.payment_schedule
+ if getdate(payment.due_date) < today
+ ),
+ doc.precision("outstanding_amount"),
)
- return (total - outstanding_amount) < payable_amount
+ return flt(total - outstanding_amount, doc.precision("outstanding_amount")) < payable_amount
def get_discounting_status(sales_invoice):
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 1c33246ee68..99867e94fcb 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -4246,6 +4246,31 @@ class TestSalesInvoice(FrappeTestCase):
doc = frappe.get_doc("Project", project.name)
self.assertEqual(doc.total_billed_amount, si.grand_total)
+ def test_total_billed_amount_with_different_projects(self):
+ # This test case is for checking the scenario where project is set at document level and for **some** child items only, not all
+ from copy import copy
+
+ si = create_sales_invoice(do_not_submit=True)
+
+ project = frappe.new_doc("Project")
+ project.company = "_Test Company"
+ project.project_name = "Test Total Billed Amount"
+ project.save()
+
+ si.project = project.name
+ si.items.append(copy(si.items[0]))
+ si.items.append(copy(si.items[0]))
+ si.items[0].project = project.name
+ si.items[1].project = project.name
+ # Not setting project on last item
+ si.items[1].insert()
+ si.items[2].insert()
+ si.submit()
+
+ project.reload()
+ self.assertIsNone(si.items[2].project)
+ self.assertEqual(project.total_billed_amount, 300)
+
def test_pos_returns_with_party_account_currency(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
index 46b430c6594..51dc3674594 100644
--- a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
+++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
@@ -13,17 +13,15 @@
"fields": [
{
"fieldname": "voucher_type",
- "fieldtype": "Link",
+ "fieldtype": "Data",
"in_list_view": 1,
- "label": "Voucher Type",
- "options": "DocType"
+ "label": "Voucher Type"
},
{
"fieldname": "voucher_name",
- "fieldtype": "Dynamic Link",
+ "fieldtype": "Data",
"in_list_view": 1,
- "label": "Voucher Name",
- "options": "voucher_type"
+ "label": "Voucher Name"
},
{
"fieldname": "taxable_amount",
@@ -36,7 +34,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-01-13 13:40:41.479208",
+ "modified": "2025-02-05 16:39:14.863698",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withheld Vouchers",
diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
index bc2003e2bea..dbb69a2e769 100644
--- a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
+++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
@@ -18,8 +18,8 @@ class TaxWithheldVouchers(Document):
parentfield: DF.Data
parenttype: DF.Data
taxable_amount: DF.Currency
- voucher_name: DF.DynamicLink | None
- voucher_type: DF.Link | None
+ voucher_name: DF.Data | None
+ voucher_type: DF.Data | None
# end: auto-generated types
pass
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 06549973242..a355e5ddf44 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -436,6 +436,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
tax_details.get("tax_withholding_category"),
company,
),
+ as_dict=1,
)
for d in journal_entries_details:
diff --git a/erpnext/accounts/print_format/sales_invoice_print/__init__.py b/erpnext/accounts/print_format/sales_invoice_print/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html b/erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html
new file mode 100644
index 00000000000..958c5c10fc1
--- /dev/null
+++ b/erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html
@@ -0,0 +1,161 @@
+{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}
+ {% if letter_head and not no_letterhead %}
+
{{ letter_head }}
+ {% endif %}
+ {% if print_heading_template %}
+ {{ frappe.render_template(print_heading_template, {"doc":doc}) }}
+ {% else %}
+ {% endif %}
+ {%- if doc.meta.is_submittable and doc.docstatus==2-%}
+
+
{{ _("CANCELLED") }}
+
+ {%- endif -%}
+{%- endmacro -%}
+{% for page in layout %}
+
+
+
+
+ {% if print_settings.repeat_header_footer %}
+
+ {% endif %}
+
+
+
+
{{ doc.customer }}
+
+ {{ doc.address_display }}
+
+
+ {{ _("Conatct: ")+doc.contact_display if doc.contact_display else '' }}
+
+
+ {{ _("Mobile: ")+doc.contact_mobile if doc.contact_mobile else '' }}
+
+
+
+
+
+
+
{{ doc.name }}
+
+
+
+
{{ frappe.utils.format_date(doc.posting_date) }}
+
+
+
+
{{ frappe.utils.format_date(doc.due_date) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | {{ _("Sr") }} |
+ {{ _("Details") }} |
+ {{ _("Qty") }} |
+ {{ _("Rate") }} |
+ {{ _("Amount") }} |
+
+
+ {% for item in doc.items %}
+
+ | {{ loop.index }} |
+
+ {{ item.item_code }}: {{ item.item_name }}
+ {% if (item.description != item.item_name) %}
+ {{ item.description }}
+ {% endif %}
+ |
+
+ {{ item.get_formatted("qty", 0) }}
+ {{ item.get_formatted("uom", 0) }}
+ |
+ {{ item.get_formatted("net_rate", doc) }} |
+ {{ item.get_formatted("net_amount", doc) }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+ {{ doc.in_words }}
+
+
+
+ {{ doc.status }}
+
+
+
+
+
+
{{ doc.get_formatted("net_total", doc) }}
+
+
+ {% for d in doc.taxes %}
+ {% if d.tax_amount %}
+
+
+
{{ d.get_formatted("tax_amount") }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
{{ doc.get_formatted("grand_total", doc) }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ doc.terms if doc.terms else '' }}
+
+
+
+
+{% endfor %}
diff --git a/erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.json b/erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.json
new file mode 100644
index 00000000000..d4acf5fb36e
--- /dev/null
+++ b/erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.json
@@ -0,0 +1,32 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2025-01-22 16:23:51.012200",
+ "css": "",
+ "custom_format": 0,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Sales Invoice",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "",
+ "font_size": 14,
+ "idx": 0,
+ "line_breaks": 0,
+ "margin_bottom": 0.0,
+ "margin_left": 0.0,
+ "margin_right": 0.0,
+ "margin_top": 0.0,
+ "modified": "2025-01-22 16:23:51.012200",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Sales Invoice Print",
+ "owner": "Administrator",
+ "page_number": "Hide",
+ "print_format_builder": 0,
+ "print_format_builder_beta": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
index 8971dc3d37b..69f332d9800 100644
--- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
+++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
@@ -38,6 +38,23 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(data[1][0].get("outstanding"), 300)
self.assertEqual(data[1][0].get("currency"), "USD")
+ def test_account_payable_for_debit_note(self):
+ pi = self.create_purchase_invoice(do_not_submit=True)
+ pi.is_return = 1
+ pi.items[0].qty = -1
+ pi = pi.save().submit()
+
+ filters = {
+ "company": self.company,
+ "party_type": "Supplier",
+ "party": [self.supplier],
+ "report_date": today(),
+ "range": "30, 60, 90, 120",
+ }
+
+ data = execute(filters)
+ self.assertEqual(data[1][0].get("invoiced"), 300)
+
def create_purchase_invoice(self, do_not_submit=False):
frappe.set_user("Administrator")
pi = make_purchase_invoice(
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 1ddf9bce06f..c7a0da5afe9 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -267,6 +267,18 @@ class ReceivablePayableReport:
row.invoiced_in_account_currency += amount_in_account_currency
else:
if self.is_invoice(ple):
+ # when invoice has is_return marked
+ if self.invoice_details.get(row.voucher_no, {}).get("is_return"):
+ # for Credit Note
+ if row.voucher_type == "Sales Invoice":
+ row.credit_note -= amount
+ row.credit_note_in_account_currency -= amount_in_account_currency
+ # for Debit Note
+ else:
+ row.invoiced -= amount
+ row.invoiced_in_account_currency -= amount_in_account_currency
+ return
+
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
@@ -421,7 +433,7 @@ class ReceivablePayableReport:
# nosemgrep
si_list = frappe.db.sql(
"""
- select name, due_date, po_no
+ select name, due_date, po_no, is_return
from `tabSales Invoice`
where posting_date <= %s
and company = %s
@@ -453,7 +465,7 @@ class ReceivablePayableReport:
# nosemgrep
for pi in frappe.db.sql(
"""
- select name, due_date, bill_no, bill_date
+ select name, due_date, bill_no, bill_date, is_return
from `tabPurchase Invoice`
where
posting_date <= %s
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 39ca78153c3..f3513286c9e 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -204,7 +204,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
expected_data_after_credit_note = [
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
- [0, 0, 100.0, 0.0, -100.0, cr_note.name],
+ [0, 0, 0, 100.0, -100.0, cr_note.name],
]
self.assertEqual(len(report[1]), 2)
si_row = next(
@@ -478,13 +478,19 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
report = execute(filters)[1]
self.assertEqual(len(report), 2)
- expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}
+ expected_data = {sr.name: [0.0, 10.0, -10.0, 0.0, -10], si.name: [100.0, 0.0, 100.0, 10.0, 90.0]}
rows = report[:2]
for row in rows:
self.assertEqual(
expected_data[row.voucher_no],
- [row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
+ [
+ row.invoiced or row.paid,
+ row.credit_note,
+ row.outstanding,
+ row.remaining_balance,
+ row.future_amount,
+ ],
)
pe.cancel()
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index bec5d128f0a..5229839bec6 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -50,6 +50,7 @@ def get_group_by_asset_category_data(filters):
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
+ - flt(row.depreciation_eliminated_via_reversal)
)
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
@@ -247,6 +248,7 @@ def get_group_by_asset_data(filters):
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
+ - flt(row.depreciation_eliminated_via_reversal)
)
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
@@ -276,6 +278,7 @@ def get_assets_for_grouped_by_category(filters):
f"""
SELECT results.asset_category,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
+ sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
@@ -284,6 +287,11 @@ def get_assets_for_grouped_by_category(filters):
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
+ ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
+ gle.credit
+ else
+ 0
+ end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
@@ -307,7 +315,6 @@ def get_assets_for_grouped_by_category(filters):
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
- and gle.debit != 0
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition} {finance_book_filter}
@@ -319,6 +326,7 @@ def get_assets_for_grouped_by_category(filters):
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
+ 0 as depreciation_eliminated_via_reversal,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
@@ -354,6 +362,7 @@ def get_assets_for_grouped_by_asset(filters):
f"""
SELECT results.name as asset,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
+ sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.name as name,
@@ -362,6 +371,11 @@ def get_assets_for_grouped_by_asset(filters):
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
+ ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
+ gle.credit
+ else
+ 0
+ end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
@@ -385,7 +399,6 @@ def get_assets_for_grouped_by_asset(filters):
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
- and gle.debit != 0
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{finance_book_filter} {condition}
@@ -397,6 +410,7 @@ def get_assets_for_grouped_by_asset(filters):
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
+ 0 as depreciation_as_on_from_date_credit,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
@@ -503,6 +517,12 @@ def get_columns(filters):
"fieldtype": "Currency",
"width": 270,
},
+ {
+ "label": _("Depreciation eliminated via reversal"),
+ "fieldname": "depreciation_eliminated_via_reversal",
+ "fieldtype": "Currency",
+ "width": 270,
+ },
{
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "net_asset_value_as_on_from_date",
diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py
index f6efc8a685c..dc6192e7544 100644
--- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py
+++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
["Purchase Invoice", "docstatus", "=", 1],
["Purchase Invoice", "per_received", "<", 100],
["Purchase Invoice", "update_stock", "=", 0],
+ ["Purchase Invoice", "is_opening", "!=", "Yes"],
]
if report_filters.get("purchase_invoice"):
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index e540aa9993c..db42d23a839 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
and ba.account=gl.account
and b.{budget_against} = gl.{budget_against}
and gl.fiscal_year between %s and %s
+ and gl.is_cancelled = 0
and b.{budget_against} = %s
and exists(
select
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
index 377777ab2a3..1c85061a551 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
@@ -307,6 +307,7 @@ class Deferred_Revenue_and_Expense_Report:
.where(
(inv.docstatus == 1)
& (deferred_flag_field == 1)
+ & (inv.company == self.filters.company)
& (
(
(self.period_list[0].from_date >= inv_item.service_start_date)
diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js
index f6051d7e04f..1be58ad9d55 100644
--- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js
+++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js
@@ -2,5 +2,27 @@
// For license information, please see license.txt
frappe.query_reports["Delivered Items To Be Billed"] = {
- filters: [],
+ filters: [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ reqd: 1,
+ default: frappe.defaults.get_default("Company"),
+ },
+ {
+ label: __("As on Date"),
+ fieldname: "posting_date",
+ fieldtype: "Date",
+ reqd: 1,
+ default: frappe.datetime.get_today(),
+ },
+ {
+ label: __("Delivery Note"),
+ fieldname: "delivery_note",
+ fieldtype: "Link",
+ options: "Delivery Note",
+ },
+ ],
};
diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py
index 59914dc29ac..d2e5ff28247 100644
--- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py
+++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py
@@ -3,6 +3,7 @@
from frappe import _
+from pypika import Order
from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data
@@ -10,7 +11,7 @@ from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_d
def execute(filters=None):
columns = get_column()
args = get_args()
- data = get_ordered_to_be_billed_data(args)
+ data = get_ordered_to_be_billed_data(args, filters)
return columns, data
@@ -76,13 +77,6 @@ def get_column():
"options": "Project",
"width": 120,
},
- {
- "label": _("Company"),
- "fieldname": "company",
- "fieldtype": "Link",
- "options": "Company",
- "width": 120,
- },
]
@@ -92,5 +86,6 @@ def get_args():
"party": "customer",
"date": "posting_date",
"order": "name",
- "order_by": "desc",
+ "order_by": Order.desc,
+ "reference_field": "delivery_note",
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index ed16892a36e..a62ba2e3732 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -534,6 +534,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
+ keylist.append(gle.get("project"))
key = tuple(keylist)
if key not in consolidated_gle:
@@ -679,10 +680,11 @@ def get_columns(filters):
{"label": _("Against Account"), "fieldname": "against", "width": 120},
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
{"label": _("Party"), "fieldname": "party", "width": 100},
- {"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
]
if filters.get("include_dimensions"):
+ columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
+
for dim in get_accounting_dimensions(as_list=False):
columns.append(
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py
index 39c5311cd99..c0ca604cc6d 100644
--- a/erpnext/accounts/report/non_billed_report.py
+++ b/erpnext/accounts/report/non_billed_report.py
@@ -4,11 +4,12 @@
import frappe
from frappe.model.meta import get_field_precision
+from frappe.query_builder.functions import IfNull, Round
from erpnext import get_default_currency
-def get_ordered_to_be_billed_data(args):
+def get_ordered_to_be_billed_data(args, filters=None):
doctype, party = args.get("doctype"), args.get("party")
child_tab = doctype + " Item"
precision = (
@@ -18,47 +19,57 @@ def get_ordered_to_be_billed_data(args):
or 2
)
- project_field = get_project_field(doctype, party)
+ doctype = frappe.qb.DocType(doctype)
+ child_doctype = frappe.qb.DocType(child_tab)
- return frappe.db.sql(
- """
- Select
- `{parent_tab}`.name, `{parent_tab}`.{date_field},
- `{parent_tab}`.{party}, `{parent_tab}`.{party}_name,
- `{child_tab}`.item_code,
- `{child_tab}`.base_amount,
- (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)),
- (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)),
- (`{child_tab}`.base_amount -
- (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) -
- (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))),
- `{child_tab}`.item_name, `{child_tab}`.description,
- {project_field}, `{parent_tab}`.company
- from
- `{parent_tab}`, `{child_tab}`
- where
- `{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1
- and `{parent_tab}`.status not in ('Closed', 'Completed')
- and `{child_tab}`.amount > 0
- and (`{child_tab}`.base_amount -
- round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) -
- (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0
- order by
- `{parent_tab}`.{order} {order_by}
- """.format(
- parent_tab="tab" + doctype,
- child_tab="tab" + child_tab,
- precision=precision,
- party=party,
- date_field=args.get("date"),
- project_field=project_field,
- order=args.get("order"),
- order_by=args.get("order_by"),
+ docname = filters.get(args.get("reference_field"), None)
+ project_field = get_project_field(doctype, child_doctype, party)
+
+ query = (
+ frappe.qb.from_(doctype)
+ .inner_join(child_doctype)
+ .on(doctype.name == child_doctype.parent)
+ .select(
+ doctype.name,
+ doctype[args.get("date")].as_("date"),
+ doctype[party],
+ doctype[party + "_name"],
+ child_doctype.item_code,
+ child_doctype.base_amount.as_("amount"),
+ (child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1)).as_("billed_amount"),
+ (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0)).as_("returned_amount"),
+ (
+ child_doctype.base_amount
+ - (child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1))
+ - (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0))
+ ).as_("pending_amount"),
+ child_doctype.item_name,
+ child_doctype.description,
+ project_field,
)
+ .where(
+ (doctype.docstatus == 1)
+ & (doctype.status.notin(["Closed", "Completed"]))
+ & (doctype.company == filters.get("company"))
+ & (doctype.posting_date <= filters.get("posting_date"))
+ & (child_doctype.amount > 0)
+ & (
+ child_doctype.base_amount
+ - Round(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1), precision)
+ - (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0))
+ )
+ > 0
+ )
+ .orderby(doctype[args.get("order")], order=args.get("order_by"))
)
+ if docname:
+ query = query.where(doctype.name == docname)
-def get_project_field(doctype, party):
+ return query.run(as_dict=True)
+
+
+def get_project_field(doctype, child_doctype, party):
if party == "supplier":
- doctype = doctype + " Item"
- return "`tab%s`.project" % (doctype)
+ return child_doctype.project
+ return doctype.project
diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.js b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.js
index ad97f270dd3..2577a82ef65 100644
--- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.js
+++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.js
@@ -2,5 +2,27 @@
// For license information, please see license.txt
frappe.query_reports["Received Items To Be Billed"] = {
- filters: [],
+ filters: [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ reqd: 1,
+ default: frappe.defaults.get_default("Company"),
+ },
+ {
+ label: __("As on Date"),
+ fieldname: "posting_date",
+ fieldtype: "Date",
+ reqd: 1,
+ default: frappe.datetime.get_today(),
+ },
+ {
+ label: __("Purchase Receipt"),
+ fieldname: "purchase_receipt",
+ fieldtype: "Link",
+ options: "Purchase Receipt",
+ },
+ ],
};
diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py
index 1dcacb97420..87b7b109b99 100644
--- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py
+++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py
@@ -3,6 +3,7 @@
from frappe import _
+from pypika import Order
from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data
@@ -10,7 +11,7 @@ from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_d
def execute(filters=None):
columns = get_column()
args = get_args()
- data = get_ordered_to_be_billed_data(args)
+ data = get_ordered_to_be_billed_data(args, filters)
return columns, data
@@ -76,13 +77,6 @@ def get_column():
"options": "Project",
"width": 120,
},
- {
- "label": _("Company"),
- "fieldname": "company",
- "fieldtype": "Link",
- "options": "Company",
- "width": 120,
- },
]
@@ -92,5 +86,6 @@ def get_args():
"party": "supplier",
"date": "posting_date",
"order": "name",
- "order_by": "desc",
+ "order_by": Order.desc,
+ "reference_field": "purchase_receipt",
}
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index db7a3a2a70f..2758ff0e26f 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -1417,7 +1417,7 @@ def repost_gle_for_stock_vouchers(
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
- stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
+ stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers, company=company)
if repost_doc and repost_doc.gl_reposting_index:
# Restore progress
stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
@@ -1470,7 +1470,9 @@ def _delete_accounting_ledger_entries(voucher_type, voucher_no):
_delete_pl_entries(voucher_type, voucher_no)
-def sort_stock_vouchers_by_posting_date(stock_vouchers: list[tuple[str, str]]) -> list[tuple[str, str]]:
+def sort_stock_vouchers_by_posting_date(
+ stock_vouchers: list[tuple[str, str]], company=None
+) -> list[tuple[str, str]]:
sle = frappe.qb.DocType("Stock Ledger Entry")
voucher_nos = [v[1] for v in stock_vouchers]
@@ -1481,7 +1483,12 @@ def sort_stock_vouchers_by_posting_date(stock_vouchers: list[tuple[str, str]]) -
.groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_datetime)
.orderby(sle.creation)
- ).run(as_dict=True)
+ )
+
+ if company:
+ sles = sles.where(sle.company == company)
+
+ sles = sles.run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers)
diff --git a/erpnext/accounts/workspace/payables/payables.json b/erpnext/accounts/workspace/payables/payables.json
index f8c85648756..96c626c7291 100644
--- a/erpnext/accounts/workspace/payables/payables.json
+++ b/erpnext/accounts/workspace/payables/payables.json
@@ -93,7 +93,7 @@
},
{
"hidden": 0,
- "is_query_report": 0,
+ "is_query_report": 1,
"label": "Accounts Payable",
"link_count": 0,
"link_to": "Accounts Payable",
@@ -103,7 +103,7 @@
},
{
"hidden": 0,
- "is_query_report": 0,
+ "is_query_report": 1,
"label": "Accounts Payable Summary",
"link_count": 0,
"link_to": "Accounts Payable Summary",
@@ -113,7 +113,7 @@
},
{
"hidden": 0,
- "is_query_report": 0,
+ "is_query_report": 1,
"label": "Purchase Register",
"link_count": 0,
"link_to": "Purchase Register",
@@ -123,7 +123,7 @@
},
{
"hidden": 0,
- "is_query_report": 0,
+ "is_query_report": 1,
"label": "Item-wise Purchase Register",
"link_count": 0,
"link_to": "Item-wise Purchase Register",
@@ -133,7 +133,7 @@
},
{
"hidden": 0,
- "is_query_report": 0,
+ "is_query_report": 1,
"label": "Purchase Order Analysis",
"link_count": 0,
"link_to": "Purchase Order Analysis",
@@ -143,7 +143,7 @@
},
{
"hidden": 0,
- "is_query_report": 0,
+ "is_query_report": 1,
"label": "Received Items To Be Billed",
"link_count": 0,
"link_to": "Received Items To Be Billed",
@@ -153,7 +153,7 @@
},
{
"hidden": 0,
- "is_query_report": 0,
+ "is_query_report": 1,
"label": "Supplier Ledger Summary",
"link_count": 0,
"link_to": "Supplier Ledger Summary",
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 06b851f2cca..aa5fd217665 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -225,7 +225,7 @@
{
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
- "label": "Gross Purchase Amount",
+ "label": "Net Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency"
},
@@ -592,7 +592,7 @@
"link_fieldname": "target_asset"
}
],
- "modified": "2025-02-11 16:01:56.140904",
+ "modified": "2025-02-20 14:09:05.421913",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js
index 28a8b81f3ad..848a31c4dfa 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js
@@ -143,14 +143,19 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
}
}
- set_consumed_stock_items_tagged_to_wip_composite_asset(asset) {
+ set_consumed_stock_items_tagged_to_wip_composite_asset(target_asset) {
var me = this;
- if (asset) {
+ if (target_asset) {
return me.frm.call({
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset",
args: {
- asset: asset,
+ params: {
+ target_asset: target_asset,
+ finance_book: me.frm.doc.finance_book,
+ posting_date: me.frm.doc.posting_date,
+ posting_time: me.frm.doc.posting_time,
+ },
},
callback: function (r) {
if (!r.exc && r.message) {
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index bed6cda43ed..f4addb66eb9 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -856,7 +856,10 @@ def get_service_item_details(args):
@frappe.whitelist()
-def get_items_tagged_to_wip_composite_asset(asset):
+def get_items_tagged_to_wip_composite_asset(params):
+ if isinstance(params, str):
+ params = json.loads(params)
+
fields = [
"item_code",
"item_name",
@@ -871,25 +874,66 @@ def get_items_tagged_to_wip_composite_asset(asset):
"amount",
"is_fixed_asset",
"parent",
+ "name",
]
pr_items = frappe.get_all(
- "Purchase Receipt Item", filters={"wip_composite_asset": asset, "docstatus": 1}, fields=fields
+ "Purchase Receipt Item",
+ filters={"wip_composite_asset": params.get("target_asset"), "docstatus": 1},
+ fields=fields,
)
stock_items = []
asset_items = []
+
for d in pr_items:
if not d.is_fixed_asset:
- stock_items.append(frappe._dict(d))
+ stock_item = process_stock_item(d)
+ if stock_item:
+ stock_items.append(stock_item)
else:
- asset_details = frappe.db.get_value(
- "Asset",
- {"item_code": d.item_code, "purchase_receipt": d.parent},
- ["name as asset", "asset_name"],
- as_dict=1,
- )
- d.update(asset_details)
- asset_items.append(frappe._dict(d))
+ asset_item = process_fixed_asset(d)
+ if asset_item:
+ asset_items.append(asset_item)
return stock_items, asset_items
+
+
+def process_stock_item(d):
+ stock_capitalized = frappe.db.exists(
+ "Asset Capitalization Stock Item",
+ {
+ "purchase_receipt_item": d.name,
+ "parentfield": "stock_items",
+ "parenttype": "Asset Capitalization",
+ "docstatus": 1,
+ },
+ )
+
+ if stock_capitalized:
+ return None
+
+ stock_item_data = frappe._dict(d)
+ stock_item_data.purchase_receipt_item = d.name
+ return stock_item_data
+
+
+def process_fixed_asset(d):
+ asset_details = frappe.db.get_value(
+ "Asset",
+ {
+ "item_code": d.item_code,
+ "purchase_receipt": d.parent,
+ "status": ("not in", ["Draft", "Scrapped", "Sold", "Capitalized"]),
+ },
+ ["name as asset", "asset_name", "company"],
+ as_dict=1,
+ )
+
+ if asset_details:
+ asset_details.update(d)
+ asset_details.update(get_consumed_asset_details(asset_details))
+ d.update(asset_details)
+
+ return frappe._dict(d)
+ return None
diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
index da05e930eab..c96681411f7 100644
--- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
+++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
@@ -10,12 +10,13 @@
"column_break_3",
"warehouse",
"section_break_6",
+ "purchase_receipt_item",
"stock_qty",
- "stock_uom",
"actual_qty",
"column_break_9",
"valuation_rate",
"amount",
+ "stock_uom",
"batch_and_serial_no_section",
"serial_and_batch_bundle",
"use_serial_batch_fields",
@@ -53,14 +54,14 @@
{
"fieldname": "section_break_6",
"fieldtype": "Section Break",
- "label": "Qty and Rate"
+ "label": "Purchase Details"
},
{
"columns": 1,
"fieldname": "stock_qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Qty",
+ "label": "Quantity",
"non_negative": 1
},
{
@@ -172,18 +173,26 @@
{
"fieldname": "column_break_mbuv",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "purchase_receipt_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Purchase Receipt Item"
}
],
+ "grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-06-26 17:06:22.564438",
+ "modified": "2025-03-05 12:46:01.074742",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Stock Item",
"owner": "Administrator",
"permissions": [],
- "sort_field": "modified",
+ "row_format": "Dynamic",
+ "sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
index 0f06cc7442e..c56f87b67fa 100644
--- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
+++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
@@ -23,6 +23,7 @@ class AssetCapitalizationStockItem(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
+ purchase_receipt_item: DF.Data | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.Text | None
stock_qty: DF.Float
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 3a15c7de4ba..0b7c9de467a 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -404,7 +404,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
} else {
- if (!doc.items.every((item) => item.qty == item.sco_qty)) {
+ if (!doc.items.every((item) => item.qty == item.subcontracted_quantity)) {
this.frm.add_custom_button(
__("Subcontracting Order"),
() => {
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 26c0101b49b..ee8ba35222f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -898,7 +898,7 @@ def is_po_fully_subcontracted(po_name):
query = (
frappe.qb.from_(table)
.select(table.name)
- .where((table.parent == po_name) & (table.qty != table.sco_qty))
+ .where((table.parent == po_name) & (table.qty != table.subcontracted_quantity))
)
return not query.run(as_dict=True)
@@ -945,7 +945,7 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
"material_request_item": "material_request_item",
},
"field_no_map": ["qty", "fg_item_qty", "amount"],
- "condition": lambda item: item.qty != item.sco_qty,
+ "condition": lambda item: item.qty != item.subcontracted_quantity,
},
},
target_doc,
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 5d8ce73e8ab..99ad609f8df 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -1076,9 +1076,9 @@ class TestPurchaseOrder(FrappeTestCase):
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
po.reload()
- self.assertEqual(po.items[0].sco_qty, 5)
- self.assertEqual(po.items[1].sco_qty, 0)
- self.assertEqual(po.items[2].sco_qty, 12.5)
+ self.assertEqual(po.items[0].subcontracted_quantity, 5)
+ self.assertEqual(po.items[1].subcontracted_quantity, 0)
+ self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
self.assertEqual(sco.items[0].amount, 2000)
@@ -1114,10 +1114,10 @@ class TestPurchaseOrder(FrappeTestCase):
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
po.reload()
- self.assertEqual(po.items[2].sco_qty, 25)
+ self.assertEqual(po.items[2].subcontracted_quantity, 25)
sco.cancel()
po.reload()
- self.assertEqual(po.items[2].sco_qty, 12.5)
+ self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
sco = make_subcontracting_order(po.name)
sco.save()
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 1724e3cc99c..894c705dc96 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -26,7 +26,7 @@
"quantity_and_rate",
"qty",
"stock_uom",
- "sco_qty",
+ "subcontracted_quantity",
"col_break2",
"uom",
"conversion_factor",
@@ -913,7 +913,7 @@
},
{
"allow_on_submit": 1,
- "fieldname": "sco_qty",
+ "fieldname": "subcontracted_quantity",
"fieldtype": "Float",
"label": "Subcontracted Quantity",
"no_copy": 1,
@@ -921,11 +921,12 @@
"read_only": 1
}
],
+ "grid_page_length": 50,
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-02-18 12:35:04.432636",
+ "modified": "2025-03-02 16:58:26.059601",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
@@ -933,6 +934,7 @@
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
+ "row_format": "Dynamic",
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py
index b80abda56c3..5e4ce19d340 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py
@@ -80,10 +80,10 @@ class PurchaseOrderItem(Document):
sales_order_item: DF.Data | None
sales_order_packed_item: DF.Data | None
schedule_date: DF.Date
- sco_qty: DF.Float
stock_qty: DF.Float
stock_uom: DF.Link
stock_uom_rate: DF.Currency
+ subcontracted_quantity: DF.Float
supplier_part_no: DF.Data | None
supplier_quotation: DF.Link | None
supplier_quotation_item: DF.Link | None
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
index 58657da9168..99b4c26ac8e 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
@@ -19,6 +19,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ on_change: (report) => {
+ report.set_filter_value("name", []);
+ report.refresh();
+ },
},
{
fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.get_today(),
+ on_change: (report) => {
+ report.set_filter_value("name", []);
+ report.refresh();
+ },
},
{
fieldname: "project",
@@ -38,13 +46,17 @@ frappe.query_reports["Purchase Order Analysis"] = {
{
fieldname: "name",
label: __("Purchase Order"),
- fieldtype: "Link",
+ fieldtype: "MultiSelectList",
width: "80",
options: "Purchase Order",
- get_query: () => {
- return {
- filters: { docstatus: 1 },
- };
+ get_data: function (txt) {
+ let filters = { docstatus: 1 };
+
+ const from_date = frappe.query_report.get_filter_value("from_date");
+ const to_date = frappe.query_report.get_filter_value("to_date");
+ if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
+
+ return frappe.db.get_link_options("Purchase Order", txt, filters);
},
},
{
@@ -52,9 +64,16 @@ frappe.query_reports["Purchase Order Analysis"] = {
label: __("Status"),
fieldtype: "MultiSelectList",
width: "80",
- options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"],
+ options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed", "Closed"],
get_data: function (txt) {
- let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"];
+ let status = [
+ "To Pay",
+ "To Bill",
+ "To Receive",
+ "To Receive and Bill",
+ "Completed",
+ "Closed",
+ ];
let options = [];
for (let option of status) {
options.push({
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
index f583ce3e6c8..b6bf1d9f8da 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
@@ -70,14 +70,16 @@ def get_data(filters):
po.company,
po_item.name,
)
- .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1))
+ .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1))
.groupby(po_item.name)
.orderby(po.transaction_date)
)
- for field in ("company", "name"):
- if filters.get(field):
- query = query.where(po[field] == filters.get(field))
+ if filters.get("company"):
+ query = query.where(po.company == filters.get("company"))
+
+ if filters.get("name"):
+ query = query.where(po.name.isin(filters.get("name")))
if filters.get("from_date") and filters.get("to_date"):
query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date")))
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index bb0a4070981..6ed55a0b55e 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -194,6 +194,14 @@ class AccountsController(TransactionBase):
self.set_incoming_rate()
self.init_internal_values()
+ # Need to set taxes based on taxes_and_charges template
+ # before calculating taxes and totals
+ if self.meta.get_field("taxes_and_charges"):
+ self.validate_enabled_taxes_and_charges()
+ self.validate_tax_account_company()
+
+ self.set_taxes_and_charges()
+
if self.meta.get_field("currency"):
self.calculate_taxes_and_totals()
@@ -204,10 +212,6 @@ class AccountsController(TransactionBase):
self.validate_all_documents_schedule()
- if self.meta.get_field("taxes_and_charges"):
- self.validate_enabled_taxes_and_charges()
- self.validate_tax_account_company()
-
self.validate_party()
self.validate_currency()
self.validate_party_account_currency()
@@ -252,8 +256,6 @@ class AccountsController(TransactionBase):
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
- self.set_taxes_and_charges()
-
if self.doctype == "Purchase Invoice":
self.calculate_paid_amount()
# apply tax withholding only if checked and applicable
@@ -821,11 +823,15 @@ class AccountsController(TransactionBase):
and item.get("use_serial_batch_fields")
)
):
- if fieldname == "batch_no" and not item.batch_no and not item.is_free_item:
- item.set("rate", ret.get("rate"))
- item.set("price_list_rate", ret.get("price_list_rate"))
item.set(fieldname, value)
+ if fieldname == "batch_no" and item.batch_no and not item.is_free_item:
+ if ret.get("rate"):
+ item.set("rate", ret.get("rate"))
+
+ if not item.get("price_list_rate") and ret.get("price_list_rate"):
+ item.set("price_list_rate", ret.get("price_list_rate"))
+
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
fieldname
):
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 9643110b76a..c2a36ac36d0 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -333,7 +333,7 @@ class BuyingController(SubcontractingController):
net_rate
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
- + flt(item.get("rate_difference_with_purchase_invoice"))
+ + flt(item.get("amount_difference_with_purchase_invoice"))
) / qty_in_stock_uom
else:
item.valuation_rate = 0.0
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index a8f9976c83b..e892c5d27e2 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -1234,7 +1234,7 @@ class StockController(AccountsController):
child_tab.item_code,
child_tab.qty,
)
- .where(parent_tab.docstatus < 2)
+ .where(parent_tab.docstatus == 1)
)
if self.doctype == "Purchase Invoice":
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index b6a7f715d22..a672360046c 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -104,18 +104,18 @@ class SubcontractingController(StockController):
)
if (
- self.doctype == "Subcontracting Order" and not item.sc_conversion_factor
+ self.doctype == "Subcontracting Order" and not item.subcontracting_conversion_factor
): # this condition will only be true if user has recently updated from develop branch
service_item_qty = frappe.get_value(
"Subcontracting Order Service Item",
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
fieldname=["qty"],
)
- item.sc_conversion_factor = service_item_qty / item.qty
+ item.subcontracting_conversion_factor = service_item_qty / item.qty
if self.doctype not in "Subcontracting Receipt" and item.qty > flt(
- get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item)
- / item.sc_conversion_factor,
+ get_pending_subcontracted_quantity(self.purchase_order).get(item.purchase_order_item)
+ / item.subcontracting_conversion_factor,
frappe.get_precision("Purchase Order Item", "qty"),
):
frappe.throw(
@@ -1132,10 +1132,14 @@ def get_item_details(items):
return item_details
-def get_pending_sco_qty(po_name):
+def get_pending_subcontracted_quantity(po_name):
table = frappe.qb.DocType("Purchase Order Item")
- query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name)
- return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)}
+ query = (
+ frappe.qb.from_(table)
+ .select(table.name, table.qty, table.subcontracted_quantity)
+ .where(table.parent == po_name)
+ )
+ return {item.name: item.qty - item.subcontracted_quantity for item in query.run(as_dict=True)}
@frappe.whitelist()
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
index 2c46f04af71..f959cbd0488 100644
--- a/erpnext/controllers/tests/test_accounts_controller.py
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -931,6 +931,35 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
+ @change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1})
+ def test_18_fetch_taxes_based_on_taxes_and_charges_template(self):
+ # Create a Sales Taxes and Charges Template
+ if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"):
+ doc = frappe.new_doc("Sales Taxes and Charges Template")
+ doc.company = self.company
+ doc.title = "_Test Tax"
+ doc.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "Sales Expenses - _TC",
+ "description": "Test taxes",
+ "rate": 9,
+ },
+ )
+ doc.insert()
+
+ # Create a Sales Invoice
+ sinv = frappe.new_doc("Sales Invoice")
+ sinv.customer = self.customer
+ sinv.company = self.company
+ sinv.currency = "INR"
+ sinv.taxes_and_charges = "_Test Tax - _TC"
+ sinv.append("items", {"item_code": "_Test Item", "qty": 1, "rate": 50})
+ sinv.insert()
+
+ self.assertEqual(sinv.total_taxes_and_charges, 4.5)
+
def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 2b23cca886e..21f301f2450 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
app_description = """ERP made simple"""
app_icon = "fa fa-th"
app_color = "#e74c3c"
-app_email = "info@erpnext.com"
+app_email = "hello@frappe.io"
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
@@ -484,7 +484,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
default_mail_footer = """
Sent via
-
+
ERPNext
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 5d13471f541..054b482b260 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1371,7 +1371,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
},
)
- def get_max_op_qty():
+ def get_max_operation_quantity():
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Job Card")
@@ -1387,7 +1387,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
)
return min([d.qty for d in query.run(as_dict=True)], default=0)
- def get_utilised_cc():
+ def get_utilised_corrective_cost():
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Stock Entry")
@@ -1417,15 +1417,15 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
)
)
):
- max_qty = get_max_op_qty() - work_order.produced_qty
- remaining_cc = work_order.corrective_operation_cost - get_utilised_cc()
+ max_qty = get_max_operation_quantity() - work_order.produced_qty
+ remaining_corrective_cost = work_order.corrective_operation_cost - get_utilised_corrective_cost()
stock_entry.append(
"additional_costs",
{
"expense_account": expense_account,
"description": "Corrective Operation Cost",
"has_corrective_cost": 1,
- "amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty),
+ "amount": remaining_corrective_cost / max_qty * flt(stock_entry.fg_completed_qty),
},
)
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
index 45ce95b6d58..c4fb6345c93 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -28,6 +28,8 @@ BOM_ITEM_FIELDS = [
"stock_uom",
"conversion_factor",
"do_not_explode",
+ "source_warehouse",
+ "allow_alternative_item",
]
@@ -291,7 +293,6 @@ class BOMCreator(Document):
"item": row.item_code,
"bom_type": "Production",
"quantity": row.qty,
- "allow_alternative_item": 1,
"bom_creator": self.name,
"bom_creator_item": bom_creator_item,
}
@@ -315,7 +316,6 @@ class BOMCreator(Document):
item_args.update(
{
"bom_no": bom_no,
- "allow_alternative_item": 1,
"allow_scrap_items": 1,
"include_item_in_manufacturing": 1,
}
@@ -428,6 +428,7 @@ def add_sub_assembly(**kwargs):
"do_not_explode": 1,
"is_expandable": 1,
"stock_uom": item_info.stock_uom,
+ "allow_alternative_item": kwargs.allow_alternative_item,
},
)
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
index 1726f898751..a6e67b956cf 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -15,6 +15,7 @@
"is_expandable",
"sourced_by_supplier",
"bom_created",
+ "allow_alternative_item",
"description_section",
"description",
"quantity_and_rate_section",
@@ -225,12 +226,18 @@
"label": "BOM Created",
"no_copy": 1,
"print_hide": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "allow_alternative_item",
+ "fieldtype": "Check",
+ "label": "Allow Alternative Item"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-06-03 18:45:24.339532",
+ "modified": "2025-02-19 13:25:15.732496",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator Item",
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
index e172f36224d..fdd3f77ae26 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
@@ -14,6 +14,7 @@ class BOMCreatorItem(Document):
if TYPE_CHECKING:
from frappe.types import DF
+ allow_alternative_item: DF.Check
amount: DF.Currency
base_amount: DF.Currency
base_rate: DF.Currency
diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py
index e511612d3a3..1c4082233e1 100644
--- a/erpnext/manufacturing/report/production_analytics/production_analytics.py
+++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py
@@ -4,7 +4,7 @@
import frappe
from frappe import _, scrub
-from frappe.utils import getdate
+from frappe.utils import getdate, today
from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get_period_date_ranges
@@ -30,11 +30,12 @@ def get_columns(filters):
def get_periodic_data(filters, entry):
periodic_data = {
- "All Work Orders": {},
"Not Started": {},
"Overdue": {},
"Pending": {},
"Completed": {},
+ "Closed": {},
+ "Stopped": {},
}
ranges = get_period_date_ranges(filters)
@@ -42,33 +43,24 @@ def get_periodic_data(filters, entry):
for from_date, end_date in ranges:
period = get_period(end_date, filters)
for d in entry:
- if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date):
- periodic_data = update_periodic_data(periodic_data, "All Work Orders", period)
- if d.status == "Completed":
- if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate(
- from_date
- ):
- periodic_data = update_periodic_data(periodic_data, "Completed", period)
- elif getdate(d.actual_start_date) < getdate(from_date):
- periodic_data = update_periodic_data(periodic_data, "Pending", period)
- elif getdate(d.planned_start_date) < getdate(from_date):
- periodic_data = update_periodic_data(periodic_data, "Overdue", period)
- else:
- periodic_data = update_periodic_data(periodic_data, "Not Started", period)
+ if getdate(from_date) <= getdate(d.creation) <= getdate(end_date) and d.status not in [
+ "Draft",
+ "Submitted",
+ "Completed",
+ "Cancelled",
+ ]:
+ if d.status in ["Not Started", "Closed", "Stopped"]:
+ periodic_data = update_periodic_data(periodic_data, d.status, period)
+ elif today() > getdate(d.planned_end_date):
+ periodic_data = update_periodic_data(periodic_data, "Overdue", period)
+ elif today() < getdate(d.planned_end_date):
+ periodic_data = update_periodic_data(periodic_data, "Pending", period)
- elif d.status == "In Process":
- if getdate(d.actual_start_date) < getdate(from_date):
- periodic_data = update_periodic_data(periodic_data, "Pending", period)
- elif getdate(d.planned_start_date) < getdate(from_date):
- periodic_data = update_periodic_data(periodic_data, "Overdue", period)
- else:
- periodic_data = update_periodic_data(periodic_data, "Not Started", period)
-
- elif d.status == "Not Started":
- if getdate(d.planned_start_date) < getdate(from_date):
- periodic_data = update_periodic_data(periodic_data, "Overdue", period)
- else:
- periodic_data = update_periodic_data(periodic_data, "Not Started", period)
+ if (
+ getdate(from_date) <= getdate(d.actual_end_date) <= getdate(end_date)
+ and d.status == "Completed"
+ ):
+ periodic_data = update_periodic_data(periodic_data, "Completed", period)
return periodic_data
@@ -88,10 +80,7 @@ def get_data(filters, columns):
"Work Order",
fields=[
"creation",
- "modified",
- "actual_start_date",
"actual_end_date",
- "planned_start_date",
"planned_end_date",
"status",
],
@@ -100,7 +89,7 @@ def get_data(filters, columns):
periodic_data = get_periodic_data(filters, entry)
- labels = ["All Work Orders", "Not Started", "Overdue", "Pending", "Completed"]
+ labels = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"]
chart_data = get_chart_data(periodic_data, columns)
ranges = get_period_date_ranges(filters)
@@ -121,21 +110,23 @@ def get_data(filters, columns):
def get_chart_data(periodic_data, columns):
labels = [d.get("label") for d in columns[1:]]
- all_data, not_start, overdue, pending, completed = [], [], [], [], []
+ not_start, overdue, pending, completed, closed, stopped = [], [], [], [], [], []
datasets = []
for d in labels:
- all_data.append(periodic_data.get("All Work Orders").get(d))
not_start.append(periodic_data.get("Not Started").get(d))
overdue.append(periodic_data.get("Overdue").get(d))
pending.append(periodic_data.get("Pending").get(d))
completed.append(periodic_data.get("Completed").get(d))
+ closed.append(periodic_data.get("Closed").get(d))
+ stopped.append(periodic_data.get("Stopped").get(d))
- datasets.append({"name": _("All Work Orders"), "values": all_data})
datasets.append({"name": _("Not Started"), "values": not_start})
datasets.append({"name": _("Overdue"), "values": overdue})
datasets.append({"name": _("Pending"), "values": pending})
datasets.append({"name": _("Completed"), "values": completed})
+ datasets.append({"name": _("Closed"), "values": closed})
+ datasets.append({"name": _("Stopped"), "values": stopped})
chart = {"data": {"labels": labels, "datasets": datasets}}
chart["type"] = "line"
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index cb10fcce1c4..505eb81e4e9 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -261,6 +261,7 @@ erpnext.patches.v14_0.show_loan_management_deprecation_warning
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
erpnext.patches.v14_0.update_proprietorship_to_individual
+erpnext.patches.v15_0.rename_subcontracting_fields
[post_model_sync]
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
@@ -394,3 +395,9 @@ execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_post
erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
erpnext.patches.v14_0.update_posting_datetime
+erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
+erpnext.patches.v15_0.rename_sla_fields
+erpnext.patches.v15_0.update_query_report
+erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
+erpnext.patches.v15_0.recalculate_amount_difference_field
+erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
index ff77fbb91ec..d4350d8f9a1 100644
--- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -82,6 +82,9 @@ def get_asset_depreciation_schedules_map():
.orderby(ds.idx)
).run(as_dict=True)
+ if len(records) > 20000:
+ frappe.db.auto_commit_on_many_writes = True
+
asset_depreciation_schedules_map = frappe._dict()
for d in records:
asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d)
diff --git a/erpnext/patches/v15_0/recalculate_amount_difference_field.py b/erpnext/patches/v15_0/recalculate_amount_difference_field.py
new file mode 100644
index 00000000000..fa45211be94
--- /dev/null
+++ b/erpnext/patches/v15_0/recalculate_amount_difference_field.py
@@ -0,0 +1,80 @@
+import frappe
+from frappe.query_builder.functions import Sum
+from frappe.utils import flt
+
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.stock.doctype.purchase_receipt.purchase_receipt import adjust_incoming_rate_for_pr
+
+
+def execute():
+ table = frappe.qb.DocType("Purchase Receipt Item")
+ parent = frappe.qb.DocType("Purchase Receipt")
+ query = (
+ frappe.qb.from_(table)
+ .join(parent)
+ .on(table.parent == parent.name)
+ .select(
+ table.parent,
+ table.name,
+ table.amount,
+ table.billed_amt,
+ table.amount_difference_with_purchase_invoice,
+ table.rate,
+ table.qty,
+ parent.conversion_rate,
+ )
+ .where((table.amount_difference_with_purchase_invoice != 0) & (table.docstatus == 1))
+ )
+ try:
+ if fiscal_year_dates := get_fiscal_year(frappe.utils.datetime.date.today()):
+ query.where(parent.posting_date.between(fiscal_year_dates[1], fiscal_year_dates[2]))
+ except Exception:
+ return
+
+ if result := query.run(as_dict=True):
+ item_wise_billed_qty = get_billed_qty_against_purchase_receipt([item.name for item in result])
+
+ purchase_receipts = set()
+ precision = frappe.get_precision("Purchase Receipt Item", "amount")
+ for item in result:
+ adjusted_amt = 0.0
+
+ if (
+ item.billed_amt is not None
+ and item.amount is not None
+ and item_wise_billed_qty.get(item.name)
+ ):
+ adjusted_amt = (
+ flt(item.billed_amt / item_wise_billed_qty.get(item.name)) - flt(item.rate)
+ ) * item.qty
+ adjusted_amt = flt(
+ adjusted_amt * flt(item.conversion_rate),
+ precision,
+ )
+
+ if adjusted_amt != item.amount_difference_with_purchase_invoice:
+ frappe.db.set_value(
+ "Purchase Receipt Item",
+ item.name,
+ "amount_difference_with_purchase_invoice",
+ adjusted_amt,
+ update_modified=False,
+ )
+ purchase_receipts.add(item.parent)
+
+ for pr in purchase_receipts:
+ adjust_incoming_rate_for_pr(frappe.get_doc("Purchase Receipt", pr))
+
+
+def get_billed_qty_against_purchase_receipt(pr_names):
+ table = frappe.qb.DocType("Purchase Invoice Item")
+ query = (
+ frappe.qb.from_(table)
+ .select(table.pr_detail, Sum(table.qty).as_("qty"))
+ .where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
+ )
+ invoice_data = query.run(as_list=1)
+
+ if not invoice_data:
+ return frappe._dict()
+ return frappe._dict(invoice_data)
diff --git a/erpnext/patches/v15_0/rename_field_from_rate_difference_to_amount_difference.py b/erpnext/patches/v15_0/rename_field_from_rate_difference_to_amount_difference.py
new file mode 100644
index 00000000000..44c8c49cba8
--- /dev/null
+++ b/erpnext/patches/v15_0/rename_field_from_rate_difference_to_amount_difference.py
@@ -0,0 +1,17 @@
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ frappe.db.set_value(
+ "DocField",
+ {"parent": "Purchase Receipt Item", "fieldname": "rate_difference_with_purchase_invoice"},
+ "label",
+ "Amount Difference with Purchase Invoice",
+ )
+ rename_field(
+ "Purchase Receipt Item",
+ "rate_difference_with_purchase_invoice",
+ "amount_difference_with_purchase_invoice",
+ )
+ frappe.clear_cache(doctype="Purchase Receipt Item")
diff --git a/erpnext/patches/v15_0/rename_sla_fields.py b/erpnext/patches/v15_0/rename_sla_fields.py
new file mode 100644
index 00000000000..5e3e92d85c2
--- /dev/null
+++ b/erpnext/patches/v15_0/rename_sla_fields.py
@@ -0,0 +1,13 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import rename_fieldname
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ doctypes = frappe.get_all("Service Level Agreement", pluck="document_type")
+ for doctype in doctypes:
+ rename_fieldname(doctype + "-resolution_by", "sla_resolution_by")
+ rename_fieldname(doctype + "-resolution_date", "sla_resolution_date")
+
+ rename_field("Issue", "resolution_by", "sla_resolution_by")
+ rename_field("Issue", "resolution_date", "sla_resolution_date")
diff --git a/erpnext/patches/v15_0/rename_subcontracting_fields.py b/erpnext/patches/v15_0/rename_subcontracting_fields.py
new file mode 100644
index 00000000000..d18d6149cac
--- /dev/null
+++ b/erpnext/patches/v15_0/rename_subcontracting_fields.py
@@ -0,0 +1,7 @@
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ rename_field("Purchase Order Item", "sco_qty", "subcontracted_quantity")
+ rename_field("Subcontracting Order Item", "sc_conversion_factor", "subcontracting_conversion_factor")
diff --git a/erpnext/patches/v15_0/set_purchase_receipt_row_item_to_capitalization_stock_item.py b/erpnext/patches/v15_0/set_purchase_receipt_row_item_to_capitalization_stock_item.py
new file mode 100644
index 00000000000..f1d17d1da4d
--- /dev/null
+++ b/erpnext/patches/v15_0/set_purchase_receipt_row_item_to_capitalization_stock_item.py
@@ -0,0 +1,21 @@
+import frappe
+
+
+def execute():
+ # nosemgrep
+ frappe.db.sql(
+ """
+ UPDATE `tabAsset Capitalization Stock Item` ACSI
+ JOIN `tabAsset Capitalization` AC
+ ON ACSI.parent = AC.name
+ JOIN `tabPurchase Receipt Item` PRI
+ ON
+ PRI.item_code = ACSI.item_code
+ AND PRI.wip_composite_asset = AC.target_asset
+ SET
+ ACSI.purchase_receipt_item = PRI.name
+ WHERE
+ ACSI.purchase_receipt_item IS NULL
+ AND AC.docstatus = 1
+ """
+ )
diff --git a/erpnext/patches/v15_0/update_query_report.py b/erpnext/patches/v15_0/update_query_report.py
new file mode 100644
index 00000000000..0efdf8af2c3
--- /dev/null
+++ b/erpnext/patches/v15_0/update_query_report.py
@@ -0,0 +1,25 @@
+import frappe
+
+
+def execute():
+ reports = [
+ "Accounts Payable",
+ "Accounts Payable Summary",
+ "Purchase Register",
+ "Item-wise Purchase Register",
+ "Purchase Order Analysis",
+ "Received Items To Be Billed",
+ "Supplier Ledger Summary",
+ ]
+ frappe.db.set_value(
+ "Workspace Link",
+ {
+ "parent": "Payables",
+ "link_type": "Report",
+ "type": "Link",
+ "link_to": ["in", reports],
+ "is_query_report": 0,
+ },
+ "is_query_report",
+ 1,
+ )
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 4ed8ffc0077..f7653d92d63 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -322,17 +322,31 @@ class Project(Document):
self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0
def update_billed_amount(self):
- # nosemgrep
+ self.total_billed_amount = self.get_billed_amount_from_parent() + self.get_billed_amount_from_child()
+
+ def get_billed_amount_from_parent(self):
total_billed_amount = frappe.db.sql(
"""select sum(base_net_amount)
- from `tabSales Invoice Item` si_item, `tabSales Invoice` si
- where si_item.parent = si.name
- and if(si_item.project, si_item.project, si.project) = %s
- and si.docstatus=1""",
+ from `tabSales Invoice` si join `tabSales Invoice Item` si_item on si_item.parent = si.name
+ where si_item.project is null
+ and si.project is not null
+ and si.project = %s
+ and si.docstatus = 1""",
self.name,
)
- self.total_billed_amount = total_billed_amount and total_billed_amount[0][0] or 0
+ return total_billed_amount and total_billed_amount[0][0] or 0
+
+ def get_billed_amount_from_child(self):
+ total_billed_amount = frappe.db.sql(
+ """select sum(base_net_amount)
+ from `tabSales Invoice Item`
+ where project = %s
+ and docstatus = 1""",
+ self.name,
+ )
+
+ return total_billed_amount and total_billed_amount[0][0] or 0
def after_rename(self, old_name, new_name, merge=False):
if old_name == self.copied_from:
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
index b1019f67ca9..6d24751792c 100644
--- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -210,6 +210,13 @@ class BOMConfigurator {
[
{ label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 },
{ label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 },
+ {
+ label: __("Allow Alternative Item"),
+ fieldname: "allow_alternative_item",
+ default: 1.0,
+ fieldtype: "Check",
+ reqd: 1,
+ },
],
(data) => {
if (!node.data.parent_id) {
@@ -224,6 +231,7 @@ class BOMConfigurator {
item_code: data.item_code,
fg_reference_id: node.data.name || this.frm.doc.name,
qty: data.qty,
+ allow_alternative_item: data.allow_alternative_item,
},
callback: (r) => {
view.events.load_tree(r, node);
@@ -258,6 +266,7 @@ class BOMConfigurator {
fg_item: node.data.value,
fg_reference_id: node.data.name || this.frm.doc.name,
bom_item: bom_item,
+ allow_alternative_item: bom_item.allow_alternative_item,
},
callback: (r) => {
view.events.load_tree(r, node);
@@ -278,6 +287,14 @@ class BOMConfigurator {
reqd: 1,
read_only: read_only,
},
+ {
+ label: __("Allow Alternative Item"),
+ fieldname: "allow_alternative_item",
+ default: 1.0,
+ fieldtype: "Check",
+ reqd: 1,
+ read_only: read_only,
+ },
{ fieldtype: "Column Break" },
{
label: __("Qty"),
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 69ecac19852..a98cb450eb3 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1857,7 +1857,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
const exist_items = items.map(row => { return {item_code: row.item_code, pricing_rules: row.pricing_rules};});
- args.free_item_data.forEach(pr_row => {
+ args.free_item_data.forEach(async pr_row => {
let row_to_modify = {};
// If there are no free items, or if the current free item doesn't exist in the table, add it
@@ -1875,6 +1875,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
for (let key in pr_row) {
row_to_modify[key] = pr_row[key];
}
+
+ if (this.frm.doc.hasOwnProperty("is_pos") && this.frm.doc.is_pos) {
+ let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "cost_center");
+ if (r.message.cost_center) {
+ row_to_modify["cost_center"] = r.message.cost_center;
+ }
+ }
+
this.frm.script_manager.copy_from_first_row("items", row_to_modify, ["expense_account", "income_account"]);
});
diff --git a/erpnext/public/js/utils/landed_taxes_and_charges_common.js b/erpnext/public/js/utils/landed_taxes_and_charges_common.js
index 2cb30160453..7d801ca91e6 100644
--- a/erpnext/public/js/utils/landed_taxes_and_charges_common.js
+++ b/erpnext/public/js/utils/landed_taxes_and_charges_common.js
@@ -14,6 +14,10 @@ erpnext.landed_cost_taxes_and_charges = {
"Income Account",
"Expenses Included In Valuation",
"Expenses Included In Asset Valuation",
+ "Expense Account",
+ "Direct Expense",
+ "Indirect Expense",
+ "Stock Received But Not Billed",
],
],
company: frm.doc.company,
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index de1faf36ef5..e02d7a3d785 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -540,6 +540,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
has_batch_no: this.item.has_batch_no,
qty: qty,
based_on: based_on,
+ posting_date: this.frm.doc.posting_date,
+ posting_time: this.frm.doc.posting_time,
},
callback: (r) => {
if (r.message) {
diff --git a/erpnext/regional/address_template/templates/united_states.html b/erpnext/regional/address_template/templates/united_states.html
index 77fce46b9d7..f00f99c1299 100644
--- a/erpnext/regional/address_template/templates/united_states.html
+++ b/erpnext/regional/address_template/templates/united_states.html
@@ -1,4 +1,4 @@
{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}
-{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}
{% endif -%}
+{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}
{% if country != "United States" %}{{ country }}{% endif -%}
diff --git a/erpnext/regional/italy/e-invoice.xml b/erpnext/regional/italy/e-invoice.xml
index 69b8e3e488d..7c436a2b449 100644
--- a/erpnext/regional/italy/e-invoice.xml
+++ b/erpnext/regional/italy/e-invoice.xml
@@ -188,9 +188,9 @@
{{ html2text(item.description or '') or item.item_name }}
{{ format_float(item.qty) }}
{{ item.stock_uom }}
- {{ format_float(item.price_list_rate or item.rate, item_meta.get_field("rate").precision) }}
+ {{ format_float(item.net_rate or item.price_list_rate or item.rate, item_meta.get_field("rate").precision) }}
{{ render_discount_or_margin(item) }}
- {{ format_float(item.amount, item_meta.get_field("amount").precision) }}
+ {{ format_float(item.net_amount, item_meta.get_field("amount").precision) }}
{{ format_float(item.tax_rate, item_meta.get_field("tax_rate").precision) }}
{%- if item.tax_exemption_reason %}
{{ item.tax_exemption_reason.split("-")[0] }}
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index effc3f3894d..74e2328be24 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -1044,7 +1044,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
ignore_permissions=True,
)
- dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1))
+ dn_item.qty = flt(sre.reserved_qty) / flt(dn_item.get("conversion_factor", 1))
if sre.reservation_based_on == "Serial and Batch" and (sre.has_serial_no or sre.has_batch_no):
dn_item.serial_and_batch_bundle = get_ssb_bundle_for_voucher(sre)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 47d42b0a9d5..003ffd5ac82 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -2097,6 +2097,45 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
+ def test_delivery_note_rate_on_change_of_warehouse(self):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ item = make_item(
+ "_Test Batch Item for Delivery Note Rate",
+ {
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BH-SDDTBIFRM-.#####",
+ },
+ )
+
+ frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
+ so = make_sales_order(
+ item_code=item.name, rate=27648.00, price_list_rate=27648.00, qty=1, do_not_submit=True
+ )
+
+ so.items[0].rate = 90
+ so.save()
+ self.assertTrue(so.items[0].discount_amount == 27558.0)
+ so.submit()
+
+ warehouse = create_warehouse("NW Warehouse FOR Rate", company=so.company)
+
+ make_stock_entry(
+ item_code=item.name,
+ qty=2,
+ target=warehouse,
+ basic_rate=100,
+ company=so.company,
+ use_serial_batch_fields=1,
+ )
+
+ dn = make_delivery_note(so.name)
+ dn.items[0].warehouse = warehouse
+ dn.save()
+
+ self.assertEqual(dn.items[0].rate, 90)
+
def test_credit_limit_on_so_reopning(self):
# set credit limit
company = "_Test Company"
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index b86a87983d5..9be5a656a8e 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -115,6 +115,14 @@ def filter_result_items(result, pos_profile):
result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups]
+@frappe.whitelist()
+def get_parent_item_group():
+ # Using get_all to ignore user permission
+ item_group = frappe.get_all("Item Group", {"lft": 1, "is_group": 1}, pluck="name")
+ if item_group:
+ return item_group[0]
+
+
@frappe.whitelist()
def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""):
warehouse, hide_unavailable_items = frappe.db.get_value(
@@ -320,13 +328,13 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = []
if search_term and status:
- invoices_by_customer = frappe.db.get_all(
+ invoices_by_customer = frappe.db.get_list(
"POS Invoice",
filters={"customer": ["like", f"%{search_term}%"], "status": status},
fields=fields,
page_length=limit,
)
- invoices_by_name = frappe.db.get_all(
+ invoices_by_name = frappe.db.get_list(
"POS Invoice",
filters={"name": ["like", f"%{search_term}%"], "status": status},
fields=fields,
@@ -335,7 +343,7 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = invoices_by_customer + invoices_by_name
elif status:
- invoice_list = frappe.db.get_all(
+ invoice_list = frappe.db.get_list(
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
)
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index bfde624f7b3..e7208c41dde 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -605,6 +605,14 @@ erpnext.PointOfSale.Controller = class {
if (this.is_current_item_being_edited(item_row) || from_selector) {
await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
+ if (item.serial_no && from_selector) {
+ await frappe.model.set_value(
+ item_row.doctype,
+ item_row.name,
+ "serial_no",
+ item_row.serial_no + `\n${item.serial_no}`
+ );
+ }
this.update_cart_html(item_row);
}
} else {
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index 333b50810c9..a0476ee6bda 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -187,6 +187,7 @@ erpnext.PointOfSale.ItemDetails = class {
this[`${fieldname}_control`].set_value(item[fieldname]);
});
+ this.resize_serial_control(item);
this.make_auto_serial_selection_btn(item);
this.bind_custom_control_change_event();
@@ -203,28 +204,27 @@ erpnext.PointOfSale.ItemDetails = class {
"actual_qty",
"price_list_rate",
];
- if (item.has_serial_no) fields.push("serial_no");
- if (item.has_batch_no) fields.push("batch_no");
+ if (item.has_serial_no || item.serial_no) fields.push("serial_no");
+ if (item.has_batch_no || item.batch_no) fields.push("batch_no");
return fields;
}
+ resize_serial_control(item) {
+ if (item.has_serial_no || item.serial_no) {
+ this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem");
+ }
+ }
+
make_auto_serial_selection_btn(item) {
- if (item.has_serial_no || item.has_batch_no) {
- if (item.has_serial_no && item.has_batch_no) {
- this.$form_container.append(
- `${__(
- "Select Serial No / Batch No"
- )}
`
- );
- } else {
- const classname = item.has_serial_no ? ".serial_no-control" : ".batch_no-control";
- const label = item.has_serial_no ? __("Select Serial No") : __("Select Batch No");
- this.$form_container
- .find(classname)
- .append(
- `${label}
`
- );
+ const doc = this.events.get_frm().doc;
+ if (!doc.is_return && (item.has_serial_no || item.serial_no)) {
+ if (!item.has_batch_no) {
+ this.$form_container.append(``);
}
+ const label = __("Auto Fetch Serial Numbers");
+ this.$form_container.append(
+ `${label}
`
+ );
this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem");
}
}
@@ -410,18 +410,41 @@ erpnext.PointOfSale.ItemDetails = class {
bind_auto_serial_fetch_event() {
this.$form_container.on("click", ".auto-fetch-btn", () => {
- let frm = this.events.get_frm();
- let item_row = this.item_row;
- item_row.type_of_transaction = "Outward";
+ this.batch_no_control && this.batch_no_control.set_value("");
+ let qty = this.qty_control.get_value();
+ let conversion_factor = this.conversion_factor_control.get_value();
+ let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : "";
- new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
- if (r) {
- frappe.model.set_value(item_row.doctype, item_row.name, {
- serial_and_batch_bundle: r.name,
- qty: Math.abs(r.total_qty),
- use_serial_batch_fields: 0,
- });
+ let numbers = frappe.call({
+ method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
+ args: {
+ qty: qty * conversion_factor,
+ item_code: this.current_item.item_code,
+ warehouse: this.warehouse_control.get_value() || "",
+ batch_nos: this.current_item.batch_no || "",
+ posting_date: expiry_date,
+ for_doctype: "POS Invoice",
+ },
+ });
+
+ numbers.then((data) => {
+ let auto_fetched_serial_numbers = data.message;
+ let records_length = auto_fetched_serial_numbers.length;
+ if (!records_length) {
+ const warehouse = this.warehouse_control.get_value().bold();
+ const item_code = this.current_item.item_code.bold();
+ frappe.msgprint(
+ __(
+ "Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.",
+ [item_code, warehouse]
+ )
+ );
+ } else if (records_length < qty) {
+ frappe.msgprint(__("Fetched only {0} available serial numbers.", [records_length]));
+ this.qty_control.set_value(records_length);
}
+ numbers = auto_fetched_serial_numbers.join(`\n`);
+ this.serial_no_control.set_value(numbers);
});
});
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 909d1bb9c2d..6ae0d675140 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -38,8 +38,13 @@ erpnext.PointOfSale.ItemSelector = class {
async load_items_data() {
if (!this.item_group) {
- const res = await frappe.db.get_value("Item Group", { lft: 1, is_group: 1 }, "name");
- this.parent_item_group = res.message.name;
+ frappe.call({
+ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_parent_item_group",
+ async: false,
+ callback: (r) => {
+ if (r.message) this.parent_item_group = r.message;
+ },
+ });
}
if (!this.price_list) {
const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list");
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index fc8b75031c8..b47c25e20bb 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -340,19 +340,11 @@ erpnext.PointOfSale.Payment = class {
// pass
}
- async render_payment_section() {
+ render_payment_section() {
this.render_payment_mode_dom();
this.make_invoice_fields_control();
this.update_totals_section();
- let r = await frappe.db.get_value(
- "POS Profile",
- this.frm.doc.pos_profile,
- "disable_grand_total_to_default_mop"
- );
-
- if (!r.message.disable_grand_total_to_default_mop) {
- this.focus_on_default_mop();
- }
+ this.unset_grand_total_to_default_mop();
}
after_render() {
@@ -637,6 +629,19 @@ erpnext.PointOfSale.Payment = class {
.toLowerCase();
}
+ async unset_grand_total_to_default_mop() {
+ const doc = this.events.get_frm().doc;
+ let r = await frappe.db.get_value(
+ "POS Profile",
+ doc.pos_profile,
+ "disable_grand_total_to_default_mop"
+ );
+
+ if (!r.message.disable_grand_total_to_default_mop) {
+ this.focus_on_default_mop();
+ }
+ }
+
validate_reqd_invoice_fields() {
const doc = this.events.get_frm().doc;
let validation_flag = true;
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
index 5866fcbc845..b7f7a34c1b8 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
@@ -19,6 +19,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ on_change: (report) => {
+ report.set_filter_value("sales_order", []);
+ report.refresh();
+ },
},
{
fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.get_today(),
+ on_change: (report) => {
+ report.set_filter_value("sales_order", []);
+ report.refresh();
+ },
},
{
fieldname: "sales_order",
@@ -35,12 +43,13 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
options: "Sales Order",
get_data: function (txt) {
- return frappe.db.get_link_options("Sales Order", txt);
- },
- get_query: () => {
- return {
- filters: { docstatus: 1 },
- };
+ let filters = { docstatus: 1 };
+
+ const from_date = frappe.query_report.get_filter_value("from_date");
+ const to_date = frappe.query_report.get_filter_value("to_date");
+ if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
+
+ return frappe.db.get_link_options("Sales Order", txt, filters);
},
},
{
@@ -53,10 +62,17 @@ frappe.query_reports["Sales Order Analysis"] = {
fieldname: "status",
label: __("Status"),
fieldtype: "MultiSelectList",
- options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"],
+ options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed", "Closed"],
width: "80",
get_data: function (txt) {
- let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"];
+ let status = [
+ "To Pay",
+ "To Bill",
+ "To Deliver",
+ "To Deliver and Bill",
+ "Completed",
+ "Closed",
+ ];
let options = [];
for (let option of status) {
options.push({
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index 8fcf29bd7a6..90c33c323ce 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -86,7 +86,7 @@ def get_data(conditions, filters):
ON sii.so_detail = soi.name and sii.docstatus = 1
WHERE
soi.parent = so.name
- and so.status not in ('Stopped', 'Closed', 'On Hold')
+ and so.status not in ('Stopped', 'On Hold')
and so.docstatus = 1
{conditions}
GROUP BY soi.name
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 97ec418d955..23ffd49de5d 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -16,7 +16,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from .default_success_action import get_default_success_action
default_mail_footer = """"""
+ ERPNext"""
def after_install():
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 638c8af6263..e41f27b4d8c 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -68,9 +68,6 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if not transaction_date:
transaction_date = nowdate()
- if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
- return rate
-
currency_settings = frappe.get_doc("Accounts Settings").as_dict()
allow_stale_rates = currency_settings.get("allow_stale")
@@ -100,6 +97,9 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"):
return 0.00
+ if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
+ return rate
+
try:
cache = frappe.cache()
key = f"currency_exchange_rate_{transaction_date}:{from_currency}:{to_currency}"
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 518782d759b..6a6bb226a9e 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on("Pick List", {
+ after_save(frm) {
+ setTimeout(() => {
+ // Added to fix the issue of locations table not getting updated after save
+ frm.reload_doc();
+ }, 500);
+ },
+
setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 4550d71677a..998f15945d3 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -73,6 +73,7 @@ class PickList(Document):
self.set_onload("has_reserved_stock", True)
def validate(self):
+ self.validate_expired_batches()
self.validate_for_qty()
self.validate_stock_qty()
self.check_serial_no_status()
@@ -205,6 +206,33 @@ class PickList(Document):
self.update_reference_qty()
self.update_sales_order_picking_status()
+ def validate_expired_batches(self):
+ batches = []
+ for row in self.get("locations"):
+ if row.get("batch_no") and row.get("picked_qty"):
+ batches.append(row.batch_no)
+
+ if batches:
+ batch = frappe.qb.DocType("Batch")
+ query = (
+ frappe.qb.from_(batch)
+ .select(batch.name)
+ .where(
+ (batch.name.isin(batches))
+ & (batch.expiry_date <= frappe.utils.nowdate())
+ & (batch.expiry_date.isnotnull())
+ )
+ )
+
+ expired_batches = query.run(as_dict=True)
+ if expired_batches:
+ msg = "" + "".join(f"- {batch.name}
" for batch in expired_batches) + "
"
+
+ frappe.throw(
+ _("The following batches are expired, please restock them:
{0}").format(msg),
+ title=_("Expired Batches"),
+ )
+
def make_bundle_using_old_serial_batch_fields(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -447,6 +475,7 @@ class PickList(Document):
self.remove(row)
updated_locations = frappe._dict()
+ len_idx = len(self.get("locations")) or 0
for item_doc in items:
item_code = item_doc.item_code
@@ -489,6 +518,8 @@ class PickList(Document):
if location.picked_qty > location.stock_qty:
location.picked_qty = location.stock_qty
+ len_idx += 1
+ location.idx = len_idx
self.append("locations", location)
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
@@ -497,7 +528,11 @@ class PickList(Document):
for location in locations_replica:
location.stock_qty = 0
location.picked_qty = 0
+
+ len_idx += 1
+ location.idx = len_idx
self.append("locations", location)
+
frappe.msgprint(
_(
"Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List."
@@ -638,8 +673,31 @@ class PickList(Document):
if serial_no:
picked_items[item_data.item_code][key]["serial_no"].extend(serial_no)
+ self.update_picked_item_from_current_pick_list(picked_items)
return picked_items
+ def update_picked_item_from_current_pick_list(self, picked_items):
+ for row in self.locations:
+ if flt(row.picked_qty) > 0:
+ key = (row.warehouse, row.batch_no) if row.batch_no else row.warehouse
+ serial_no = [x for x in row.serial_no.split("\n") if x] if row.serial_no else None
+ if row.item_code not in picked_items:
+ picked_items[row.item_code] = {}
+
+ if key not in picked_items[row.item_code]:
+ picked_items[row.item_code][key] = frappe._dict(
+ {
+ "picked_qty": 0,
+ "serial_no": [],
+ "batch_no": row.batch_no or "",
+ "warehouse": row.warehouse,
+ }
+ )
+
+ picked_items[row.item_code][key]["picked_qty"] += flt(row.stock_qty) or flt(row.picked_qty)
+ if serial_no:
+ picked_items[row.item_code][key]["serial_no"].extend(serial_no)
+
def _get_pick_list_items(self, items):
pi = frappe.qb.DocType("Pick List")
pi_item = frappe.qb.DocType("Pick List Item")
@@ -653,9 +711,11 @@ class PickList(Document):
pi_item.batch_no,
pi_item.serial_and_batch_bundle,
pi_item.serial_no,
- (Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
- "picked_qty"
- ),
+ (
+ Case()
+ .when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty)
+ .else_(pi_item.stock_qty)
+ ).as_("picked_qty"),
)
.where(
(pi_item.item_code.isin([x.item_code for x in items]))
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 0328c447ec2..7f2c04316e9 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -424,6 +424,14 @@ class PurchaseReceipt(BuyingController):
self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order()
+ def before_cancel(self):
+ super().before_cancel()
+ self.remove_amount_difference_with_purchase_invoice()
+
+ def remove_amount_difference_with_purchase_invoice(self):
+ for item in self.items:
+ item.amount_difference_with_purchase_invoice = 0
+
def get_gl_entries(self, warehouse_account=None, via_landed_cost_voucher=False):
from erpnext.accounts.general_ledger import process_gl_map
@@ -571,15 +579,15 @@ class PurchaseReceipt(BuyingController):
item=item,
)
- def make_rate_difference_entry(item):
- if item.rate_difference_with_purchase_invoice and stock_asset_rbnb:
+ def make_amount_difference_entry(item):
+ if item.amount_difference_with_purchase_invoice and stock_asset_rbnb:
account_currency = get_account_currency(stock_asset_rbnb)
self.add_gl_entry(
gl_entries=gl_entries,
account=stock_asset_rbnb,
cost_center=item.cost_center,
debit=0.0,
- credit=flt(item.rate_difference_with_purchase_invoice),
+ credit=flt(item.amount_difference_with_purchase_invoice),
remarks=_("Adjustment based on Purchase Invoice rate"),
against_account=stock_asset_account_name,
account_currency=account_currency,
@@ -612,7 +620,7 @@ class PurchaseReceipt(BuyingController):
+ flt(item.landed_cost_voucher_amount)
+ flt(item.rm_supp_cost)
+ flt(item.item_tax_amount)
- + flt(item.rate_difference_with_purchase_invoice)
+ + flt(item.amount_difference_with_purchase_invoice)
)
divisional_loss = flt(
@@ -712,7 +720,7 @@ class PurchaseReceipt(BuyingController):
make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
outgoing_amount = make_stock_received_but_not_billed_entry(d)
make_landed_cost_gl_entries(d)
- make_rate_difference_entry(d)
+ make_amount_difference_entry(d)
make_sub_contracting_gl_entries(d)
make_divisional_loss_gl_entry(d, outgoing_amount)
elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
@@ -1048,15 +1056,19 @@ def get_billed_amount_against_po(po_items):
if not po_items:
return {}
+ purchase_invoice = frappe.qb.DocType("Purchase Invoice")
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
+ .inner_join(purchase_invoice)
+ .on(purchase_invoice_item.parent == purchase_invoice.name)
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
.where(
(purchase_invoice_item.po_detail.isin(po_items))
- & (purchase_invoice_item.docstatus == 1)
+ & (purchase_invoice.docstatus == 1)
& (purchase_invoice_item.pr_detail.isnull())
+ & (purchase_invoice.update_stock == 0)
)
.groupby(purchase_invoice_item.po_detail)
).run(as_dict=1)
@@ -1090,11 +1102,19 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
if adjust_incoming_rate:
adjusted_amt = 0.0
- if item.billed_amt is not None and item.amount is not None:
- adjusted_amt = flt(item.billed_amt) - flt(item.amount)
+ item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc)
- adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate)
- item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
+ if (
+ item.billed_amt is not None
+ and item.amount is not None
+ and item_wise_billed_qty.get(item.name)
+ ):
+ adjusted_amt = (
+ flt(item.billed_amt / item_wise_billed_qty.get(item.name)) - flt(item.rate)
+ ) * item.qty
+
+ adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
+ item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)
@@ -1107,6 +1127,21 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
adjust_incoming_rate_for_pr(pr_doc)
+def get_billed_qty_against_purchase_receipt(pr_doc):
+ pr_names = [d.name for d in pr_doc.items]
+ table = frappe.qb.DocType("Purchase Invoice Item")
+ query = (
+ frappe.qb.from_(table)
+ .select(table.pr_detail, fn.Sum(table.qty).as_("qty"))
+ .where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
+ )
+ invoice_data = query.run(as_list=1)
+
+ if not invoice_data:
+ return frappe._dict()
+ return frappe._dict(invoice_data)
+
+
def adjust_incoming_rate_for_pr(doc):
doc.update_valuation_rate(reset_outgoing_rate=False)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index b097c0e6441..9cf9f4d4958 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -4047,6 +4047,36 @@ class TestPurchaseReceipt(FrappeTestCase):
batch_return.save()
batch_return.submit()
+ def test_pr_status_based_on_invoices_with_update_stock(self):
+ from erpnext.buying.doctype.purchase_order.purchase_order import (
+ make_purchase_invoice as _make_purchase_invoice,
+ )
+ from erpnext.buying.doctype.purchase_order.purchase_order import (
+ make_purchase_receipt as _make_purchase_receipt,
+ )
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import (
+ create_pr_against_po,
+ create_purchase_order,
+ )
+
+ item_code = "Test Item for PR Status Based on Invoices"
+ create_item(item_code)
+
+ po = create_purchase_order(item_code=item_code, qty=10)
+ pi = _make_purchase_invoice(po.name)
+ pi.update_stock = 1
+ pi.items[0].qty = 5
+ pi.submit()
+
+ po.reload()
+ self.assertEqual(po.per_billed, 50)
+
+ pr = _make_purchase_receipt(po.name)
+ self.assertEqual(pr.items[0].qty, 5)
+ pr.submit()
+ pr.reload()
+ self.assertEqual(pr.status, "To Bill")
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
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 610bceddf0f..12d3f99e37b 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -71,7 +71,7 @@
"item_tax_amount",
"rm_supp_cost",
"landed_cost_voucher_amount",
- "rate_difference_with_purchase_invoice",
+ "amount_difference_with_purchase_invoice",
"billed_amt",
"warehouse_and_reference",
"warehouse",
@@ -998,14 +998,6 @@
"label": "Has Item Scanned",
"read_only": 1
},
- {
- "fieldname": "rate_difference_with_purchase_invoice",
- "fieldtype": "Currency",
- "label": "Rate Difference with Purchase Invoice",
- "no_copy": 1,
- "print_hide": 1,
- "read_only": 1
- },
{
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
@@ -1135,12 +1127,20 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1
+ },
+ {
+ "fieldname": "amount_difference_with_purchase_invoice",
+ "fieldtype": "Currency",
+ "label": "Amount Difference with Purchase Invoice",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-07-19 12:14:21.521466",
+ "modified": "2025-02-17 13:15:36.692202",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
index 2154007771d..0db866f52c1 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
@@ -16,6 +16,7 @@ class PurchaseReceiptItem(Document):
allow_zero_valuation_rate: DF.Check
amount: DF.Currency
+ amount_difference_with_purchase_invoice: DF.Currency
apply_tds: DF.Check
asset_category: DF.Link | None
asset_location: DF.Link | None
@@ -76,7 +77,6 @@ class PurchaseReceiptItem(Document):
qty: DF.Float
quality_inspection: DF.Link | None
rate: DF.Currency
- rate_difference_with_purchase_invoice: DF.Currency
rate_with_margin: DF.Currency
received_qty: DF.Float
received_stock_qty: DF.Float
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 274ce0f0d9d..1560db6a114 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -230,15 +230,17 @@ def get_pos_reserved_serial_nos(filters):
pos_transacted_sr_nos = query.run(as_dict=True)
- reserved_sr_nos = set()
- returned_sr_nos = set()
+ reserved_sr_nos = list()
+ returned_sr_nos = list()
for d in pos_transacted_sr_nos:
if d.is_return == 0:
- [reserved_sr_nos.add(x) for x in get_serial_nos(d.serial_no)]
+ [reserved_sr_nos.append(x) for x in get_serial_nos(d.serial_no)]
elif d.is_return == 1:
- [returned_sr_nos.add(x) for x in get_serial_nos(d.serial_no)]
+ [returned_sr_nos.append(x) for x in get_serial_nos(d.serial_no)]
- reserved_sr_nos = list(reserved_sr_nos - returned_sr_nos)
+ for x in returned_sr_nos:
+ if x in reserved_sr_nos:
+ reserved_sr_nos.remove(x)
return reserved_sr_nos
@@ -254,12 +256,7 @@ def fetch_serial_numbers(filters, qty, do_not_include=None):
query = (
frappe.qb.from_(serial_no)
.select(serial_no.name)
- .where(
- (serial_no.item_code == filters["item_code"])
- & (serial_no.warehouse == filters["warehouse"])
- & (Coalesce(serial_no.sales_invoice, "") == "")
- & (Coalesce(serial_no.delivery_document_no, "") == "")
- )
+ .where((serial_no.item_code == filters["item_code"]) & (serial_no.warehouse == filters["warehouse"]))
.orderby(serial_no.creation)
.limit(qty or 1)
)
diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json
index 53b549deec1..8b6e70ac65b 100644
--- a/erpnext/stock/doctype/shipment/shipment.json
+++ b/erpnext/stock/doctype/shipment/shipment.json
@@ -34,6 +34,7 @@
"shipment_parcel",
"parcel_template",
"add_template",
+ "total_weight",
"column_break_28",
"shipment_delivery_note",
"shipment_details_section",
@@ -429,11 +430,17 @@
"label": "Pickup Contact Person",
"mandatory_depends_on": "eval:doc.pickup_from_type === 'Company'",
"options": "User"
+ },
+ {
+ "fieldname": "total_weight",
+ "fieldtype": "Float",
+ "label": "Total Weight (kg)",
+ "read_only": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2022-11-17 17:23:27.025802",
+ "modified": "2025-02-20 16:55:20.076418",
"modified_by": "Administrator",
"module": "Stock",
"name": "Shipment",
diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py
index 346e70e6060..cf9d165fdd3 100644
--- a/erpnext/stock/doctype/shipment/shipment.py
+++ b/erpnext/stock/doctype/shipment/shipment.py
@@ -65,6 +65,7 @@ class Shipment(Document):
shipment_parcel: DF.Table[ShipmentParcel]
shipment_type: DF.Literal["Goods", "Documents"]
status: DF.Literal["Draft", "Submitted", "Booked", "Cancelled", "Completed"]
+ total_weight: DF.Float
tracking_status: DF.Literal["", "In Progress", "Delivered", "Returned", "Lost"]
tracking_status_info: DF.Data | None
tracking_url: DF.SmallText | None
@@ -75,6 +76,7 @@ class Shipment(Document):
self.validate_weight()
self.validate_pickup_time()
self.set_value_of_goods()
+ self.set_total_weight()
if self.docstatus == 0:
self.status = "Draft"
@@ -93,6 +95,12 @@ class Shipment(Document):
if flt(parcel.weight) <= 0:
frappe.throw(_("Parcel weight cannot be 0"))
+ def set_total_weight(self):
+ self.total_weight = self.get_total_weight()
+
+ def get_total_weight(self):
+ return sum(flt(parcel.weight) * parcel.count for parcel in self.shipment_parcel if parcel.count > 0)
+
def validate_pickup_time(self):
if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from):
frappe.throw(_("Pickup To time should be greater than Pickup From time"))
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
index 4d4eadc339b..1c91a054ebc 100644
--- a/erpnext/stock/doctype/shipment/test_shipment.py
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -20,6 +20,17 @@ class TestShipment(FrappeTestCase):
self.assertEqual(len(second_shipment.shipment_delivery_note), 1)
self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name)
+ def test_get_total_weight(self):
+ shipment = frappe.new_doc("Shipment")
+ shipment.extend(
+ "shipment_parcel",
+ [
+ {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5},
+ {"length": 5, "width": 5, "height": 5, "weight": 10, "count": 1},
+ ],
+ )
+ self.assertEqual(shipment.get_total_weight(), 35)
+
def create_test_delivery_note():
company = get_shipment_company()
diff --git a/erpnext/stock/doctype/stock_ledger_entry/patches/__init__.py b/erpnext/stock/doctype/stock_ledger_entry/patches/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/doctype/stock_ledger_entry/patches/ensure_sle_indexes.py b/erpnext/stock/doctype/stock_ledger_entry/patches/ensure_sle_indexes.py
new file mode 100644
index 00000000000..7f29b27af3f
--- /dev/null
+++ b/erpnext/stock/doctype/stock_ledger_entry/patches/ensure_sle_indexes.py
@@ -0,0 +1,9 @@
+from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
+ on_doctype_update as create_sle_indexes,
+)
+
+
+def execute():
+ """Ensure SLE Indexes"""
+
+ create_sle_indexes()
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 22a24d1bfa1..1987bc8642d 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -46,18 +46,20 @@
"auto_reserve_serial_and_batch",
"serial_and_batch_item_settings_tab",
"section_break_7",
+ "allow_existing_serial_no",
"do_not_use_batchwise_valuation",
"auto_create_serial_and_batch_bundle_for_outward",
"pick_serial_and_batch_based_on",
- "naming_series_prefix",
"column_break_mhzc",
"disable_serial_no_and_batch_selector",
- "use_naming_series",
"use_serial_batch_fields",
"do_not_update_serial_batch_on_creation_of_auto_bundle",
- "allow_existing_serial_no",
"serial_and_batch_bundle_section",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
+ "section_break_gnhq",
+ "use_naming_series",
+ "column_break_wslv",
+ "naming_series_prefix",
"stock_planning_tab",
"auto_material_request",
"auto_indent",
@@ -480,6 +482,14 @@
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
"fieldtype": "Check",
"label": "Set Serial and Batch Bundle Naming Based on Naming Series"
+ },
+ {
+ "fieldname": "section_break_gnhq",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_wslv",
+ "fieldtype": "Column Break"
}
],
"icon": "icon-cog",
@@ -487,7 +497,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2025-02-17 14:36:36.177743",
+ "modified": "2025-02-28 16:08:35.938840",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 5c5fe5db276..ff87d159e36 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -221,7 +221,7 @@ def update_stock(ctx, out, doc=None):
else:
qty -= batch_qty
- out.update({"batch_no": batch_no, "actual_batch_qty": qty})
+ out.update({"batch_no": batch_no, "actual_batch_qty": batch_qty})
if rate:
out.update({"rate": rate, "price_list_rate": rate})
@@ -1051,7 +1051,11 @@ def get_batch_based_item_price(params, item_code) -> float:
if not item_price:
item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True)
- if item_price and item_price[0][2] == params.get("uom"):
+ if (
+ item_price
+ and item_price[0][2] == params.get("uom")
+ and not params.get("items", [{}])[0].get("is_free_item", 0)
+ ):
return item_price[0][1]
return 0.0
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index cc597e14196..0edce832aec 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -51,6 +51,10 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
latest_age = date_diff(to_date, fifo_queue[-1][1])
range_values = get_range_age(filters, fifo_queue, to_date, item_dict)
+ check_and_replace_valuations_if_moving_average(
+ range_values, details.valuation_method, details.valuation_rate
+ )
+
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
if filters.get("show_warehouse_wise_stock"):
@@ -72,6 +76,15 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
return data
+def check_and_replace_valuations_if_moving_average(range_values, item_valuation_method, valuation_rate):
+ if item_valuation_method == "Moving Average" or (
+ not item_valuation_method
+ and frappe.db.get_single_value("Stock Settings", "valuation_method") == "Moving Average"
+ ):
+ for i in range(0, len(range_values), 2):
+ range_values[i + 1] = range_values[i] * valuation_rate
+
+
def get_average_age(fifo_queue: list, to_date: str) -> float:
batch_age = age_qty = total_qty = 0.0
for batch in fifo_queue:
@@ -267,7 +280,7 @@ class FIFOSlots:
self.__update_balances(d, key)
- # Note that stock_ledger_entries is an iterator, you can not reuse it like a list
+ # Note that stock_ledger_entries is an iterator, you can not reuse it like a list
del stock_ledger_entries
if not self.filters.get("show_warehouse_wise_stock"):
@@ -396,6 +409,7 @@ class FIFOSlots:
self.item_details[key]["total_qty"] += row.actual_qty
self.item_details[key]["has_serial_no"] = row.has_serial_no
+ self.item_details[key]["details"].valuation_rate = row.valuation_rate
def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict:
"Aggregate Item-Wh wise data into single Item entry."
@@ -437,8 +451,10 @@ class FIFOSlots:
item.description,
item.stock_uom,
item.has_serial_no,
+ item.valuation_method,
sle.actual_qty,
sle.stock_value_difference,
+ sle.valuation_rate,
sle.posting_date,
sle.voucher_type,
sle.voucher_no,
@@ -506,7 +522,14 @@ class FIFOSlots:
item_table = frappe.qb.DocType("Item")
item = frappe.qb.from_("Item").select(
- "name", "item_name", "description", "stock_uom", "brand", "item_group", "has_serial_no"
+ "name",
+ "item_name",
+ "description",
+ "stock_uom",
+ "brand",
+ "item_group",
+ "has_serial_no",
+ "valuation_method",
)
if self.filters.get("item_code"):
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 993d918f8bc..c88df01665f 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -1006,6 +1006,10 @@ class SerialBatchCreation:
elif self.has_serial_no and not self.get("serial_nos"):
self.serial_nos = get_serial_nos_for_outward(kwargs)
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
+ if self.get("posting_date"):
+ kwargs["posting_date"] = self.get("posting_date")
+ kwargs["posting_time"] = self.get("posting_time")
+
self.batches = get_available_batches(kwargs)
def set_auto_serial_batch_entries_for_inward(self):
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index ca08a5ef121..aaeb90b7d30 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -561,12 +561,28 @@ class update_entries_after:
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 = flt(self.args.reserved_stock)
+ self.reserved_stock = self.get_reserved_stock()
self.data = frappe._dict()
self.initialize_previous_data(self.args)
self.build()
+ def get_reserved_stock(self):
+ sre = frappe.qb.DocType("Stock Reservation Entry")
+ posting_datetime = get_combine_datetime(self.args.posting_date, self.args.posting_time)
+ query = (
+ frappe.qb.from_(sre)
+ .select(Sum(sre.reserved_qty) - Sum(sre.delivered_qty))
+ .where(
+ (sre.item_code == self.item_code)
+ & (sre.warehouse == self.args.warehouse)
+ & (sre.docstatus == 1)
+ & (sre.creation <= posting_datetime)
+ )
+ ).run()
+
+ return flt(query[0][0]) if query else 0.0
+
def set_precision(self):
self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
self.currency_precision = get_field_precision(
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
index e9513a47597..bf0c8dc53f8 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
@@ -16,14 +16,14 @@ frappe.ui.form.on("Subcontracting Order Item", {
service_item.doctype,
service_item.name,
"qty",
- row.qty * row.sc_conversion_factor
+ row.qty * row.subcontracting_conversion_factor
);
frappe.model.set_value(service_item.doctype, service_item.name, "fg_item_qty", row.qty);
frappe.model.set_value(
service_item.doctype,
service_item.name,
"amount",
- row.qty * row.sc_conversion_factor * service_item.rate
+ row.qty * row.subcontracting_conversion_factor * service_item.rate
);
},
before_items_remove(frm, cdt, cdn) {
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index f6d3fa04148..d171883c408 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -119,12 +119,12 @@ class SubcontractingOrder(SubcontractingController):
def on_submit(self):
self.update_prevdoc_status()
self.update_status()
- self.update_sco_qty_in_po()
+ self.update_subcontracted_quantity_in_po()
def on_cancel(self):
self.update_prevdoc_status()
self.update_status()
- self.update_sco_qty_in_po(cancel=True)
+ self.update_subcontracted_quantity_in_po(cancel=True)
def validate_purchase_order_for_subcontracting(self):
if self.purchase_order:
@@ -162,7 +162,7 @@ class SubcontractingOrder(SubcontractingController):
item = next(
item for item in self.items if item.purchase_order_item == service_item.purchase_order_item
)
- service_item.qty = item.qty * item.sc_conversion_factor
+ service_item.qty = item.qty * item.subcontracting_conversion_factor
service_item.fg_item_qty = item.qty
service_item.amount = service_item.qty * service_item.rate
@@ -250,7 +250,7 @@ class SubcontractingOrder(SubcontractingController):
item = frappe.get_doc("Item", si.fg_item)
po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item)
- available_qty = po_item.qty - po_item.sco_qty
+ available_qty = po_item.qty - po_item.subcontracted_quantity
if available_qty == 0:
continue
@@ -276,7 +276,7 @@ class SubcontractingOrder(SubcontractingController):
"schedule_date": self.schedule_date,
"description": item.description,
"qty": si.fg_item_qty,
- "sc_conversion_factor": conversion_factor,
+ "subcontracting_conversion_factor": conversion_factor,
"stock_uom": item.stock_uom,
"bom": bom,
"purchase_order_item": si.purchase_order_item,
@@ -330,10 +330,14 @@ class SubcontractingOrder(SubcontractingController):
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
- def update_sco_qty_in_po(self, cancel=False):
+ def update_subcontracted_quantity_in_po(self, cancel=False):
for service_item in self.service_items:
doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item)
- doc.sco_qty = (doc.sco_qty + service_item.qty) if not cancel else (doc.sco_qty - service_item.qty)
+ doc.subcontracted_quantity = (
+ (doc.subcontracted_quantity + service_item.qty)
+ if not cancel
+ else (doc.subcontracted_quantity - service_item.qty)
+ )
doc.save()
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json
index 31616944fda..e85929dd2bb 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json
@@ -52,7 +52,7 @@
"section_break_34",
"purchase_order_item",
"page_break",
- "sc_conversion_factor"
+ "subcontracting_conversion_factor"
],
"fields": [
{
@@ -384,18 +384,35 @@
"search_index": 1
},
{
- "fieldname": "sc_conversion_factor",
+ "fieldname": "references_section",
+ "fieldtype": "Section Break",
+ "label": "References"
+ },
+ {
+ "fieldname": "job_card",
+ "fieldtype": "Link",
+ "label": "Job Card",
+ "options": "Job Card",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_nfod",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "subcontracting_conversion_factor",
"fieldtype": "Float",
"hidden": 1,
- "label": "SC Conversion Factor",
+ "label": "Subcontracting Conversion Factor",
"read_only": 1
}
],
+ "grid_page_length": 50,
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-12-13 13:35:28.935898",
+ "modified": "2025-03-02 17:05:28.386492",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Item",
@@ -403,6 +420,7 @@
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
+ "row_format": "Dynamic",
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py
index d8f2e5664e7..db49fccce3c 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py
@@ -42,10 +42,10 @@ class SubcontractingOrderItem(Document):
received_qty: DF.Float
returned_qty: DF.Float
rm_cost_per_qty: DF.Currency
- sc_conversion_factor: DF.Float
schedule_date: DF.Date | None
service_cost_per_qty: DF.Currency
stock_uom: DF.Link
+ subcontracting_conversion_factor: DF.Float
warehouse: DF.Link
# end: auto-generated types
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index 03d209e99e3..eb53469acd5 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -15,7 +15,9 @@ frappe.ui.form.on("Issue", {
}
}
);
+ },
+ refresh: function (frm) {
// buttons
if (frm.doc.status !== "Closed") {
frm.add_custom_button(__("Close"), function () {
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index 3ff7d02f1ae..622e19e9e3a 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -27,7 +27,7 @@
"reset_service_level_agreement",
"cb",
"agreement_status",
- "resolution_by",
+ "sla_resolution_by",
"service_level_agreement_creation",
"on_hold_since",
"total_hold_time",
@@ -41,7 +41,7 @@
"column_break1",
"opening_date",
"opening_time",
- "resolution_date",
+ "sla_resolution_date",
"resolution_time",
"user_resolution_time",
"additional_info",
@@ -176,13 +176,6 @@
"options": "fa fa-pushpin",
"read_only": 1
},
- {
- "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
- "fieldname": "resolution_by",
- "fieldtype": "Datetime",
- "label": "Resolution By",
- "read_only": 1
- },
{
"collapsible": 1,
"fieldname": "response",
@@ -287,16 +280,6 @@
"oldfieldtype": "Time",
"read_only": 1
},
- {
- "depends_on": "eval:!doc.__islocal",
- "fieldname": "resolution_date",
- "fieldtype": "Datetime",
- "label": "Resolution Date",
- "no_copy": 1,
- "oldfieldname": "resolution_date",
- "oldfieldtype": "Date",
- "read_only": 1
- },
{
"fieldname": "content_type",
"fieldtype": "Data",
@@ -386,12 +369,29 @@
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
+ "fieldname": "sla_resolution_by",
+ "fieldtype": "Datetime",
+ "label": "Resolution By",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "sla_resolution_date",
+ "fieldtype": "Datetime",
+ "label": "Resolution Date",
+ "no_copy": 1,
+ "oldfieldname": "resolution_date",
+ "oldfieldtype": "Date",
+ "read_only": 1
}
],
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
- "modified": "2021-11-24 13:13:10.276630",
+ "modified": "2025-02-18 21:18:52.797745",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",
@@ -420,4 +420,4 @@
"title_field": "subject",
"track_changes": 1,
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index aeaf28c169c..22630d80f38 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -48,13 +48,13 @@ class Issue(Document):
priority: DF.Link | None
project: DF.Link | None
raised_by: DF.Data | None
- resolution_by: DF.Datetime | None
- resolution_date: DF.Datetime | None
resolution_details: DF.TextEditor | None
resolution_time: DF.Duration | None
response_by: DF.Datetime | None
service_level_agreement: DF.Link | None
service_level_agreement_creation: DF.Datetime | None
+ sla_resolution_by: DF.Datetime | None
+ sla_resolution_date: DF.Datetime | None
status: DF.Literal["Open", "Replied", "On Hold", "Resolved", "Closed"]
subject: DF.Data
total_hold_time: DF.Duration | None
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index acad115f626..c334a2251b3 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -33,48 +33,48 @@ class TestIssue(TestSetUp):
issue = make_issue(creation, "_Test Customer", 1)
self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00"))
- self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 15:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 15:00"))
# make issue with customer_group specific SLA
create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory")
issue = make_issue(creation, "__Test Customer", 2)
self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00"))
- self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 15:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 15:00"))
# make issue with territory specific SLA
create_customer("___Test Customer", "__Test SLA Customer Group", "_Test SLA Territory")
issue = make_issue(creation, "___Test Customer", 3)
self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00"))
- self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 15:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 15:00"))
# make issue with default SLA
issue = make_issue(creation=creation, index=4)
self.assertEqual(issue.response_by, get_datetime("2019-03-04 16:00"))
- self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 18:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 18:00"))
# make issue with default SLA before working hours
creation = get_datetime("2019-03-04 7:00")
issue = make_issue(creation=creation, index=5)
self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00"))
- self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 16:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 16:00"))
# make issue with default SLA after working hours
creation = get_datetime("2019-03-04 20:00")
issue = make_issue(creation, index=6)
self.assertEqual(issue.response_by, get_datetime("2019-03-06 14:00"))
- self.assertEqual(issue.resolution_by, get_datetime("2019-03-06 16:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-06 16:00"))
# make issue with default SLA next day
creation = get_datetime("2019-03-04 14:00")
issue = make_issue(creation=creation, index=7)
self.assertEqual(issue.response_by, get_datetime("2019-03-04 18:00"))
- self.assertEqual(issue.resolution_by, get_datetime("2019-03-06 12:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-06 12:00"))
frappe.flags.current_time = get_datetime("2019-03-04 15:00")
issue.reload()
@@ -98,7 +98,7 @@ class TestIssue(TestSetUp):
issue.save()
self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
- self.assertFalse(issue.resolution_by)
+ self.assertFalse(issue.sla_resolution_by)
creation = get_datetime("2020-03-04 5:00")
frappe.flags.current_time = get_datetime("2020-03-04 5:00")
@@ -106,7 +106,7 @@ class TestIssue(TestSetUp):
issue.reload()
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
- self.assertEqual(issue.resolution_by, get_datetime("2020-03-04 16:45"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2020-03-04 16:45"))
creation = get_datetime("2020-03-04 5:05")
create_communication(issue.name, "test@admin.com", "Sent", creation)
@@ -140,8 +140,8 @@ class TestIssue(TestSetUp):
issue.status = "Closed"
issue.save()
- self.assertEqual(issue.resolution_by, get_datetime("2021-11-22 06:00:00"))
- self.assertEqual(issue.resolution_date, get_datetime("2021-11-22 01:00:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-22 06:00:00"))
+ self.assertEqual(issue.sla_resolution_date, get_datetime("2021-11-22 01:00:00"))
self.assertEqual(issue.agreement_status, "Fulfilled")
def test_issue_open_after_closed(self):
@@ -153,7 +153,7 @@ class TestIssue(TestSetUp):
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
self.assertEqual(issue.agreement_status, "First Response Due")
self.assertEqual(issue.response_by, get_datetime("2021-11-01 17:00"))
- self.assertEqual(issue.resolution_by, get_datetime("2021-11-01 19:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-01 19:00"))
# Replied on → 2 pm
frappe.flags.current_time = get_datetime("2021-11-01 14:00")
@@ -173,7 +173,7 @@ class TestIssue(TestSetUp):
# Hold Time + 1 Hrs
self.assertEqual(issue.total_hold_time, 3600)
# Resolution By should increase by one hrs
- self.assertEqual(issue.resolution_by, get_datetime("2021-11-01 20:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-01 20:00"))
# Replied on → 4 pm, Open → 1 hr, Resolution Due → 8 pm
frappe.flags.current_time = get_datetime("2021-11-01 16:00")
@@ -190,9 +190,9 @@ class TestIssue(TestSetUp):
# Hold Time + 6 Hrs
self.assertEqual(issue.total_hold_time, 3600 + 21600)
# Resolution By should increase by 6 hrs
- self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 02:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-02 02:00"))
self.assertEqual(issue.agreement_status, "Fulfilled")
- self.assertEqual(issue.resolution_date, frappe.flags.current_time)
+ self.assertEqual(issue.sla_resolution_date, frappe.flags.current_time)
# Customer Open → 3 am i.e after resolution by is crossed
frappe.flags.current_time = get_datetime("2021-11-02 03:00")
@@ -201,17 +201,17 @@ class TestIssue(TestSetUp):
# Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm)
self.assertEqual(issue.total_hold_time, 3600 + 21600 + 18000)
# Resolution By should increase by 5 hrs
- self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 07:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-02 07:00"))
self.assertEqual(issue.agreement_status, "Resolution Due")
- self.assertFalse(issue.resolution_date)
+ self.assertFalse(issue.sla_resolution_date)
# We Closed → 4 am, SLA should be Fulfilled
frappe.flags.current_time = get_datetime("2021-11-02 04:00")
issue.status = "Closed"
issue.save()
- self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 07:00"))
+ self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-02 07:00"))
self.assertEqual(issue.agreement_status, "Fulfilled")
- self.assertEqual(issue.resolution_date, frappe.flags.current_time)
+ self.assertEqual(issue.sla_resolution_date, frappe.flags.current_time)
def test_recording_of_assignment_on_first_reponse_failure(self):
from frappe.desk.form.assign_to import add as add_assignment
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index 011a5bc371f..3433a842ea8 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -514,7 +514,7 @@ def apply(doc, method=None):
def remove_sla_if_applied(doc):
doc.service_level_agreement = None
doc.response_by = None
- doc.resolution_by = None
+ doc.sla_resolution_by = None
def process_sla(doc, sla):
@@ -557,7 +557,7 @@ def handle_status_change(doc, apply_sla_for_resolution):
# In case issue was closed and after few days it has been opened
# The hold time should be calculated from resolution_date
- on_hold_since = doc.resolution_date or doc.on_hold_since
+ on_hold_since = doc.sla_resolution_date or doc.on_hold_since
if on_hold_since:
current_hold_hours = time_diff_in_seconds(now_time, on_hold_since)
doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours
@@ -582,7 +582,7 @@ def handle_status_change(doc, apply_sla_for_resolution):
# Open to Closed
if is_open_status(prev_status) and is_fulfilled_status(doc.status):
# Issue is closed -> Set resolution_date
- doc.resolution_date = now_time
+ doc.sla_resolution_date = now_time
set_resolution_time(doc)
# Closed to Open
@@ -606,7 +606,7 @@ def handle_status_change(doc, apply_sla_for_resolution):
calculate_hold_hours()
# Issue is closed -> Set resolution_date
if apply_sla_for_resolution:
- doc.resolution_date = now_time
+ doc.sla_resolution_date = now_time
set_resolution_time(doc)
@@ -713,7 +713,7 @@ def get_support_days(service_level):
def set_resolution_time(doc):
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
if doc.meta.has_field("resolution_time"):
- doc.resolution_time = time_diff_in_seconds(doc.resolution_date, start_date_time)
+ doc.resolution_time = time_diff_in_seconds(doc.sla_resolution_date, start_date_time)
# total time taken by a user to close the issue apart from wait_time
if not doc.meta.has_field("user_resolution_time"):
@@ -737,7 +737,7 @@ def set_resolution_time(doc):
pending_time.append(wait_time)
total_pending_time = sum(pending_time)
- resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, start_date_time)
+ resolution_time_in_secs = time_diff_in_seconds(doc.sla_resolution_date, start_date_time)
doc.user_resolution_time = resolution_time_in_secs - total_pending_time
@@ -793,8 +793,8 @@ def reset_service_level_agreement(doctype: str, docname: str, reason, user):
def reset_resolution_metrics(doc):
- if doc.meta.has_field("resolution_date"):
- doc.resolution_date = None
+ if doc.meta.has_field("sla_resolution_date"):
+ doc.sla_resolution_date = None
if doc.meta.has_field("resolution_time"):
doc.resolution_time = None
@@ -861,8 +861,8 @@ def on_communication_update(doc, status):
def reset_expected_response_and_resolution(doc):
if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"):
doc.response_by = None
- if doc.meta.has_field("resolution_by") and not doc.get("resolution_date"):
- doc.resolution_by = None
+ if doc.meta.has_field("sla_resolution_by") and not doc.get("sla_resolution_date"):
+ doc.sla_resolution_by = None
def set_response_by(doc, start_date_time, priority):
@@ -879,12 +879,14 @@ def set_response_by(doc, start_date_time, priority):
def set_resolution_by(doc, start_date_time, priority):
- if doc.meta.has_field("resolution_by"):
- doc.resolution_by = get_expected_time_for(
+ if doc.meta.has_field("sla_resolution_by"):
+ doc.sla_resolution_by = get_expected_time_for(
parameter="resolution", service_level=priority, start_date_time=start_date_time
)
if doc.meta.has_field("total_hold_time") and doc.get("total_hold_time"):
- doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get("total_hold_time")))
+ doc.sla_resolution_by = add_to_date(
+ doc.sla_resolution_by, seconds=round(doc.get("total_hold_time"))
+ )
def record_assigned_users_on_failure(doc):
@@ -943,7 +945,7 @@ def get_service_level_agreement_fields():
"read_only": 1,
},
{
- "fieldname": "resolution_by",
+ "fieldname": "sla_resolution_by",
"fieldtype": "Datetime",
"label": "Resolution By",
"read_only": 1,
@@ -957,7 +959,7 @@ def get_service_level_agreement_fields():
},
{
"depends_on": "eval:!doc.__islocal",
- "fieldname": "resolution_date",
+ "fieldname": "sla_resolution_date",
"fieldtype": "Datetime",
"label": "Resolution Date",
"no_copy": 1,
@@ -977,9 +979,9 @@ def update_agreement_status(doc, apply_sla_for_resolution):
if apply_sla_for_resolution:
if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"):
doc.agreement_status = "First Response Due"
- elif doc.meta.has_field("resolution_date") and not doc.get("resolution_date"):
+ elif doc.meta.has_field("sla_resolution_date") and not doc.get("sla_resolution_date"):
doc.agreement_status = "Resolution Due"
- elif get_datetime(doc.get("resolution_date")) <= get_datetime(doc.get("resolution_by")):
+ elif get_datetime(doc.get("sla_resolution_date")) <= get_datetime(doc.get("sla_resolution_by")):
doc.agreement_status = "Fulfilled"
else:
doc.agreement_status = "Failed"
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index 47ea251b0fe..cabd38f6427 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -227,7 +227,7 @@ class TestServiceLevelAgreement(unittest.TestCase):
self.assertEqual(lead.service_level_agreement, lead_sla.name)
self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0))
- self.assertEqual(lead.resolution_by, datetime.datetime(2019, 3, 4, 18, 0))
+ self.assertEqual(lead.sla_resolution_by, datetime.datetime(2019, 3, 4, 18, 0))
frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 0)
lead.reload()
@@ -268,7 +268,7 @@ class TestServiceLevelAgreement(unittest.TestCase):
lead.reload()
self.assertEqual(flt(lead.total_hold_time, 2), 3000)
- self.assertEqual(lead.resolution_by, datetime.datetime(2020, 3, 4, 16, 50))
+ self.assertEqual(lead.sla_resolution_by, datetime.datetime(2020, 3, 4, 16, 50))
def test_failed_sla_for_response_only(self):
doctype = "Lead"
diff --git a/erpnext/templates/includes/footer/footer_powered.html b/erpnext/templates/includes/footer/footer_powered.html
index 8310063e575..fb73931d18e 100644
--- a/erpnext/templates/includes/footer/footer_powered.html
+++ b/erpnext/templates/includes/footer/footer_powered.html
@@ -1 +1 @@
-{{ _("Powered by {0}").format('ERPNext') }}
+{{ _("Powered by {0}").format('ERPNext') }}
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 9ef1d4bc63a..aa8d7322958 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -5098,7 +5098,7 @@ Percentage you are allowed to transfer more against the quantity ordered. For ex
PUR-ORD-.YYYY.-,PUR-ORD-.YYYY.-,
Get Items from Open Material Requests,Hole Artikel von offenen Material Anfragen,
Fetch items based on Default Supplier.,Abrufen von Elementen basierend auf dem Standardlieferanten.,
-Required By,Benötigt von,
+Required By,Benötigt bis,
Order Confirmation No,Auftragsbestätigung Nr,
Order Confirmation Date,Auftragsbestätigungsdatum,
Customer Mobile No,Mobilnummer des Kunden,
diff --git a/package.json b/package.json
index 4e686f7ca74..509fe275e05 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"type": "git",
"url": "git+https://github.com/frappe/erpnext.git"
},
- "homepage": "https://erpnext.com",
+ "homepage": "https://frappe.io/erpnext",
"author": "Frappe Technologies Pvt. Ltd.",
"license": "GPL-3.0",
"bugs": {
diff --git a/pyproject.toml b/pyproject.toml
index d891b186d89..e122b2d176c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,6 +70,6 @@ docstring-code-format = true
[project.urls]
-Homepage = "https://erpnext.com/"
+Homepage = "https://frappe.io/erpnext"
Repository = "https://github.com/frappe/erpnext.git"
"Bug Reports" = "https://github.com/frappe/erpnext/issues"