diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7375ca14727..13cbf66a5af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ exclude: 'node_modules|.git' -default_stages: [commit] +default_stages: [pre-commit] fail_fast: false diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 77eeba5cf3a..00c9a337499 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -3300,26 +3300,25 @@ def set_paid_amount_and_received_amount( if party_account_currency == bank.account_currency: paid_amount = received_amount = abs(outstanding_amount) else: - company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency") - if payment_type == "Receive": - paid_amount = abs(outstanding_amount) - if bank_amount: - received_amount = bank_amount - else: - if bank and company_currency != bank.account_currency: - received_amount = paid_amount / doc.get("conversion_rate", 1) - else: - received_amount = paid_amount * doc.get("conversion_rate", 1) + # settings if it is for receive + paid_amount = abs(outstanding_amount) + if bank_amount: + received_amount = bank_amount else: - received_amount = abs(outstanding_amount) - if bank_amount: - paid_amount = bank_amount + company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency") + if bank and company_currency != bank.account_currency: + # doc currency can be different from bank currency + posting_date = doc.get("posting_date") or doc.get("transaction_date") + conversion_rate = get_exchange_rate( + bank.account_currency, party_account_currency, posting_date + ) + received_amount = paid_amount / conversion_rate else: - if bank and company_currency != bank.account_currency: - paid_amount = received_amount / doc.get("conversion_rate", 1) - else: - # if party account currency and bank currency is different then populate paid amount as well - paid_amount = received_amount * doc.get("conversion_rate", 1) + received_amount = paid_amount * doc.get("conversion_rate", 1) + + # if payment type is pay, then paid amount and received amount are swapped + if payment_type == "Pay": + paid_amount, received_amount = received_amount, paid_amount return paid_amount, received_amount diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 828fc30db6e..0a0be67edc2 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -37,6 +37,7 @@ "column_break_19", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break1", "rate", @@ -847,11 +848,17 @@ { "fieldname": "column_break_ciit", "fieldtype": "Column Break" + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "istable": 1, "links": [], - "modified": "2024-05-07 15:56:53.343317", + "modified": "2024-05-07 15:56:54.343317", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py index c24db1d6a03..429f340a4f5 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py @@ -39,6 +39,7 @@ class POSInvoiceItem(Document): description: DF.TextEditor discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency dn_detail: DF.Data | None enable_deferred_revenue: DF.Check expense_account: DF.Link | None diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 084a262a890..7c4ba47f9ba 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2688,13 +2688,13 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): To test if after applying discount on grand total, the grand total is calculated correctly without any rounding errors """ - invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True) + invoice = make_purchase_invoice(qty=3, rate=100, do_not_save=True, do_not_submit=True) invoice.append( "items", { "item_code": "_Test Item", - "qty": 1, - "rate": 21.39, + "qty": 3, + "rate": 50.3, }, ) invoice.append( @@ -2703,18 +2703,19 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): "charge_type": "On Net Total", "account_head": "_Test Account VAT - _TC", "description": "VAT", - "rate": 15.5, + "rate": 15, }, ) - # the grand total here will be 255.71 + # the grand total here will be 518.54 invoice.disable_rounded_total = 1 - # apply discount on grand total to adjust the grand total to 255 - invoice.discount_amount = 0.71 + # apply discount on grand total to adjust the grand total to 518 + invoice.discount_amount = 0.54 + invoice.save() - # check if grand total is 496 and not something like 254.99 due to rounding errors - self.assertEqual(invoice.grand_total, 255) + # check if grand total is 518 and not something like 517.99 due to rounding errors + self.assertEqual(invoice.grand_total, 518) def test_apply_discount_on_grand_total_with_previous_row_total_tax(self): """ diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index eb218708b8e..69d5da7b9a3 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -38,6 +38,7 @@ "column_break_30", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "sec_break2", "rate", @@ -840,7 +841,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "section_break_26", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -971,12 +972,18 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1 + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2025-03-12 16:33:12.453290", + "modified": "2025-03-12 16:33:13.453290", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py index a8f844c6c1c..96db9d66f05 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py @@ -34,6 +34,7 @@ class PurchaseInvoiceItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency enable_deferred_expense: DF.Check expense_account: DF.Link | None from_warehouse: DF.Link | None diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index e82649a25ab..008d63ae76f 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -196,7 +196,7 @@ "fieldname": "item_wise_tax_detail", "fieldtype": "Code", "hidden": 1, - "label": "Item Wise Tax Detail ", + "label": "Item Wise Tax Detail", "oldfieldname": "item_wise_tax_detail", "oldfieldtype": "Small Text", "print_hide": 1, @@ -235,10 +235,11 @@ "read_only": 1 } ], + "grid_page_length": 50, "idx": 1, "istable": 1, "links": [], - "modified": "2024-04-08 19:51:36.678551", + "modified": "2025-04-15 13:14:48.936047", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 792e1ddbdad..540f3778232 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -782,24 +782,6 @@ frappe.ui.form.on("Sales Invoice", { }; }; }, - // When multiple companies are set up. in case company name is changed set default company address - company: function (frm) { - if (frm.doc.company) { - frappe.call({ - method: "erpnext.setup.doctype.company.company.get_default_company_address", - args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" }, - debounce: 2000, - callback: function (r) { - if (r.message) { - frm.set_value("company_address", r.message); - } else { - frm.set_value("company_address", ""); - } - }, - }); - } - }, - onload: function (frm) { frm.redemption_conversion_factor = null; }, diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 3d0998851fb..a5b93eae931 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -37,6 +37,7 @@ "column_break_19", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break1", "rate", @@ -259,7 +260,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -932,6 +933,12 @@ "fieldname": "column_break_ytgd", "fieldtype": "Column Break" }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" + }, { "fieldname": "available_quantity_section", "fieldtype": "Section Break", @@ -976,7 +983,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-03-12 16:33:52.503777", + "modified": "2025-03-12 16:33:55.503777", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index 943f08871a1..65d82a4deb3 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -40,6 +40,7 @@ class SalesInvoiceItem(Document): discount_account: DF.Link | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency dn_detail: DF.Data | None enable_deferred_revenue: DF.Check expense_account: DF.Link | None diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index ad05d770314..3dfcd4463d9 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -100,7 +100,7 @@ class PartyLedgerSummaryReport: conditions.append(doctype.territory.isin(self.filters.territory)) if self.filters.get(group_field): - conditions.append(doctype.get(group_field).isin(self.filters.get(group_field))) + conditions.append(doctype[group_field].isin(self.filters.get(group_field))) if self.filters.payment_terms_template: conditions.append(doctype.payment_terms == self.filters.payment_terms_template) diff --git a/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py b/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py index ea95772af4d..4686ccd094b 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py +++ b/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py @@ -59,3 +59,33 @@ class TestSupplierLedgerSummary(FrappeTestCase, AccountsTestMixin): for field in expected: with self.subTest(field=field): self.assertEqual(report_output[0].get(field), expected.get(field)) + + def test_supplier_ledger_summary_with_filters(self): + self.create_purchase_invoice() + + supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group") + + filters = { + "company": self.company, + "from_date": today(), + "to_date": today(), + "supplier_group": supplier_group, + } + + expected = { + "party": "_Test Supplier", + "party_name": "_Test Supplier", + "opening_balance": 0, + "invoiced_amount": 300.0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 300.0, + "currency": "INR", + "supplier_name": "_Test Supplier", + } + + report_output = execute(filters)[1] + self.assertEqual(len(report_output), 1) + for field in expected: + with self.subTest(field=field): + self.assertEqual(report_output[0].get(field), expected.get(field)) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 42c6c82b19f..ee52da291b1 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.utils import getdate def execute(filters=None): @@ -33,6 +34,7 @@ def execute(filters=None): def validate_filters(filters): """Validate if dates are properly set""" + filters = frappe._dict(filters or {}) if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) @@ -68,7 +70,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ if not tax_withholding_category: tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") - rate = tax_rate_map.get(tax_withholding_category) + rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date) if net_total_map.get((voucher_type, name)): if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount @@ -435,12 +437,22 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): def get_tax_rate_map(filters): rate_map = frappe.get_all( "Tax Withholding Rate", - filters={ - "from_date": ("<=", filters.get("from_date")), - "to_date": (">=", filters.get("to_date")), - }, - fields=["parent", "tax_withholding_rate"], - as_list=1, + filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)}, + fields=["parent", "tax_withholding_rate", "from_date", "to_date"], ) - return frappe._dict(rate_map) + rate_list = frappe._dict() + + for rate in rate_map: + rate_list.setdefault(rate.parent, []).append(frappe._dict(rate)) + + return rate_list + + +def get_tax_withholding_rates(tax_withholding, posting_date): + # returns the row that matches with the fiscal year from posting date + for rate in tax_withholding: + if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date): + return rate.tax_withholding_rate + + return 0 diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py index 7515616b0b8..6eff81e7f42 100644 --- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import today +from frappe.utils import add_to_date, today from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice @@ -60,6 +60,56 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): ] self.check_expected_values(result, expected_values) + def test_date_filters_in_multiple_tax_withholding_rules(self): + create_tax_category("TDS - 3", rate=10, account="TDS - _TC", cumulative_threshold=1) + # insert new rate in same fiscal year + fiscal_year = get_fiscal_year(today(), company="_Test Company") + mid_year = add_to_date(fiscal_year[1], months=6) + tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3") + tds_doc.rates[0].to_date = mid_year + tds_doc.append( + "rates", + { + "tax_withholding_rate": 20, + "from_date": add_to_date(mid_year, days=1), + "to_date": fiscal_year[2], + "single_threshold": 1, + "cumulative_threshold": 1, + }, + ) + + tds_doc.save() + + inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_1.apply_tds = 1 + inv_1.tax_withholding_category = "TDS - 3" + inv_1.submit() + + inv_2 = make_purchase_invoice( + rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True + ) + inv_2.set_posting_time = 1 + + inv_1.apply_tds = 1 + inv_2.tax_withholding_category = "TDS - 3" + inv_2.save() + inv_2.submit() + + result = execute( + frappe._dict( + company="_Test Company", + party_type="Supplier", + from_date=fiscal_year[1], + to_date=fiscal_year[2], + ) + )[1] + + expected_values = [ + [inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500], + [inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000], + ] + self.check_expected_values(result, expected_values) + def check_expected_values(self, result, expected_values): for i in range(len(result)): voucher = frappe._dict(result[i]) 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 4473d021635..e0e0990c103 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -43,6 +43,7 @@ "column_break_28", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "sec_break2", "rate", @@ -781,7 +782,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -911,6 +912,12 @@ "fieldname": "column_break_fyqr", "fieldtype": "Column Break" }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" + }, { "allow_on_submit": 1, "depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow", @@ -927,7 +934,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-03-13 17:27:43.468602", + "modified": "2025-03-13 17:27:44.468602", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", 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 5e4ce19d340..451ea97d11f 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py @@ -37,6 +37,7 @@ class PurchaseOrderItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency expected_delivery_date: DF.Date | None expense_account: DF.Link | None fg_item: DF.Link | None diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index d7c2c3f24b1..96597bd9753 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -154,9 +154,31 @@ frappe.ui.form.on("Request for Quotation", { ); frm.page.set_inner_btn_group_as_primary(__("Create")); + + frm.add_custom_button( + __("Supplier Quotation Comparison"), + function () { + frm.trigger("show_supplier_quotation_comparison"); + }, + __("View") + ); } }, + show_supplier_quotation_comparison(frm) { + const today = new Date(); + const oneMonthAgo = new Date(today); + oneMonthAgo.setMonth(today.getMonth() - 1); + + frappe.route_options = { + company: frm.doc.company, + from_date: moment(oneMonthAgo).format("YYYY-MM-DD"), + to_date: moment(today).format("YYYY-MM-DD"), + request_for_quotation: frm.doc.name, + }; + frappe.set_route("query-report", "Supplier Quotation Comparison"); + }, + make_supplier_quotation: function (frm) { var doc = frm.doc; var dialog = new frappe.ui.Dialog({ diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index a6229b5950b..91019104949 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -32,6 +32,7 @@ "price_list_rate", "discount_percentage", "discount_amount", + "distributed_discount_amount", "col_break_price_list", "base_price_list_rate", "sec_break1", @@ -565,13 +566,19 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-17 12:25:26.235367", + "modified": "2024-06-02 06:22:18.864822", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py index d2f4a59930b..a51b9500fd8 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py @@ -26,6 +26,7 @@ class SupplierQuotationItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency expected_delivery_date: DF.Date | None image: DF.Attach | None is_free_item: DF.Check diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 46418ca8a63..f576bc91541 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1841,8 +1841,11 @@ class AccountsController(TransactionBase): and self.get("discount_amount") and self.get("additional_discount_account") ): - amount = item.amount - base_amount = item.base_amount + amount += item.distributed_discount_amount + base_amount += flt( + item.distributed_discount_amount * self.get("conversion_rate"), + item.precision("distributed_discount_amount"), + ) return amount, base_amount @@ -2395,13 +2398,12 @@ class AccountsController(TransactionBase): base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount") ) d.outstanding = d.payment_amount - d.base_outstanding = flt( - d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding") - ) + d.base_outstanding = d.base_payment_amount elif not d.invoice_portion: d.base_payment_amount = flt( d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount") ) + d.base_outstanding = d.base_payment_amount else: self.fetch_payment_terms_from_order( po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 670d46453f9..f3e66cba663 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -8,9 +8,10 @@ from collections import OrderedDict, defaultdict import frappe from frappe import qb, scrub from frappe.desk.reportview import get_filters_cond, get_match_cond +from frappe.permissions import has_permission from frappe.query_builder import Criterion, CustomFunction from frappe.query_builder.functions import Concat, Locate, Sum -from frappe.utils import nowdate, today, unique +from frappe.utils import cint, nowdate, today, unique from pypika import Order import erpnext @@ -20,10 +21,28 @@ from erpnext.stock.get_item_details import _get_item_tax_template # searches for active employees @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def employee_query(doctype, txt, searchfield, start, page_len, filters): +def employee_query( + doctype, + txt, + searchfield, + start, + page_len, + filters, + reference_doctype: str | None = None, + ignore_user_permissions: bool = False, +): doctype = "Employee" conditions = [] fields = get_fields(doctype, ["name", "employee_name"]) + ignore_permissions = False + + if reference_doctype and ignore_user_permissions: + ignore_permissions = has_ignored_field(reference_doctype, doctype) and has_permission( + doctype, + ptype="select" if frappe.only_has_select_perm(doctype) else "read", + ) + + mcond = "" if ignore_permissions else get_match_cond(doctype) return frappe.db.sql( """select {fields} from `tabEmployee` @@ -42,13 +61,32 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): "fields": ", ".join(fields), "key": searchfield, "fcond": get_filters_cond(doctype, filters, conditions), - "mcond": get_match_cond(doctype), + "mcond": mcond, } ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, ) +def has_ignored_field(reference_doctype, doctype): + meta = frappe.get_meta(reference_doctype) + for field in meta.fields: + if not field.ignore_user_permissions: + continue + if field.fieldtype == "Link" and field.options == doctype: + return True + elif field.fieldtype == "Dynamic Link": + options = meta.get_link_doctype(field.fieldname) + if not options: + continue + if isinstance(options, str): + options = options.split("\n") + if doctype in options or "DocType" in options: + return True + + return False + + # searches for leads which are not converted @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -921,7 +959,7 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters): return frappe.get_all( "UOM", - filters={"name": ["like", f"%{txt}%"]}, + filters={"name": ["like", f"%{txt}%"], "enabled": 1}, fields=["name"], limit_start=start, limit_page_length=page_len, diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index e1e4c4ce8f2..cd92a102234 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -84,8 +84,8 @@ status_map = { "Delivery Note": [ ["Draft", None], ["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"], - ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], + ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 10799744d41..fbc98a1a2f4 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -988,7 +988,13 @@ class StockController(AccountsController): def update_billing_percentage(self, update_modified=True): target_ref_field = "amount" if self.doctype == "Delivery Note": - target_ref_field = "amount - (returned_qty * rate)" + total_amount = total_returned = 0 + for item in self.items: + total_amount += flt(item.amount) + total_returned += flt(item.returned_qty * item.rate) + + if total_returned < total_amount: + target_ref_field = "(amount - (returned_qty * rate))" self._update_percent_field( { @@ -1153,6 +1159,12 @@ class StockController(AccountsController): if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]: return + self.__inter_company_reference = ( + self.get("inter_company_reference") + if self.doctype == "Purchase Invoice" + else self.get("inter_company_invoice_reference") + ) + item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty() if not item_wise_transfer_qty: return @@ -1182,15 +1194,11 @@ class StockController(AccountsController): bold(key[1]), bold(flt(transferred_qty, precision)), bold(parent_doctype), - get_link_to_form(parent_doctype, self.get("inter_company_reference")), + get_link_to_form(parent_doctype, self.__inter_company_reference), ) ) def get_item_wise_inter_transfer_qty(self): - reference_field = "inter_company_reference" - if self.doctype == "Purchase Invoice": - reference_field = "inter_company_invoice_reference" - parent_doctype = { "Purchase Receipt": "Delivery Note", "Purchase Invoice": "Sales Invoice", @@ -1210,7 +1218,7 @@ class StockController(AccountsController): child_tab.item_code, child_tab.qty, ) - .where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1)) + .where((parent_tab.name == self.__inter_company_reference) & (parent_tab.docstatus == 1)) ) data = query.run(as_dict=True) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 955c9261031..5543129d323 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -377,20 +377,22 @@ class calculate_taxes_and_totals: self._calculate() def calculate_taxes(self): - self.grand_total_diff = 0 + doc = self.doc + if not doc.get("taxes"): + return # maintain actual tax rate based on idx actual_tax_dict = dict( [ [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] - for tax in self.doc.get("taxes") + for tax in doc.taxes if tax.charge_type == "Actual" ] ) for n, item in enumerate(self._items): item_tax_map = self._load_item_tax_rate(item.item_tax_rate) - for i, tax in enumerate(self.doc.get("taxes")): + for i, tax in enumerate(doc.taxes): # tax_amount represents the amount of tax for the current step current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map) if frappe.flags.round_row_wise_tax: @@ -425,30 +427,39 @@ class calculate_taxes_and_totals: tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount) else: tax.grand_total_for_current_item = flt( - self.doc.get("taxes")[i - 1].grand_total_for_current_item + current_tax_amount + doc.taxes[i - 1].grand_total_for_current_item + current_tax_amount ) - # set precision in the last item iteration - if n == len(self._items) - 1: - self.round_off_totals(tax) - self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) + discount_amount_applied = self.discount_amount_applied + if doc.apply_discount_on == "Grand Total" and ( + discount_amount_applied or doc.discount_amount or doc.additional_discount_percentage + ): + tax_amount_precision = doc.taxes[0].precision("tax_amount") - self.round_off_base_values(tax) - self.set_cumulative_total(i, tax) + for i, tax in enumerate(doc.taxes): + if discount_amount_applied: + tax.tax_amount_after_discount_amount = flt( + tax.tax_amount_after_discount_amount, tax_amount_precision + ) - self._set_in_company_currency(tax, ["total"]) + self.set_cumulative_total(i, tax) - # adjust Discount Amount loss in last tax iteration - if ( - i == (len(self.doc.get("taxes")) - 1) - and self.discount_amount_applied - and self.doc.discount_amount - and self.doc.apply_discount_on == "Grand Total" - ): - self.grand_total_diff = flt( - self.doc.grand_total - flt(self.doc.discount_amount) - tax.total, - self.doc.precision("rounding_adjustment"), - ) + if not discount_amount_applied: + self.grand_total_for_distributing_discount = doc.taxes[-1].total + else: + self.grand_total_diff = flt( + self.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[-1].total, + doc.precision("grand_total"), + ) + + for i, tax in enumerate(doc.taxes): + self.round_off_totals(tax) + self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) + + self.round_off_base_values(tax) + self.set_cumulative_total(i, tax) + + self._set_in_company_currency(tax, ["total"]) def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax): # if just for valuation, do not add the tax amount in total @@ -571,16 +582,20 @@ class calculate_taxes_and_totals: if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")): self.grand_total_diff = diff + else: + self.grand_total_diff = 0 def calculate_totals(self): + grand_total_diff = getattr(self, "grand_total_diff", 0) + if self.doc.get("taxes"): - self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff + self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff else: self.doc.grand_total = flt(self.doc.net_total) if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( - self.doc.grand_total - self.doc.net_total - self.grand_total_diff, + self.doc.grand_total - self.doc.net_total - grand_total_diff, self.doc.precision("total_taxes_and_charges"), ) else: @@ -695,6 +710,9 @@ class calculate_taxes_and_totals: adjusted_net_amount = item.net_amount - distributed_amount expected_net_total += adjusted_net_amount item.net_amount = flt(adjusted_net_amount, item.precision("net_amount")) + item.distributed_discount_amount = flt( + distributed_amount, item.precision("distributed_discount_amount") + ) net_total += item.net_amount # discount amount rounding adjustment @@ -704,6 +722,10 @@ class calculate_taxes_and_totals: item.net_amount = flt( item.net_amount + rounding_difference, item.precision("net_amount") ) + item.distributed_discount_amount = flt( + distributed_amount + rounding_difference, + item.precision("distributed_discount_amount"), + ) net_total += rounding_difference item.net_rate = ( @@ -718,7 +740,8 @@ class calculate_taxes_and_totals: self.doc.base_discount_amount = 0 def get_total_for_discount_amount(self): - if self.doc.apply_discount_on == "Net Total": + doc = self.doc + if doc.apply_discount_on == "Net Total" or not doc.get("taxes"): return self.doc.net_total total_actual_tax = 0 @@ -738,7 +761,7 @@ class calculate_taxes_and_totals: "cumulative_tax_amount": total_actual_tax, } - for tax in self.doc.get("taxes"): + for tax in doc.taxes: if tax.charge_type in ["Actual", "On Item Quantity"]: update_actual_tax_dict(tax, tax.tax_amount) continue @@ -757,7 +780,7 @@ class calculate_taxes_and_totals: ) update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100) - return self.doc.grand_total - total_actual_tax + return getattr(self, "grand_total_for_distributing_discount", doc.grand_total) - total_actual_tax def calculate_total_advance(self): if not self.doc.docstatus.is_cancelled(): diff --git a/erpnext/controllers/tests/test_distributed_discount.py b/erpnext/controllers/tests/test_distributed_discount.py new file mode 100644 index 00000000000..e4dbdbb1480 --- /dev/null +++ b/erpnext/controllers/tests/test_distributed_discount.py @@ -0,0 +1,61 @@ +from frappe.tests.utils import FrappeTestCase + +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + +class TestTaxesAndTotals(AccountsTestMixin, FrappeTestCase): + def test_distributed_discount_amount(self): + so = make_sales_order(do_not_save=1) + so.apply_discount_on = "Net Total" + so.discount_amount = 100 + so.items[0].qty = 5 + so.items[0].rate = 100 + so.append("items", so.items[0].as_dict()) + so.items[1].qty = 5 + so.items[1].rate = 200 + so.save() + + calculate_taxes_and_totals(so) + + self.assertAlmostEqual(so.items[0].distributed_discount_amount, 33.33, places=2) + self.assertAlmostEqual(so.items[1].distributed_discount_amount, 66.67, places=2) + self.assertAlmostEqual(so.items[0].net_amount, 466.67, places=2) + self.assertAlmostEqual(so.items[1].net_amount, 933.33, places=2) + self.assertEqual(so.total, 1500) + self.assertEqual(so.net_total, 1400) + self.assertEqual(so.grand_total, 1400) + + def test_distributed_discount_amount_with_taxes(self): + so = make_sales_order(do_not_save=1) + so.apply_discount_on = "Grand Total" + so.discount_amount = 100 + so.items[0].qty = 5 + so.items[0].rate = 100 + so.append("items", so.items[0].as_dict()) + so.items[1].qty = 5 + so.items[1].rate = 200 + so.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "included_in_print_rate": True, + "rate": 10, + }, + ) + so.save() + + calculate_taxes_and_totals(so) + + # like in test_distributed_discount_amount, but reduced by the included tax + self.assertAlmostEqual(so.items[0].distributed_discount_amount, 33.33 / 1.1, places=2) + self.assertAlmostEqual(so.items[1].distributed_discount_amount, 66.67 / 1.1, places=2) + self.assertAlmostEqual(so.items[0].net_amount, 466.67 / 1.1, places=2) + self.assertAlmostEqual(so.items[1].net_amount, 933.33 / 1.1, places=2) + self.assertEqual(so.total, 1500) + self.assertAlmostEqual(so.net_total, 1272.73, places=2) + self.assertEqual(so.grand_total, 1400) diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 0ef108e5f7b..ba380a1a3a1 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -2,6 +2,9 @@ import unittest from functools import partial import frappe +from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.core.doctype.user_permission.user_permission import add_user_permissions +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.controllers import queries @@ -81,3 +84,54 @@ class TestQueries(unittest.TestCase): def test_default_uoms(self): self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10) + + def test_employee_query_with_user_permissions(self): + # party field is a dynamic link field in Payment Entry doctype with ignore_user_permissions=0 + ps = make_property_setter( + doctype="Payment Entry", + fieldname="party", + property="ignore_user_permissions", + value=1, + property_type="Check", + ) + ps.save() + + user = create_user("test_employee_query@example.com", ("Accounts User", "HR User")) + add_user_permissions( + { + "user": user.name, + "doctype": "Employee", + "docname": "_T-Employee-00001", + "is_default": 1, + "apply_to_all_doctypes": 1, + "applicable_doctypes": [], + "hide_descendants": 0, + } + ) + + frappe.reload_doc("accounts", "doctype", "payment entry") + + frappe.set_user(user.name) + params = { + "doctype": "Employee", + "txt": "", + "searchfield": "name", + "start": 0, + "page_len": 20, + "filters": None, + "reference_doctype": "Payment Entry", + "ignore_user_permissions": 1, + } + + result = queries.employee_query(**params) + self.assertGreater(len(result), 1) + + ps.delete(ignore_permissions=1, force=1, delete_permanently=1) + frappe.reload_doc("accounts", "doctype", "payment entry") + frappe.clear_cache() + + # only one employee should be returned even though ignore_user_permissions is passed as 1 + result = queries.employee_query(**params) + self.assertEqual(len(result), 1) + + frappe.set_user("Administrator") diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index cfd3f789e78..681abc8ddde 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -4,6 +4,7 @@ import copy import json +from collections import defaultdict import frappe from frappe import _, msgprint @@ -913,8 +914,7 @@ class ProductionPlan(Document): if material_request_list: material_request_list = [ - f"""{m.name}""" - for m in material_request_list + get_link_to_form("Material Request", m.name) for m in material_request_list ] msgprint(_("{0} created").format(comma_and(material_request_list))) else: @@ -925,6 +925,7 @@ class ProductionPlan(Document): "Fetch sub assembly items and optionally combine them." self.sub_assembly_items = [] sub_assembly_items_store = [] # temporary store to process all subassembly items + bin_details = frappe._dict() for row in self.po_items: if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse: @@ -939,6 +940,8 @@ class ProductionPlan(Document): bom_data = [] get_sub_assembly_items( + [item.production_item for item in sub_assembly_items_store], + bin_details, row.bom_no, bom_data, row.planned_qty, @@ -1528,10 +1531,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d so_item_details = frappe._dict() - sub_assembly_items = {} + sub_assembly_items = defaultdict(int) if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"): for d in doc.get("sub_assembly_items"): - sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty")) + sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("qty") for data in po_items: if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): @@ -1560,6 +1563,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d item_details = {} if doc.get("sub_assembly_items"): item_details = get_raw_materials_of_sub_assembly_items( + so_item_details[doc.get("sales_order")].keys() if so_item_details else [], item_details, company, bom_no, @@ -1737,6 +1741,8 @@ def get_item_data(item_code): def get_sub_assembly_items( + sub_assembly_items, + bin_details, bom_no, bom_data, to_produce_qty, @@ -1751,25 +1757,27 @@ def get_sub_assembly_items( parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) - bin_details = frappe._dict() - if skip_available_sub_assembly_item: - bin_details = get_bin_details(d, company, for_warehouse=warehouse) + if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items: + bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) - for _bin_dict in bin_details: + for _bin_dict in bin_details[d.item_code]: if _bin_dict.projected_qty > 0: - if _bin_dict.projected_qty > stock_qty: + if _bin_dict.projected_qty >= stock_qty: + _bin_dict.projected_qty -= stock_qty stock_qty = 0 continue else: stock_qty = stock_qty - _bin_dict.projected_qty elif warehouse: - bin_details = get_bin_details(d, company, for_warehouse=warehouse) + bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) if stock_qty > 0: bom_data.append( frappe._dict( { - "actual_qty": bin_details[0].get("actual_qty", 0) if bin_details else 0, + "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) + if bin_details.get(d.item_code) + else 0, "parent_item_code": parent_item_code, "description": d.description, "production_item": d.item_code, @@ -1787,6 +1795,8 @@ def get_sub_assembly_items( if d.value: get_sub_assembly_items( + sub_assembly_items, + bin_details, d.value, bom_data, stock_qty, @@ -1866,7 +1876,13 @@ def get_non_completed_production_plans(): def get_raw_materials_of_sub_assembly_items( - item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 + existing_sub_assembly_items, + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=1, ): bei = frappe.qb.DocType("BOM Item") bom = frappe.qb.DocType("BOM") @@ -1910,12 +1926,13 @@ def get_raw_materials_of_sub_assembly_items( for item in items: key = (item.item_code, item.bom_no) - if item.bom_no and key not in sub_assembly_items: + if (item.bom_no and key not in sub_assembly_items) or (item.item_code in existing_sub_assembly_items): continue if item.bom_no: planned_qty = flt(sub_assembly_items[key]) get_raw_materials_of_sub_assembly_items( + existing_sub_assembly_items, item_details, company, item.bom_no, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index c7228823bed..e5b60c7a4e6 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1635,6 +1635,64 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(row.production_item, sf_item) self.assertEqual(row.qty, 5.0) + def test_calculation_of_sub_assembly_items(self): + make_item("Sub Assembly Item ", properties={"is_stock_item": 1}) + make_item("RM Item 1", properties={"is_stock_item": 1}) + make_item("RM Item 2", properties={"is_stock_item": 1}) + make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"]) + make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"]) + make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"]) + + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + make_stock_entry( + item_code="Sub Assembly Item", + qty=80, + purpose="Material Receipt", + to_warehouse="_Test Warehouse - _TC", + ) + make_stock_entry( + item_code="RM Item 1", qty=90, purpose="Material Receipt", to_warehouse="_Test Warehouse - _TC" + ) + + plan = create_production_plan( + skip_available_sub_assembly_item=1, + sub_assembly_warehouse="_Test Warehouse - _TC", + warehouse="_Test Warehouse - _TC", + item_code="_Test FG Item", + skip_getting_mr_items=1, + planned_qty=100, + do_not_save=1, + ) + plan.get_items_from = "" + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": "_Test FG Item 2", + "bom_no": frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), + "planned_qty": 50, + "planned_start_date": now_datetime(), + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ) + plan.save() + + plan.get_sub_assembly_items() + + self.assertEqual(plan.sub_assembly_items[0].qty, 20) + self.assertEqual(plan.sub_assembly_items[1].qty, 50) + + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_items_for_material_requests, + ) + + mr_items = get_items_for_material_requests(plan.as_dict()) + + self.assertEqual(mr_items[0].get("quantity"), 80) + self.assertEqual(mr_items[1].get("quantity"), 70) + def create_production_plan(**args): """ diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 4bc787f85ff..66b348e9eeb 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -342,12 +342,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } calculate_taxes() { + const doc = this.frm.doc; + if (!doc.taxes?.length) return; + var me = this; - this.grand_total_diff = 0; var actual_tax_dict = {}; // maintain actual tax rate based on idx - $.each(this.frm.doc["taxes"] || [], function(i, tax) { + $.each(doc.taxes, function(i, tax) { if (tax.charge_type == "Actual") { actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax)); } @@ -355,7 +357,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { $.each(this.frm._items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); - $.each(me.frm.doc["taxes"] || [], function(i, tax) { + $.each(doc.taxes, function(i, tax) { // tax_amount represents the amount of tax for the current step var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map); if (frappe.flags.round_row_wise_tax) { @@ -400,29 +402,40 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { tax.grand_total_for_current_item = flt(me.frm.doc["taxes"][i-1].grand_total_for_current_item + current_tax_amount); } - - // set precision in the last item iteration - if (n == me.frm._items.length - 1) { - me.round_off_totals(tax); - me.set_in_company_currency(tax, - ["tax_amount", "tax_amount_after_discount_amount"]); - - me.round_off_base_values(tax); - - // in tax.total, accumulate grand total for each item - me.set_cumulative_total(i, tax); - - me.set_in_company_currency(tax, ["total"]); - - // adjust Discount Amount loss in last tax iteration - if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied - && me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) { - me.grand_total_diff = flt(me.frm.doc.grand_total - - flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment")); - } - } }); }); + + const discount_amount_applied = this.discount_amount_applied; + if (doc.apply_discount_on === "Grand Total" && (discount_amount_applied || doc.discount_amount || doc.additional_discount_percentage)) { + const tax_amount_precision = precision("tax_amount", doc.taxes[0]); + + for (const [i, tax] of doc.taxes.entries()) { + if (discount_amount_applied) + tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, tax_amount_precision); + + this.set_cumulative_total(i, tax); + } + + if (!this.discount_amount_applied) { + this.grand_total_for_distributing_discount = doc.taxes[doc.taxes.length - 1].total; + } else { + this.grand_total_diff = flt( + this.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[doc.taxes.length - 1].total, precision("grand_total")); + } + } + + for (const [i, tax] of doc.taxes.entries()) { + me.round_off_totals(tax); + me.set_in_company_currency(tax, + ["tax_amount", "tax_amount_after_discount_amount"]); + + me.round_off_base_values(tax); + + // in tax.total, accumulate grand total for each tax + me.set_cumulative_total(i, tax); + + me.set_in_company_currency(tax, ["total"]); + } } set_cumulative_total(row_idx, tax) { @@ -571,10 +584,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_totals() { // Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency - var me = this; - var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0; + const me = this; + const tax_count = this.frm.doc.taxes?.length; + const grand_total_diff = this.grand_total_diff || 0; + this.frm.doc.grand_total = flt(tax_count - ? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff + ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff : this.frm.doc.net_total); if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { @@ -606,7 +621,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total - - this.grand_total_diff, precision("total_taxes_and_charges")); + - grand_total_diff, precision("total_taxes_and_charges")); this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]); @@ -729,8 +744,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } get_total_for_discount_amount() { - if(this.frm.doc.apply_discount_on == "Net Total") - return this.frm.doc.net_total; + const doc = this.frm.doc; + + if (doc.apply_discount_on == "Net Total" || !doc.taxes?.length) + return doc.net_total; let total_actual_tax = 0.0; let actual_taxes_dict = {}; @@ -745,7 +762,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }; } - $.each(this.frm.doc["taxes"] || [], function(i, tax) { + doc.taxes.forEach(tax => { if (["Actual", "On Item Quantity"].includes(tax.charge_type)) { update_actual_taxes_dict(tax, tax.tax_amount); return; @@ -760,7 +777,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100); }); - return this.frm.doc.grand_total - total_actual_tax; + return (this.grand_total_for_distributing_discount || doc.grand_total) - total_actual_tax; } calculate_total_advance(update_paid_amount) { diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 3879b38aa13..2ced08ed5d2 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -240,13 +240,16 @@ erpnext.setup.fiscal_years = { Afghanistan: ["12-21", "12-20"], Australia: ["07-01", "06-30"], Bangladesh: ["07-01", "06-30"], - Canada: ["04-01", "03-31"], "Costa Rica": ["10-01", "09-30"], Egypt: ["07-01", "06-30"], + Ethiopia: ["07-08", "07-07"], "Hong Kong": ["04-01", "03-31"], India: ["04-01", "03-31"], Iran: ["06-23", "06-22"], + Kenya: ["07-01", "06-30"], + Malaysia: ["07-01", "06-30"], Myanmar: ["04-01", "03-31"], + Nepal: ["07-16", "07-15"], "New Zealand": ["04-01", "03-31"], Pakistan: ["07-01", "06-30"], Singapore: ["04-01", "03-31"], diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 36c0f1b51ae..68bf11de5d8 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -77,35 +77,34 @@ erpnext.accounts.dimensions = { }, update_dimension(frm, doctype) { - if (this.accounting_dimensions) { - this.accounting_dimensions.forEach((dimension) => { - if (frm.is_new()) { - if ( - frm.doc.company && - Object.keys(this.default_dimensions || {}).length > 0 && - this.default_dimensions[frm.doc.company] - ) { - let default_dimension = - this.default_dimensions[frm.doc.company][dimension["fieldname"]]; + if ( + !this.accounting_dimensions || + !frm.is_new() || + !frm.doc.company || + !this.default_dimensions?.[frm.doc.company] + ) + return; - if (default_dimension) { - if (frappe.meta.has_field(doctype, dimension["fieldname"])) { - frm.set_value(dimension["fieldname"], default_dimension); - } - - $.each(frm.doc.items || frm.doc.accounts || [], function (i, row) { - frappe.model.set_value( - row.doctype, - row.name, - dimension["fieldname"], - default_dimension - ); - }); - } - } - } - }); + // don't set default dimensions if any of the dimension is already set due to mapping + if (frm.doc.__onload?.load_after_mapping) { + for (const dimension of this.accounting_dimensions) { + if (frm.doc[dimension["fieldname"]]) return; + } } + + this.accounting_dimensions.forEach((dimension) => { + const default_dimension = this.default_dimensions[frm.doc.company][dimension["fieldname"]]; + + if (!default_dimension) return; + + if (frappe.meta.has_field(doctype, dimension["fieldname"])) { + frm.set_value(dimension["fieldname"], default_dimension); + } + + (frm.doc.items || frm.doc.accounts || []).forEach((row) => { + frappe.model.set_value(row.doctype, row.name, dimension["fieldname"], default_dimension); + }); + }); }, copy_dimension_from_first_row(frm, cdt, cdn, fieldname) { diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index bbb2a88e629..ffff536f068 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -111,6 +111,33 @@ erpnext.sales_common = { this.toggle_editable_price_list_rate(); } + company() { + super.company(); + this.set_default_company_address(); + } + + set_default_company_address() { + if (!frappe.meta.has_field(this.frm.doc.doctype, "company_address")) return; + var me = this; + if (this.frm.doc.company) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: { + name: this.frm.doc.company, + existing_address: this.frm.doc.company_address || "", + }, + debounce: 2000, + callback: function (r) { + if (r.message) { + me.frm.set_value("company_address", r.message); + } else { + me.frm.set_value("company_address", ""); + } + }, + }); + } + } + customer() { var me = this; erpnext.utils.get_party_details(this.frm, null, null, function () { diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 9dd65a8b4f8..2818b49913d 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -38,6 +38,7 @@ "column_break_18", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break1", "rate", @@ -238,7 +239,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -668,6 +669,12 @@ "print_hide": 1, "read_only": 1 }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" + }, { "fieldname": "available_quantity_section", "fieldtype": "Section Break", @@ -691,7 +698,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-12-12 13:49:17.765883", + "modified": "2024-12-12 13:49:18.765883", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.py b/erpnext/selling/doctype/quotation_item/quotation_item.py index 7d68eaf07ba..bbdd8643593 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.py +++ b/erpnext/selling/doctype/quotation_item/quotation_item.py @@ -33,6 +33,7 @@ class QuotationItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency gross_profit: DF.Currency has_alternative_item: DF.Check image: DF.Attach | None diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index de4053458e4..400acd3c6f1 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -164,27 +164,6 @@ frappe.ui.form.on("Sales Order", { ); }, - // When multiple companies are set up. in case company name is changed set default company address - company: function (frm) { - if (frm.doc.company) { - frappe.call({ - method: "erpnext.setup.doctype.company.company.get_default_company_address", - args: { - name: frm.doc.company, - existing_address: frm.doc.company_address || "", - }, - debounce: 2000, - callback: function (r) { - if (r.message) { - frm.set_value("company_address", r.message); - } else { - frm.set_value("company_address", ""); - } - }, - }); - } - }, - onload: function (frm) { if (!frm.doc.transaction_date) { frm.set_value("transaction_date", frappe.datetime.get_today()); diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index ea1a646ba2d..21054a9d81b 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -40,6 +40,7 @@ "column_break_19", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break_simple1", "rate", @@ -287,7 +288,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -913,6 +914,12 @@ "print_hide": 1, "report_hide": 1 }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" + }, { "allow_on_submit": 1, "fieldname": "company_total_stock", @@ -964,7 +971,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-02-28 09:45:43.934947", + "modified": "2025-02-28 09:45:44.934947", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 2fa06ac7299..731cff665da 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -40,6 +40,7 @@ class SalesOrderItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency ensure_delivery_based_on_produced_serial_no: DF.Check grant_commission: DF.Check gross_profit: DF.Currency diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index dee052912ff..d67ad067c26 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -188,6 +188,7 @@ erpnext.PointOfSale.ItemCart = class { await me.events.checkout(); me.toggle_checkout_btn(false); + me.disable_customer_selection(); me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); }); @@ -195,6 +196,7 @@ erpnext.PointOfSale.ItemCart = class { this.$totals_section.on("click", ".edit-cart-btn", () => { this.events.edit_cart(); this.toggle_checkout_btn(true); + me.enable_customer_selection(); }); this.$component.on("click", ".add-discount-wrapper", () => { @@ -698,6 +700,25 @@ erpnext.PointOfSale.ItemCart = class { } } + disable_customer_selection() { + this.$customer_section.find(".reset-customer-btn").css("visibility", "hidden"); + this.$customer_section.off("click", ".customer-display"); + this.$customer_section.off("click", ".reset-customer-btn"); + } + + enable_customer_selection() { + this.$customer_section.find(".reset-customer-btn").css("visibility", "visible"); + this.$customer_section.on("click", ".customer-display", (e) => { + if ($(e.target).closest(".reset-customer-btn").length) return; + + const show = this.$cart_container.is(":visible"); + this.toggle_customer_info(show); + }); + this.$customer_section.on("click", ".reset-customer-btn", () => { + this.reset_customer_selector(); + }); + } + highlight_checkout_btn(toggle) { if (toggle) { this.$add_discount_elem.css("display", "flex"); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8ae843abc24..e5f26073a33 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -282,6 +282,7 @@ class Company(NestedSet): frappe.clear_cache() def create_default_warehouses(self): + parent_warehouse = None for wh_detail in [ {"warehouse_name": _("All Warehouses"), "is_group": 1}, {"warehouse_name": _("Stores"), "is_group": 0}, @@ -289,24 +290,31 @@ class Company(NestedSet): {"warehouse_name": _("Finished Goods"), "is_group": 0}, {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}, ]: - if not frappe.db.exists("Warehouse", "{} - {}".format(wh_detail["warehouse_name"], self.abbr)): - warehouse = frappe.get_doc( - { - "doctype": "Warehouse", - "warehouse_name": wh_detail["warehouse_name"], - "is_group": wh_detail["is_group"], - "company": self.name, - "parent_warehouse": "{} - {}".format(_("All Warehouses"), self.abbr) - if not wh_detail["is_group"] - else "", - "warehouse_type": wh_detail["warehouse_type"] - if "warehouse_type" in wh_detail - else None, - } - ) - warehouse.flags.ignore_permissions = True - warehouse.flags.ignore_mandatory = True - warehouse.insert() + if frappe.db.exists( + "Warehouse", + { + "warehouse_name": wh_detail["warehouse_name"], + "company": self.name, + }, + ): + continue + + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": wh_detail["warehouse_name"], + "is_group": wh_detail["is_group"], + "company": self.name, + "parent_warehouse": parent_warehouse, + "warehouse_type": wh_detail.get("warehouse_type"), + } + ) + warehouse.flags.ignore_permissions = True + warehouse.flags.ignore_mandatory = True + warehouse.insert() + + if wh_detail["is_group"]: + parent_warehouse = warehouse.name def create_default_accounts(self): from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 93af08d365f..41bc41d5d34 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -85,20 +85,21 @@ class Employee(NestedSet): self.reset_employee_emails_cache() def update_user_permissions(self): - if not self.create_user_permission: - return - if not has_permission("User Permission", ptype="write", raise_exception=False): + if not has_permission("User Permission", ptype="write") or ( + not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission") + ): return employee_user_permission_exists = frappe.db.exists( "User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id} ) - if employee_user_permission_exists: - return - - add_user_permission("Employee", self.name, self.user_id) - add_user_permission("Company", self.company, self.user_id) + if employee_user_permission_exists and not self.create_user_permission: + remove_user_permission("Employee", self.name, self.user_id) + remove_user_permission("Company", self.company, self.user_id) + elif not employee_user_permission_exists and self.create_user_permission: + add_user_permission("Employee", self.name, self.user_id) + add_user_permission("Company", self.company, self.user_id) def update_user(self): # add employee role if missing diff --git a/erpnext/stock/doctype/bin/bin.js b/erpnext/stock/doctype/bin/bin.js index 02ff8b62396..c725b691db4 100644 --- a/erpnext/stock/doctype/bin/bin.js +++ b/erpnext/stock/doctype/bin/bin.js @@ -2,5 +2,20 @@ // For license information, please see license.txt frappe.ui.form.on("Bin", { - refresh: function (frm) {}, + refresh(frm) { + frm.trigger("recalculate_bin_quantity"); + }, + + recalculate_bin_quantity(frm) { + frm.add_custom_button(__("Recalculate Bin Qty"), () => { + frappe.call({ + method: "recalculate_qty", + freeze: true, + doc: frm.doc, + callback: function (r) { + frappe.show_alert(__("Bin Qty Recalculated"), 2); + }, + }); + }); + }, }); diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index d3de1897633..338fd863ffc 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -35,6 +35,28 @@ class Bin(Document): warehouse: DF.Link # end: auto-generated types + @frappe.whitelist() + def recalculate_qty(self): + from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production + from erpnext.stock.stock_balance import ( + get_indented_qty, + get_ordered_qty, + get_planned_qty, + get_reserved_qty, + ) + + self.actual_qty = get_actual_qty(self.item_code, self.warehouse) + self.planned_qty = get_planned_qty(self.item_code, self.warehouse) + self.indented_qty = get_indented_qty(self.item_code, self.warehouse) + self.ordered_qty = get_ordered_qty(self.item_code, self.warehouse) + self.reserved_qty = get_reserved_qty(self.item_code, self.warehouse) + self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) + + self.update_reserved_qty_for_sub_contracting(update_qty=False) + self.update_reserved_qty_for_production_plan(skip_project_qty_update=True, update_qty=False) + self.set_projected_qty() + self.save() + def before_save(self): if self.get("__islocal") or not self.stock_uom: self.stock_uom = frappe.get_cached_value("Item", self.item_code, "stock_uom") @@ -52,7 +74,7 @@ class Bin(Document): - flt(self.reserved_qty_for_production_plan) ) - def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False): + def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False, update_qty=True): """Update qty reserved for production from Production Plan tables in open production plan""" from erpnext.manufacturing.doctype.production_plan.production_plan import ( @@ -68,11 +90,12 @@ class Bin(Document): self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan) - self.db_set( - "reserved_qty_for_production_plan", - flt(self.reserved_qty_for_production_plan), - update_modified=True, - ) + if update_qty: + self.db_set( + "reserved_qty_for_production_plan", + flt(self.reserved_qty_for_production_plan), + update_modified=True, + ) if not skip_project_qty_update: self.set_projected_qty() @@ -115,7 +138,9 @@ class Bin(Document): self.set_projected_qty() self.db_set("projected_qty", self.projected_qty, update_modified=True) - def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): + def update_reserved_qty_for_sub_contracting( + self, subcontract_doctype="Subcontracting Order", update_qty=True + ): # reserved qty subcontract_order = frappe.qb.DocType(subcontract_doctype) @@ -191,9 +216,11 @@ class Bin(Document): else: reserved_qty_for_sub_contract = 0 - self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) - self.set_projected_qty() - self.db_set("projected_qty", self.projected_qty, update_modified=True) + self.reserved_qty_for_sub_contract = reserved_qty_for_sub_contract + if update_qty: + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) + self.set_projected_qty() + self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_stock(self): """Update `Reserved Stock` on change in Reserved Qty of Stock Reservation Entry""" @@ -235,27 +262,10 @@ def update_qty(bin_name, args): bin_details = get_bin_details(bin_name) # actual qty is already updated by processing current voucher actual_qty = bin_details.actual_qty or 0.0 - sle = frappe.qb.DocType("Stock Ledger Entry") # actual qty is not up to date in case of backdated transaction if future_sle_exists(args, allow_force_reposting=False): - last_sle_qty = ( - frappe.qb.from_(sle) - .select(sle.qty_after_transaction) - .where( - (sle.item_code == args.get("item_code")) - & (sle.warehouse == args.get("warehouse")) - & (sle.is_cancelled == 0) - ) - .orderby(sle.posting_datetime, order=Order.desc) - .orderby(sle.creation, order=Order.desc) - .limit(1) - .run() - ) - - actual_qty = 0.0 - if last_sle_qty: - actual_qty = last_sle_qty[0][0] + actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse")) ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty")) @@ -287,3 +297,23 @@ def update_qty(bin_name, args): }, update_modified=True, ) + + +def get_actual_qty(item_code, warehouse): + sle = frappe.qb.DocType("Stock Ledger Entry") + + last_sle_qty = ( + frappe.qb.from_(sle) + .select(sle.qty_after_transaction) + .where((sle.item_code == item_code) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0)) + .orderby(sle.posting_datetime, order=Order.desc) + .orderby(sle.creation, order=Order.desc) + .limit(1) + .run() + ) + + actual_qty = 0.0 + if last_sle_qty: + actual_qty = last_sle_qty[0][0] + + return actual_qty diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 3394e0ce83a..924bc1255d3 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2521,6 +2521,28 @@ class TestDeliveryNote(FrappeTestCase): for d in bundle_data: self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + def test_delivery_note_per_billed_after_return(self): + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + + so = make_sales_order(qty=2) + dn = make_delivery_note(so.name) + dn.submit() + self.assertEqual(dn.per_billed, 0) + + si = make_sales_invoice(dn.name) + si.location = "Test Location" + si.submit() + + dn_return = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True) + dn_return.items[0].dn_detail = dn.items[0].name + dn_return.submit() + + returned = frappe.get_doc("Delivery Note", dn_return.name) + returned.update_prevdoc_status() + dn.load_from_db() + self.assertEqual(dn.per_billed, 100) + self.assertEqual(dn.per_returned, 100) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index ff64db8ee76..e951aaf1e18 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -40,6 +40,7 @@ "column_break_19", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "section_break_1", "rate", @@ -277,7 +278,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -912,6 +913,12 @@ "fieldname": "column_break_rxvc", "fieldtype": "Column Break" }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" + }, { "allow_on_submit": 1, "fieldname": "company_total_stock", @@ -934,7 +941,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-02-05 14:28:32.322181", + "modified": "2025-02-05 14:28:33.322181", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py index 716cd7d4856..7fb0e24be0b 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py @@ -37,6 +37,7 @@ class DeliveryNoteItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Float + distributed_discount_amount: DF.Currency dn_detail: DF.Data | None expense_account: DF.Link | None grant_commission: DF.Check diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 83b63e225b3..5f46ef968b0 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -107,14 +107,6 @@ frappe.ui.form.on("Material Request", { if (flt(frm.doc.per_received, precision) < 100) { frm.add_custom_button(__("Stop"), () => frm.events.update_status(frm, "Stopped")); - - if (frm.doc.material_request_type === "Purchase") { - frm.add_custom_button( - __("Purchase Order"), - () => frm.events.make_purchase_order(frm), - __("Create") - ); - } } if (flt(frm.doc.per_ordered, precision) < 100) { @@ -158,14 +150,18 @@ frappe.ui.form.on("Material Request", { } if (frm.doc.material_request_type === "Purchase") { + frm.add_custom_button( + __("Purchase Order"), + () => frm.events.make_purchase_order(frm), + __("Create") + ); + frm.add_custom_button( __("Request for Quotation"), () => frm.events.make_request_for_quotation(frm), __("Create") ); - } - if (frm.doc.material_request_type === "Purchase") { frm.add_custom_button( __("Supplier Quotation"), () => frm.events.make_supplier_quotation(frm), @@ -181,6 +177,14 @@ frappe.ui.form.on("Material Request", { ); } + if (frm.doc.material_request_type === "Subcontracting") { + frm.add_custom_button( + __("Subcontracted Purchase Order"), + () => frm.events.make_purchase_order(frm), + __("Create") + ); + } + frm.page.set_inner_btn_group_as_primary(__("Create")); } } diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 25c765bbced..8df61fe7fb8 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -357,7 +357,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2023-09-15 12:07:24.789471", + "modified": "2025-04-21 18:36:04.827917", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 59634cb9f7c..54b3e17641e 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -379,7 +379,7 @@ def set_missing_values(source, target_doc): def update_item(obj, target, source_parent): target.conversion_factor = obj.conversion_factor - qty = obj.received_qty or obj.ordered_qty + qty = obj.ordered_qty or obj.received_qty target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor target.stock_qty = target.qty * target.conversion_factor if getdate(target.schedule_date) < getdate(nowdate()): @@ -432,7 +432,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): filtered_items = args.get("filtered_children", []) child_filter = d.name in filtered_items if filtered_items else True - qty = d.received_qty or d.ordered_qty + qty = d.ordered_qty or d.received_qty return qty < d.stock_qty and child_filter @@ -721,6 +721,7 @@ def make_stock_entry(source_name, target_doc=None): "uom": "stock_uom", "job_card_item": "job_card_item", }, + "field_no_map": ["expense_account"], "postprocess": update_item, "condition": lambda doc: ( flt(doc.ordered_qty, doc.precision("ordered_qty")) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index e295127e6fc..e95a1a2e9f8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -17,7 +17,7 @@ frappe.listview_settings["Purchase Receipt"] = { return [__("Closed"), "green", "status,=,Closed"]; } else if (flt(doc.per_returned, 2) === 100) { return [__("Return Issued"), "grey", "per_returned,=,100|docstatus,=,1"]; - } else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) == 0) { + } else if (flt(doc.grand_total || doc.base_grand_total) !== 0 && flt(doc.per_billed, 2) == 0) { return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"]; } else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) { return [__("Partly Billed"), "yellow", "per_billed,<,100|docstatus,=,1"]; 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 810c08a545e..5f8a9b58e91 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -48,6 +48,7 @@ "column_break_37", "discount_percentage", "discount_amount", + "distributed_discount_amount", "base_rate_with_margin", "sec_break1", "rate", @@ -911,7 +912,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -1128,6 +1129,12 @@ "options": "Company:company:default_currency", "print_hide": 1 }, + { + "fieldname": "distributed_discount_amount", + "fieldtype": "Currency", + "label": "Distributed Discount Amount", + "options": "currency" + }, { "fieldname": "amount_difference_with_purchase_invoice", "fieldtype": "Currency", @@ -1140,7 +1147,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-03-12 17:10:42.780622", + "modified": "2025-03-12 17:10:43.780622", "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 0db866f52c1..9f65ae56a83 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py @@ -37,6 +37,7 @@ class PurchaseReceiptItem(Document): description: DF.TextEditor | None discount_amount: DF.Currency discount_percentage: DF.Percent + distributed_discount_amount: DF.Currency expense_account: DF.Link | None from_warehouse: DF.Link | None has_item_scanned: DF.Check