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 %} +
+
+ {{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }} +
+ + + {% 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) }}
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + {% for item in doc.items %} + + + + + + + + {% endfor %} +
{{ _("Sr") }}{{ _("Details") }}{{ _("Qty") }}{{ _("Rate") }}{{ _("Amount") }}
{{ 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) }}
+ +
+ +
+
+ + {{ doc.in_words }} +
+
+ + {{ doc.status }} +
+
+
+
+
{{ _("Sub Total") }}
+
{{ doc.get_formatted("net_total", doc) }}
+
+
+ {% for d in doc.taxes %} + {% if d.tax_amount %} +
+
{{ _(d.description) }}
+
{{ d.get_formatted("tax_amount") }}
+
+ {% endif %} + {% endfor %} +
+
+
{{ _("Total") }}
+
{{ 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 = """
Sent via - ERPNext
""" + 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 = "" + + 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"