diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 68055546c24..1012ff56ebf 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -246,6 +246,7 @@ class PaymentRequest(Document): "payer_name": data.customer_name, "order_id": self.name, "currency": self.currency, + "payment_gateway": self.payment_gateway, } ) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index ee9dd2be8c3..c4825a6d519 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -53,6 +53,7 @@ "column_break_42", "free_item_uom", "round_free_qty", + "dont_enforce_free_item_qty", "is_recursive", "recurse_for", "apply_recursion_over", @@ -643,12 +644,19 @@ "fieldname": "has_priority", "fieldtype": "Check", "label": "Has Priority" + }, + { + "default": "0", + "depends_on": "eval:doc.price_or_product_discount == 'Product'", + "fieldname": "dont_enforce_free_item_qty", + "fieldtype": "Check", + "label": "Don't Enforce Free Item Qty" } ], "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2024-09-16 18:14:51.314765", + "modified": "2025-02-17 18:15:39.824639", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 902af5c2a77..afd27325820 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -553,7 +553,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra if pricing_rule.margin_type in ["Percentage", "Amount"]: item_details.margin_rate_or_amount = 0.0 item_details.margin_type = None - elif pricing_rule.get("free_item"): + elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"): item_details.remove_free_item = ( item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item") ) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 57f2d791199..d4e39a1315b 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -428,6 +428,54 @@ class TestPricingRule(FrappeTestCase): self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].item_code, "_Test Item 2") + def test_dont_enforce_free_item_qty(self): + # this test is only for testing non-enforcement as all other tests in this file already test with enforcement + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "rate_or_discount": "Discount Percentage", + "rate": 0, + "min_qty": 0, + "max_qty": 7, + "discount_percentage": 17.5, + "price_or_product_discount": "Product", + "same_item": 0, + "free_item": "_Test Item 2", + "free_qty": 1, + "company": "_Test Company", + } + pricing_rule = frappe.get_doc(test_record.copy()).insert() + + # With enforcement + so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True) + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item 2") + + # Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save + so.items.pop(1) + so.save() + so.reload() + self.assertEqual(len(so.items), 2) + + # Without enforcement + pricing_rule.dont_enforce_free_item_qty = 1 + pricing_rule.save() + + # Test 2 : Deleted free item will not be fetched again on save without enforcement + so.items.pop(1) + so.save() + so.reload() + self.assertEqual(len(so.items), 1) + def test_cumulative_pricing_rule(self): frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule") test_record = { @@ -1239,6 +1287,7 @@ def make_pricing_rule(**args): "discount_amount": args.discount_amount or 0.0, "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0, "has_priority": args.has_priority or 0, + "enforce_free_item_qty": args.dont_enforce_free_item_qty or 0, } ) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index bec4d2407de..fe81945ad41 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -691,7 +691,10 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args): args.pop((item.item_code, item.pricing_rules)) for free_item in args.values(): - doc.append("items", free_item) + if doc.is_new() or not frappe.get_value( + "Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty" + ): + doc.append("items", free_item) def get_pricing_rule_items(pr_doc, other_items=False) -> list: diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index a6a2b2410c9..511c89dfc8f 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -177,17 +177,21 @@ def get_ar_filters(doc, entry): def get_html(doc, filters, entry, col, res, ageing): base_template_path = "frappe/www/printview.html" - template_path = ( - "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" - if doc.report == "General Ledger" - else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html" - ) + template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html" + if doc.report == "General Ledger": + template_path = ( + "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + ) + + process_soa_html = frappe.get_hooks("process_soa_html") + # fetching custom print format for Process Statement of Accounts + if process_soa_html and process_soa_html.get(doc.report): + template_path = process_soa_html[doc.report][-1] if doc.letter_head: from frappe.www.printview import get_letter_head letter_head = get_letter_head(doc, 0) - html = frappe.render_template( template_path, { @@ -203,7 +207,6 @@ def get_html(doc, filters, entry, col, res, ageing): else None, }, ) - html = frappe.render_template( base_template_path, {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer}, 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 2115d44322d..da329324e95 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -236,7 +236,7 @@ def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no): def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None): - vouchers, voucher_wise_amount = get_invoice_vouchers( + vouchers, voucher_wise_amount, zero_rate_ldc_invoices = get_invoice_vouchers( parties, tax_details, inv.company, party_type=party_type ) @@ -290,7 +290,8 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N # once tds is deducted, not need to add vouchers in the invoice voucher_wise_amount = {} else: - tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers) + taxable_vouchers = list(set(vouchers) - set(zero_rate_ldc_invoices)) + tax_amount = get_tds_amount(ldc, parties, inv, tax_details, taxable_vouchers) elif party_type == "Customer": if tax_deducted: @@ -309,12 +310,33 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" - field = ( - "base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total" - ) + field = [ + "name", + "base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total", + "posting_date", + ] voucher_wise_amount = {} vouchers = [] + ldcs = frappe.db.get_all( + "Lower Deduction Certificate", + filters={ + "valid_from": [">=", tax_details.from_date], + "valid_upto": ["<=", tax_details.to_date], + "company": company, + "supplier": ["in", parties], + }, + fields=["supplier", "valid_from", "valid_upto", "rate"], + ) + + doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" + field = [ + "base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total", + "name", + "grand_total", + "posting_date", + ] + filters = { "company": company, frappe.scrub(party_type): ["in", parties], @@ -328,11 +350,31 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} ) - invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field]) + invoices_details = frappe.get_all(doctype, filters=filters, fields=field) + ldcs = frappe.db.get_all( + "Lower Deduction Certificate", + filters={ + "valid_from": [">=", tax_details.from_date], + "valid_upto": ["<=", tax_details.to_date], + "company": company, + "supplier": ["in", parties], + "rate": 0, + }, + fields=["name", "supplier", "valid_from", "valid_upto"], + ) + + zero_rate_ldc_invoices = [] for d in invoices_details: vouchers.append(d.name) - voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}}) + _voucher_detail = {"amount": d.base_net_total, "voucher_type": doctype} + + if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]: + if ldc[0].supplier in parties: + _voucher_detail.update({"amount": 0}) + zero_rate_ldc_invoices.append(d.name) + + voucher_wise_amount.update({d.name: _voucher_detail}) journal_entries_details = frappe.db.sql( """ @@ -363,7 +405,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): vouchers.append(d.name) voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}}) - return vouchers, voucher_wise_amount + return vouchers, voucher_wise_amount, zero_rate_ldc_invoices def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"): diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index d459b77865c..6a7fc3c43d2 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, today +from frappe.utils import add_days, add_months, today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.utils import get_fiscal_year @@ -614,6 +614,49 @@ class TestTaxWithholdingCategory(FrappeTestCase): pi2.cancel() pi3.cancel() + def test_ldc_at_0_rate(self): + frappe.db.set_value( + "Supplier", + "Test LDC Supplier", + { + "tax_withholding_category": "Test Service Category", + "pan": "ABCTY1234D", + }, + ) + + fiscal_year = get_fiscal_year(today(), company="_Test Company") + valid_from = fiscal_year[1] + valid_upto = add_months(valid_from, 1) + create_lower_deduction_certificate( + supplier="Test LDC Supplier", + certificate_no="1AE0423AAJ", + tax_withholding_category="Test Service Category", + tax_rate=0, + limit=50000, + valid_from=valid_from, + valid_upto=valid_upto, + ) + + pi1 = create_purchase_invoice( + supplier="Test LDC Supplier", rate=35000, posting_date=valid_from, set_posting_time=True + ) + pi1.submit() + self.assertEqual(pi1.taxes, []) + + pi2 = create_purchase_invoice( + supplier="Test LDC Supplier", + rate=35000, + posting_date=add_days(valid_upto, 1), + set_posting_time=True, + ) + pi2.submit() + self.assertEqual(len(pi2.taxes), 1) + # pi1 net total shouldn't be included as it lies within LDC at rate of '0' + self.assertEqual(pi2.taxes[0].tax_amount, 3500) + + pi1.cancel() + pi2.cancel() + def set_previous_fy_and_tax_category(self): test_company = "_Test Company" category = "Cumulative Threshold TDS" @@ -771,7 +814,8 @@ def create_purchase_invoice(**args): pi = frappe.get_doc( { "doctype": "Purchase Invoice", - "posting_date": today(), + "set_posting_time": args.set_posting_time or False, + "posting_date": args.posting_date or today(), "apply_tds": 0 if args.do_not_apply_tds else 1, "supplier": args.supplier, "company": "_Test Company", @@ -1099,7 +1143,9 @@ def create_tax_withholding_category( ).insert() -def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_rate, certificate_no, limit): +def create_lower_deduction_certificate( + supplier, tax_withholding_category, tax_rate, certificate_no, limit, valid_from=None, valid_upto=None +): fiscal_year = get_fiscal_year(today(), company="_Test Company") if not frappe.db.exists("Lower Deduction Certificate", certificate_no): frappe.get_doc( @@ -1110,8 +1156,8 @@ def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_r "certificate_no": certificate_no, "tax_withholding_category": tax_withholding_category, "fiscal_year": fiscal_year[0], - "valid_from": fiscal_year[1], - "valid_upto": fiscal_year[2], + "valid_from": valid_from or fiscal_year[1], + "valid_upto": valid_upto or fiscal_year[2], "rate": tax_rate, "certificate_limit": limit, } diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 171a94e1151..44fa96e3c0d 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -111,6 +111,7 @@ frappe.query_reports["Accounts Payable"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 92ea9e8f598..24b82c56d89 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -88,6 +88,7 @@ frappe.query_reports["Accounts Payable Summary"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 7e4563ee85e..4b60ba8adc0 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -56,6 +56,7 @@ frappe.query_reports["Accounts Receivable"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 964abc23747..df141d0732a 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -88,6 +88,7 @@ frappe.query_reports["Accounts Receivable Summary"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 83bd48c71f3..c74450191aa 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -91,6 +91,7 @@ function get_filters() { fieldname: "budget_against_filter", label: __("Dimension Filter"), fieldtype: "MultiSelectList", + options: "budget_against", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 3431b76dfb1..40455fa882d 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -68,6 +68,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; @@ -146,6 +147,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "cost_center", label: __("Cost Center"), fieldtype: "MultiSelectList", + options: "Cost Center", get_data: function (txt) { return frappe.db.get_link_options("Cost Center", txt, { company: frappe.query_report.get_filter_value("company"), @@ -156,6 +158,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "project", label: __("Project"), fieldtype: "MultiSelectList", + options: "Project", get_data: function (txt) { return frappe.db.get_link_options("Project", txt, { company: frappe.query_report.get_filter_value("company"), diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.js b/erpnext/accounts/report/payment_ledger/payment_ledger.js index e0ea7522b12..0121580858f 100644 --- a/erpnext/accounts/report/payment_ledger/payment_ledger.js +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.js @@ -51,6 +51,7 @@ function get_filters() { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js index 50578d314e3..62482ac162c 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js @@ -68,16 +68,12 @@ frappe.query_reports["Trial Balance for Party"] = { { fieldname: "account", label: __("Account"), - fieldtype: "Link", + fieldtype: "MultiSelectList", options: "Account", - get_query: function () { - var company = frappe.query_report.get_filter_value("company"); - return { - doctype: "Account", - filters: { - company: company, - }, - }; + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, }, { diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index dd1a12514e2..f6c79eb6c45 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -4,8 +4,10 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum from frappe.utils import cint, flt +from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children from erpnext.accounts.report.trial_balance.trial_balance import validate_filters @@ -35,9 +37,14 @@ def get_data(filters, show_party_name): filters=party_filters, order_by="name", ) + + account_filter = [] + if filters.get("account"): + account_filter = get_accounts_with_children(filters.get("account")) + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") - opening_balances = get_opening_balances(filters) - balances_within_period = get_balances_within_period(filters) + opening_balances = get_opening_balances(filters, account_filter) + balances_within_period = get_balances_within_period(filters, account_filter) data = [] # total_debit, total_credit = 0, 0 @@ -89,30 +96,34 @@ def get_data(filters, show_party_name): return data -def get_opening_balances(filters): - account_filter = "" - if filters.get("account"): - account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) +def get_opening_balances(filters, account_filter=None): + GL_Entry = frappe.qb.DocType("GL Entry") - gle = frappe.db.sql( - f""" - select party, sum(debit) as opening_debit, sum(credit) as opening_credit - from `tabGL Entry` - where company=%(company)s - and is_cancelled=0 - and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) - {account_filter} - group by party""", - { - "company": filters.company, - "from_date": filters.from_date, - "to_date": filters.to_date, - "party_type": filters.party_type, - }, - as_dict=True, + query = ( + frappe.qb.from_(GL_Entry) + .select( + GL_Entry.party, + Sum(GL_Entry.debit).as_("opening_debit"), + Sum(GL_Entry.credit).as_("opening_credit"), + ) + .where( + (GL_Entry.company == filters.company) + & (GL_Entry.is_cancelled == 0) + & (GL_Entry.party_type == filters.party_type) + & (GL_Entry.party != "") + & ( + (GL_Entry.posting_date < filters.from_date) + | ((GL_Entry.is_opening == "Yes") & (GL_Entry.posting_date <= filters.to_date)) + ) + ) + .groupby(GL_Entry.party) ) + if account_filter: + query = query.where(GL_Entry.account.isin(account_filter)) + + gle = query.run(as_dict=True) + opening = frappe._dict() for d in gle: opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit) @@ -121,31 +132,33 @@ def get_opening_balances(filters): return opening -def get_balances_within_period(filters): - account_filter = "" - if filters.get("account"): - account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) +def get_balances_within_period(filters, account_filter=None): + GL_Entry = frappe.qb.DocType("GL Entry") - gle = frappe.db.sql( - f""" - select party, sum(debit) as debit, sum(credit) as credit - from `tabGL Entry` - where company=%(company)s - and is_cancelled = 0 - and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and posting_date >= %(from_date)s and posting_date <= %(to_date)s - and ifnull(is_opening, 'No') = 'No' - {account_filter} - group by party""", - { - "company": filters.company, - "from_date": filters.from_date, - "to_date": filters.to_date, - "party_type": filters.party_type, - }, - as_dict=True, + query = ( + frappe.qb.from_(GL_Entry) + .select( + GL_Entry.party, + Sum(GL_Entry.debit).as_("debit"), + Sum(GL_Entry.credit).as_("credit"), + ) + .where( + (GL_Entry.company == filters.company) + & (GL_Entry.is_cancelled == 0) + & (GL_Entry.party_type == filters.party_type) + & (GL_Entry.party != "") + & (GL_Entry.posting_date >= filters.from_date) + & (GL_Entry.posting_date <= filters.to_date) + & (GL_Entry.is_opening == "No") + ) + .groupby(GL_Entry.party) ) + if account_filter: + query = query.where(GL_Entry.account.isin(account_filter)) + + gle = query.run(as_dict=True) + balances_within_period = frappe._dict() for d in gle: balances_within_period.setdefault(d.party, [d.debit, d.credit]) 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 ad2a9e88737..2d4e4be59dc 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -53,6 +53,7 @@ frappe.query_reports["Purchase Order Analysis"] = { label: __("Status"), fieldtype: "MultiSelectList", width: "80", + options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"], get_data: function (txt) { let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"]; let options = []; diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index f7d0d947b61..05cd88633bc 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -50,6 +50,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldname: "supplier", label: __("Supplier"), fieldtype: "MultiSelectList", + options: "Supplier", get_data: function (txt) { return frappe.db.get_link_options("Supplier", txt); }, @@ -58,6 +59,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldtype: "MultiSelectList", label: __("Supplier Quotation"), fieldname: "supplier_quotation", + options: "Supplier Quotation", default: "", get_data: function (txt) { return frappe.db.get_link_options("Supplier Quotation", txt, { docstatus: ["<", 2] }); diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 14dc038b0d6..1d4bd3b01f3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -158,7 +158,7 @@ class AccountsController(TransactionBase): self.validate_qty_is_not_zero() if ( - self.doctype in ["Sales Invoice", "Purchase Invoice"] + self.doctype in ["Sales Invoice", "Purchase Invoice", "POS Invoice"] and self.get("is_return") and self.get("update_stock") ): diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 839bd5e154f..057cac85ae6 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -285,7 +285,7 @@ def get_already_returned_items(doc): field = ( frappe.scrub(doc.doctype) + "_item" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"] + if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice", "POS Invoice"] else "dn_detail" ) data = frappe.db.sql( @@ -653,6 +653,7 @@ def get_return_against_item_fields(voucher_type): "Delivery Note": "dn_detail", "Sales Invoice": "sales_invoice_item", "Subcontracting Receipt": "subcontracting_receipt_item", + "POS Invoice": "sales_invoice_item", } return return_against_item_fields[voucher_type] diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index 5c449284ab6..50b554d2341 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -32,6 +32,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["Open", "Converted", "Quotation", "Replied"], get_data: function () { return [ { value: "Open", description: "Status" }, diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js index aac687c1413..bd19211d82a 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -57,6 +57,7 @@ frappe.query_reports["Job Card Summary"] = { label: __("Work Orders"), fieldname: "work_order", fieldtype: "MultiSelectList", + options: "Work Order", get_data: function (txt) { return frappe.db.get_link_options("Work Order", txt); }, @@ -65,6 +66,7 @@ frappe.query_reports["Job Card Summary"] = { label: __("Production Item"), fieldname: "production_item", fieldtype: "MultiSelectList", + options: "Item", get_data: function (txt) { return frappe.db.get_link_options("Item", txt); }, diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js index bde90504e67..6ace52b35e9 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js @@ -42,7 +42,7 @@ frappe.query_reports["Production Planning Report"] = { fieldname: "docnames", label: __("Document Name"), fieldtype: "MultiSelectList", - options: "Sales Order", + options: "based_on", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js index 67e6e706c59..2fa1bec92a0 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -43,6 +43,7 @@ frappe.query_reports["Work Order Summary"] = { label: __("Sales Orders"), fieldname: "sales_order", fieldtype: "MultiSelectList", + options: "Sales Order", get_data: function (txt) { return frappe.db.get_link_options("Sales Order", txt); }, @@ -51,6 +52,7 @@ frappe.query_reports["Work Order Summary"] = { label: __("Production Item"), fieldname: "production_item", fieldtype: "MultiSelectList", + options: "Item", get_data: function (txt) { return frappe.db.get_link_options("Item", txt); }, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 94e48ef135d..ab842095412 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -370,3 +370,5 @@ erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter erpnext.patches.v14_0.update_stock_uom_in_work_order_item erpnext.patches.v14_0.disable_add_row_in_gross_profit execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") +erpnext.patches.v14_0.update_posting_datetime + diff --git a/erpnext/patches/v14_0/update_posting_datetime.py b/erpnext/patches/v14_0/update_posting_datetime.py new file mode 100644 index 00000000000..cb28193b74b --- /dev/null +++ b/erpnext/patches/v14_0/update_posting_datetime.py @@ -0,0 +1,10 @@ +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE `tabStock Ledger Entry` + SET posting_datetime = timestamp(posting_date, posting_time) + """ + ) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 6fd7f7f4588..ee6b5de28cd 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -381,7 +381,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { query: "erpnext.controllers.queries.get_batch_no", }; }, - change: function () { + onchange: function () { const batch_no = this.get_value(); if (!batch_no) { this.grid_row.on_grid_fields_dict.available_qty.set_value(0); diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 32c84b2538b..13d41a60961 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -88,6 +88,7 @@ function get_filters() { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["Overdue", "Unpaid", "Completed", "Partly Paid"], width: 100, get_data: function (txt) { let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"]; 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 25089c4b870..672d28209cc 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js @@ -48,6 +48,7 @@ frappe.query_reports["Sales Order Analysis"] = { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"], width: "80", get_data: function (txt) { let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]; diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 6ba2f0ac051..7f406e5d8c3 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -262,6 +262,11 @@ frappe.ui.form.on("Stock Reconciliation Item", { qty: function (frm, cdt, cdn) { frm.events.set_amount_quantity(frm, cdt, cdn); + + let row = locals[cdt][cdn]; + if (!row.qty) { + frappe.model.set_value(cdt, cdn, "serial_no", ""); + } }, valuation_rate: function (frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index adc67c6bb44..d1b81aa2ebe 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -250,7 +250,7 @@ class StockReconciliation(StockController): validate_is_stock_item(item_code, item.is_stock_item) # item should not be serialized - if item.has_serial_no and not row.serial_no and not item.serial_no_series: + if item.has_serial_no and row.qty and not row.serial_no and not item.serial_no_series: raise frappe.ValidationError( _("Serial no(s) required for serialized item {0}").format(item_code) ) diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.js b/erpnext/stock/report/item_shortage_report/item_shortage_report.js index 5a6a54734a4..4cacfdcccb6 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.js +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.js @@ -17,6 +17,7 @@ frappe.query_reports["Item Shortage Report"] = { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "MultiSelectList", + options: "Warehouse", width: "100", get_data: function (txt) { return frappe.db.get_link_options("Warehouse", txt); diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index f951ac019f5..636d620fad3 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -631,4 +631,4 @@ def get_combine_datetime(posting_date, posting_time): if isinstance(posting_time, datetime.timedelta): posting_time = (datetime.datetime.min + posting_time).time() - return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0) + return datetime.datetime.combine(posting_date, posting_time) diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py index 57750a56f6f..15af9f0f014 100644 --- a/erpnext/templates/utils.py +++ b/erpnext/templates/utils.py @@ -3,6 +3,7 @@ import frappe +from frappe.utils import escape_html @frappe.whitelist(allow_guest=True) @@ -11,6 +12,8 @@ def send_message(sender, message, subject="Website Query"): website_send_message(sender, message, subject) + message = escape_html(message) + lead = customer = None customer = frappe.db.sql( """select distinct dl.link_name from `tabDynamic Link` dl