diff --git a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py index 65784dbb6c72..4abc82d8becd 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py @@ -22,8 +22,10 @@ def setUp(self): cost_centers = [ "Main Cost Center 1", "Main Cost Center 2", + "Main Cost Center 3", "Sub Cost Center 1", "Sub Cost Center 2", + "Sub Cost Center 3", ] for cc in cost_centers: create_cost_center(cost_center_name=cc, company="_Test Company") @@ -36,7 +38,7 @@ def test_gle_based_on_cost_center_allocation(self): ) jv = make_journal_entry( - "_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True + "Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True ) expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]] @@ -120,7 +122,7 @@ def test_total_percentage(self): def test_valid_from_based_on_existing_gle(self): # GLE posted against Sub Cost Center 1 on today jv = make_journal_entry( - "_Test Cash - _TC", + "Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", @@ -141,6 +143,53 @@ def test_valid_from_based_on_existing_gle(self): jv.cancel() + def test_multiple_cost_center_allocation_on_same_main_cost_center(self): + coa1 = create_cost_center_allocation( + "_Test Company", + "Main Cost Center 3 - _TC", + {"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40}, + valid_from=add_days(today(), -5), + ) + + coa2 = create_cost_center_allocation( + "_Test Company", + "Main Cost Center 3 - _TC", + {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}, + valid_from=add_days(today(), -1), + ) + + jv = make_journal_entry( + "Cash - _TC", + "Sales - _TC", + 100, + cost_center="Main Cost Center 3 - _TC", + posting_date=today(), + submit=True, + ) + + expected_values = {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50} + + gle = frappe.qb.DocType("GL Entry") + gl_entries = ( + frappe.qb.from_(gle) + .select(gle.cost_center, gle.debit, gle.credit) + .where(gle.voucher_type == "Journal Entry") + .where(gle.voucher_no == jv.name) + .where(gle.account == "Sales - _TC") + .orderby(gle.cost_center) + ).run(as_dict=1) + + self.assertTrue(gl_entries) + + for gle in gl_entries: + self.assertTrue(gle.cost_center in expected_values) + self.assertEqual(gle.debit, 0) + self.assertEqual(gle.credit, expected_values[gle.cost_center]) + + coa1.cancel() + coa2.cancel() + jv.cancel() + def create_cost_center_allocation( company, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index d290d794df19..faa38763b80d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -360,21 +360,23 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro accounts_add(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); + row.exchange_rate = 1; $.each(doc.accounts, function (i, d) { if (d.account && d.party && d.party_type) { row.account = d.account; row.party = d.party; row.party_type = d.party_type; + row.exchange_rate = d.exchange_rate; } }); // set difference if (doc.difference) { if (doc.difference > 0) { - row.credit_in_account_currency = doc.difference; + row.credit_in_account_currency = doc.difference / row.exchange_rate; row.credit = doc.difference; } else { - row.debit_in_account_currency = -doc.difference; + row.debit_in_account_currency = -doc.difference / row.exchange_rate; row.debit = -doc.difference; } } @@ -680,6 +682,7 @@ $.extend(erpnext.journal_entry, { callback: function (r) { if (r.message) { $.extend(d, r.message); + erpnext.journal_entry.set_amount_on_last_row(frm, dt, dn); erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn); refresh_field("accounts"); } @@ -687,4 +690,26 @@ $.extend(erpnext.journal_entry, { }); } }, + set_amount_on_last_row: function (frm, dt, dn) { + let row = locals[dt][dn]; + let length = frm.doc.accounts.length; + if (row.idx != length) return; + + let difference = frm.doc.accounts.reduce((total, row) => { + if (row.idx == length) return total; + + return total + row.debit - row.credit; + }, 0); + + if (difference) { + if (difference > 0) { + row.credit_in_account_currency = difference / row.exchange_rate; + row.credit = difference; + } else { + row.debit_in_account_currency = -difference / row.exchange_rate; + row.debit = -difference; + } + } + refresh_field("accounts"); + }, }); diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 6adc8be3f7db..593fa48e8560 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -195,6 +195,11 @@ def on_submit(self): self.update_booked_depreciation() def on_update_after_submit(self): + # Flag will be set on Reconciliation + # Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost. + if self.flags.get("ignore_reposting_on_reconciliation"): + return + self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []}) if self.needs_repost: self.validate_for_repost() diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index b50dcd217e7c..f46c782112c5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -385,7 +385,15 @@ frappe.ui.form.on("Payment Entry", { payment_type: function (frm) { if (frm.doc.payment_type == "Internal Transfer") { $.each( - ["party", "party_balance", "paid_from", "paid_to", "references", "total_allocated_amount"], + [ + "party", + "party_type", + "party_balance", + "paid_from", + "paid_to", + "references", + "total_allocated_amount", + ], function (i, field) { frm.set_value(field, null); } diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index cc03dc260bbd..771c91a462ce 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1791,6 +1791,79 @@ def test_opening_flag_for_advance_as_liability(self): # 'Is Opening' should always be 'No' for normal advance payments self.assertEqual(gl_with_opening_set, []) + @change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1}) + def test_delete_linked_exchange_gain_loss_journal(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=83.970000000, + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # create a payment entry for the invoice + pe = get_payment_entry("Sales Invoice", si.name) + pe.reference_no = "1" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 100 + pe.source_exchange_rate = 90 + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 2710, + }, + ) + pe.save() + pe.submit() + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + {"reference_type": pe.doctype, "reference_name": pe.name, "docstatus": 1}, + pluck="parent", + ) + self.assertTrue(jv) + + # check cancellation of payment entry and journal entry + pe.cancel() + self.assertTrue(pe.docstatus == 2) + self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2) + + # check deletion of payment entry and journal entry + pe.delete() + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name) + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0]) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 5aa411158a8f..883c638398c5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -5,7 +5,7 @@ import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate +from frappe.utils import add_days, add_years, flt, getdate, nowdate, today from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -13,6 +13,7 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account +from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.stock.doctype.item.test_item import create_item @@ -1845,6 +1846,78 @@ def test_cr_note_payment_limit_filter(self): self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) + def test_reconciliation_on_closed_period_payment(self): + # create backdated fiscal year + first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)") + prev_fy_start_date = add_years(first_fy_start_date, -1) + prev_fy_end_date = add_days(first_fy_start_date, -1) + create_fiscal_year( + company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date + ) + + # make journal entry for previous year + je_1 = frappe.new_doc("Journal Entry") + je_1.posting_date = add_days(prev_fy_start_date, 20) + je_1.company = self.company + je_1.user_remark = "test" + je_1.set( + "accounts", + [ + { + "account": self.debit_to, + "cost_center": self.cost_center, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 0, + "credit_in_account_currency": 1000, + }, + { + "account": self.bank, + "cost_center": self.sub_cc.name, + "credit_in_account_currency": 0, + "debit_in_account_currency": 500, + }, + { + "account": self.cash, + "cost_center": self.sub_cc.name, + "credit_in_account_currency": 0, + "debit_in_account_currency": 500, + }, + ], + ) + je_1.submit() + + # make period closing voucher + pcv = make_period_closing_voucher( + company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date + ) + pcv.reload() + # check if period closing voucher is completed + self.assertEqual(pcv.gle_processing_status, "Completed") + + # make journal entry for active year + je_2 = self.create_journal_entry( + acc1=self.debit_to, acc2=self.income_account, amount=1000, posting_date=today() + ) + je_2.accounts[0].party_type = "Customer" + je_2.accounts[0].party = self.customer + je_2.submit() + + # process reconciliation on closed period payment + pr = self.create_payment_reconciliation(party_is_customer=True) + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = None + pr.get_unreconciled_entries() + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + je_1.reload() + je_2.reload() + + # check whether the payment reconciliation is done on the closed period + self.assertEqual(pr.get("invoices"), []) + self.assertEqual(pr.get("payments"), []) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): @@ -1872,3 +1945,61 @@ def make_supplier(supplier_name, currency=None): return supplier.name else: return supplier_name + + +def create_fiscal_year(company, year_start_date, year_end_date): + fy_docname = frappe.db.exists( + "Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date} + ) + if not fy_docname: + fy_doc = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": f"{getdate(year_start_date).year}-{getdate(year_end_date).year}", + "year_start_date": year_start_date, + "year_end_date": year_end_date, + "companies": [{"company": company}], + } + ).save() + return fy_doc + else: + fy_doc = frappe.get_doc("Fiscal Year", fy_docname) + if not frappe.db.exists("Fiscal Year Company", {"parent": fy_docname, "company": company}): + fy_doc.append("companies", {"company": company}) + fy_doc.save() + return fy_doc + + +def make_period_closing_voucher(company, cost_center, posting_date=None, submit=True): + from erpnext.accounts.doctype.account.test_account import create_account + + parent_account = frappe.db.get_value( + "Account", {"company": company, "account_name": "Current Liabilities", "is_group": 1}, "name" + ) + surplus_account = create_account( + account_name="Reserve and Surplus", + is_group=0, + company=company, + root_type="Liability", + report_type="Balance Sheet", + account_currency="INR", + parent_account=parent_account, + doctype="Account", + ) + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": posting_date or today(), + "posting_date": posting_date or today(), + "company": company, + "fiscal_year": get_fiscal_year(posting_date or today(), company=company)[0], + "cost_center": cost_center, + "closing_account_head": surplus_account, + "remarks": "test", + } + ) + pcv.insert() + if submit: + pcv.submit() + + return pcv diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 6f191c106c91..ee9dd2be8c35 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -419,7 +419,8 @@ "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", - "label": "Rate" + "label": "Rate", + "options": "currency" }, { "default": "0", @@ -647,7 +648,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2024-05-17 13:16:34.496704", + "modified": "2024-09-16 18:14:51.314765", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", @@ -709,4 +710,4 @@ "sort_order": "DESC", "states": [], "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 235fddf3ab39..3fece4aeeab8 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -5,6 +5,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -14,7 +15,7 @@ from erpnext.stock.get_item_details import get_item_details -class TestPricingRule(unittest.TestCase): +class TestPricingRule(FrappeTestCase): def setUp(self): delete_existing_pricing_rules() setup_pricing_rule_data() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 9c8c8cdfb030..2d5cbb9e6c3a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -648,11 +648,11 @@ frappe.ui.form.on("Purchase Invoice", { }, onload: function (frm) { - if (frm.doc.__onload && frm.is_new()) { - if (frm.doc.supplier) { + if (frm.doc.__onload && frm.doc.supplier) { + if (frm.is_new()) { frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; } - if (!frm.doc.__onload.enable_apply_tds) { + if (!frm.doc.__onload.supplier_tds) { frm.set_df_property("apply_tds", "read_only", 1); } } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 3751c027c975..c2ee4166ed16 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1271,6 +1271,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", + "no_copy": 1, "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1 }, @@ -1630,7 +1631,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2024-07-25 19:42:36.931278", + "modified": "2024-09-11 12:59:19.130593", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 623fb941b891..c9f3b220cc61 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -346,22 +346,6 @@ def set_missing_values(self, for_validate=False): self.tax_withholding_category = tds_category self.set_onload("supplier_tds", tds_category) - # If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox - if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]: - po = qb.DocType("Purchase Order") - po_with_tds = ( - qb.from_(po) - .select(po.name) - .where( - po.docstatus.eq(1) - & (po.name.isin(purchase_orders)) - & (po.apply_tds.eq(1)) - & (po.tax_withholding_category.notnull()) - ) - .run() - ) - self.set_onload("enable_apply_tds", True if po_with_tds else False) - super().set_missing_values(for_validate) def validate_credit_to_acc(self): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 12e26623ad00..1214ddee8cd2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -951,7 +951,7 @@ def add_remarks(self): if self.po_no: self.remarks = _("Against Customer Order {0}").format(self.po_no) if self.po_date: - self.remarks += " " + _("dated {0}").format(formatdate(self.po_data)) + self.remarks += " " + _("dated {0}").format(formatdate(self.po_date)) else: self.remarks = _("No Remarks") diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index beb347e07dba..7ac0d34e671c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -8,7 +8,7 @@ from frappe import qb from frappe.model.dynamic_links import get_dynamic_link_map from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, getdate, nowdate, today +from frappe.utils import add_days, flt, format_date, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -3997,6 +3997,14 @@ def test_common_party_with_foreign_currency_jv(self): self.assertTrue(jv) self.assertEqual(jv[0], si.grand_total) + def test_invoice_remarks(self): + si = frappe.copy_doc(test_records[0]) + si.po_no = "Test PO" + si.po_date = nowdate() + si.save() + si.submit() + self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}") + def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index a4d128a58459..ad8cc97e101d 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -179,50 +179,53 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None): - cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"]) - if not cost_center_allocation: - return gl_map - new_gl_map = [] for d in gl_map: cost_center = d.get("cost_center") # Validate budget against main cost center validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)) - - if cost_center and cost_center_allocation.get(cost_center): - for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items(): - gle = copy.deepcopy(d) - gle.cost_center = sub_cost_center - for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"): - gle[field] = flt(flt(d.get(field)) * percentage / 100, precision) - new_gl_map.append(gle) - else: + cost_center_allocation = get_cost_center_allocation_data( + gl_map[0]["company"], gl_map[0]["posting_date"], cost_center + ) + if not cost_center_allocation: new_gl_map.append(d) + continue + + for sub_cost_center, percentage in cost_center_allocation: + gle = copy.deepcopy(d) + gle.cost_center = sub_cost_center + for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"): + gle[field] = flt(flt(d.get(field)) * percentage / 100, precision) + new_gl_map.append(gle) return new_gl_map -def get_cost_center_allocation_data(company, posting_date): - par = frappe.qb.DocType("Cost Center Allocation") - child = frappe.qb.DocType("Cost Center Allocation Percentage") +def get_cost_center_allocation_data(company, posting_date, cost_center): + cost_center_allocation = frappe.db.get_value( + "Cost Center Allocation", + { + "docstatus": 1, + "company": company, + "valid_from": ("<=", posting_date), + "main_cost_center": cost_center, + }, + pluck="name", + order_by="valid_from desc", + ) - records = ( - frappe.qb.from_(par) - .inner_join(child) - .on(par.name == child.parent) - .select(par.main_cost_center, child.cost_center, child.percentage) - .where(par.docstatus == 1) - .where(par.company == company) - .where(par.valid_from <= posting_date) - .orderby(par.valid_from, order=frappe.qb.desc) - ).run(as_dict=True) + if not cost_center_allocation: + return [] - cc_allocation = frappe._dict() - for d in records: - cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage) + records = frappe.db.get_all( + "Cost Center Allocation Percentage", + {"parent": cost_center_allocation}, + ["cost_center", "percentage"], + as_list=True, + ) - return cc_allocation + return records def merge_similar_entries(gl_map, precision=None): diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 34cced2ca171..b288e8e5ac73 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -232,9 +232,15 @@ def get_group_by_asset_data(filters): def get_assets_for_grouped_by_category(filters): condition = "" if filters.get("asset_category"): - condition = " and a.asset_category = '{}'".format(filters.get("asset_category")) + condition = f" and a.asset_category = '{filters.get('asset_category')}'" + finance_book_filter = "" + if filters.get("finance_book"): + finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + + # nosemgrep return frappe.db.sql( - """ + f""" SELECT results.asset_category, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, @@ -264,7 +270,14 @@ def get_assets_for_grouped_by_category(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} + where + a.docstatus=1 + and a.company=%(company)s + and a.purchase_date <= %(to_date)s + and gle.debit != 0 + and gle.is_cancelled = 0 + and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + {condition} {finance_book_filter} group by a.asset_category union SELECT a.asset_category, @@ -280,11 +293,16 @@ def get_assets_for_grouped_by_category(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} group by a.asset_category) as results group by results.asset_category - """.format(condition), - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """, + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "finance_book": filters.get("finance_book", ""), + }, as_dict=1, ) @@ -292,9 +310,15 @@ def get_assets_for_grouped_by_category(filters): def get_assets_for_grouped_by_asset(filters): condition = "" if filters.get("asset"): - condition = " and a.name = '{}'".format(filters.get("asset")) + condition = f" and a.name = '{filters.get('asset')}'" + finance_book_filter = "" + if filters.get("finance_book"): + finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + + # nosemgrep return frappe.db.sql( - """ + f""" SELECT results.name as asset, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, @@ -324,7 +348,14 @@ def get_assets_for_grouped_by_asset(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} + where + a.docstatus=1 + and a.company=%(company)s + and a.purchase_date <= %(to_date)s + and gle.debit != 0 + and gle.is_cancelled = 0 + and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + {finance_book_filter} {condition} group by a.name union SELECT a.name as name, @@ -340,11 +371,16 @@ def get_assets_for_grouped_by_asset(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} group by a.name) as results group by results.name - """.format(condition), - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """, + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "finance_book": filters.get("finance_book", ""), + }, as_dict=1, ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index e462f749b54e..8e47ddc3652b 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -665,6 +665,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): # will work as update after submit journal_entry.flags.ignore_validate_update_after_submit = True + # Ledgers will be reposted by Reconciliation tool + journal_entry.flags.ignore_reposting_on_reconciliation = True if not do_not_save: journal_entry.save(ignore_permissions=True) @@ -745,40 +747,74 @@ def cancel_exchange_gain_loss_journal( Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. """ if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: - journals = frappe.db.get_all( - "Journal Entry Account", - filters={ - "reference_type": parent_doc.doctype, - "reference_name": parent_doc.name, - "docstatus": 1, - }, - fields=["parent"], - as_list=1, + gain_loss_journals = get_linked_exchange_gain_loss_journal( + referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=1 ) - - if journals: - gain_loss_journals = frappe.db.get_all( - "Journal Entry", - filters={ - "name": ["in", [x[0] for x in journals]], - "voucher_type": "Exchange Gain Or Loss", - "docstatus": 1, - }, - as_list=1, - ) - for doc in gain_loss_journals: - gain_loss_je = frappe.get_doc("Journal Entry", doc[0]) - if referenced_dt and referenced_dn: - references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] - if ( - len(references) == 2 - and (referenced_dt, referenced_dn) in references - and (parent_doc.doctype, parent_doc.name) in references - ): - # only cancel JE generated against parent_doc and referenced_dn - gain_loss_je.cancel() - else: + for doc in gain_loss_journals: + gain_loss_je = frappe.get_doc("Journal Entry", doc) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only cancel JE generated against parent_doc and referenced_dn gain_loss_je.cancel() + else: + gain_loss_je.cancel() + + +def delete_exchange_gain_loss_journal( + parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None +) -> None: + """ + Delete Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. + """ + if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: + gain_loss_journals = get_linked_exchange_gain_loss_journal( + referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=2 + ) + for doc in gain_loss_journals: + gain_loss_je = frappe.get_doc("Journal Entry", doc) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only delete JE generated against parent_doc and referenced_dn + gain_loss_je.delete() + else: + gain_loss_je.delete() + + +def get_linked_exchange_gain_loss_journal(referenced_dt: str, referenced_dn: str, je_docstatus: int) -> list: + """ + Get all the linked exchange gain/loss journal entries for a given document. + """ + gain_loss_journals = [] + if journals := frappe.db.get_all( + "Journal Entry Account", + { + "reference_type": referenced_dt, + "reference_name": referenced_dn, + "docstatus": je_docstatus, + }, + pluck="parent", + ): + gain_loss_journals = frappe.db.get_all( + "Journal Entry", + { + "name": ["in", journals], + "voucher_type": "Exchange Gain Or Loss", + "is_system_generated": 1, + "docstatus": je_docstatus, + }, + pluck="name", + ) + return gain_loss_journals def cancel_common_party_journal(self): diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 58f813247f0a..6c77a1b9b0ee 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -623,6 +623,9 @@ def get_manual_depreciation_entries(self): return records def validate_make_gl_entry(self): + if self.is_composite_asset: + return True + purchase_document = self.get_purchase_document() if not purchase_document: return False diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index bd92ebef3d7e..ac2aa43f23db 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -65,6 +65,31 @@ frappe.ui.form.on("Purchase Order", { } }, + supplier: function (frm) { + // Do not update if inter company reference is there as the details will already be updated + if (frm.updating_party_details || frm.doc.inter_company_invoice_reference) return; + + if (frm.doc.__onload && frm.doc.__onload.load_after_mapping) return; + + erpnext.utils.get_party_details( + frm, + "erpnext.accounts.party.get_party_details", + { + posting_date: frm.doc.transaction_date, + bill_date: frm.doc.bill_date, + party: frm.doc.supplier, + party_type: "Supplier", + account: frm.doc.credit_to, + price_list: frm.doc.buying_price_list, + fetch_payment_terms_template: cint(!frm.doc.ignore_default_payment_terms_template), + }, + function () { + frm.set_df_property("apply_tds", "read_only", frm.supplier_tds ? 0 : 1); + frm.set_df_property("tax_withholding_category", "hidden", frm.supplier_tds ? 0 : 1); + } + ); + }, + get_materials_from_supplier: function (frm) { let po_details = []; @@ -108,6 +133,15 @@ frappe.ui.form.on("Purchase Order", { frm.set_value("transaction_date", frappe.datetime.get_today()); } + if (frm.doc.__onload && frm.doc.supplier) { + if (frm.is_new()) { + frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; + } + if (!frm.doc.__onload.supplier_tds) { + frm.set_df_property("apply_tds", "read_only", 1); + } + } + erpnext.queries.setup_queries(frm, "Warehouse", function () { return erpnext.queries.warehouse(frm.doc); }); diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 0508483a0fc9..14424dfdf4ab 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -648,6 +648,13 @@ def update_subcontracting_order_status(self): if sco: update_sco_status(sco, "Closed" if self.status == "Closed" else None) + def set_missing_values(self, for_validate=False): + tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") + if tds_category and not for_validate: + self.set_onload("supplier_tds", tds_category) + + super().set_missing_values(for_validate) + @frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): @@ -760,6 +767,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions def postprocess(source, target): target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) + + # set tax_withholding_category from Purchase Order + if source.apply_tds and source.tax_withholding_category and target.apply_tds: + target.tax_withholding_category = source.tax_withholding_category + # Get the advance paid Journal Entries in Purchase Invoice Advance if target.get("allocate_advances_automatically"): target.set_advances() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 49198be9b484..92efa5168f3b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -346,12 +346,17 @@ def _remove_references_in_repost_doctypes(self): repost_doc.save(ignore_permissions=True) def on_trash(self): + from erpnext.accounts.utils import delete_exchange_gain_loss_journal + self._remove_references_in_repost_doctypes() self._remove_references_in_unreconcile() self.remove_serial_and_batch_bundle() # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): + # delete linked exchange gain/loss journal + delete_exchange_gain_loss_journal(self) + ple = frappe.qb.DocType("Payment Ledger Entry") frappe.qb.from_(ple).delete().where( (ple.voucher_type == self.doctype) & (ple.voucher_no == self.name) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 766e67be0f67..7ad12b54eff5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -640,7 +640,7 @@ def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: item.gross_profit = flt( - ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), + ((flt(item.stock_uom_rate) - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item), ) diff --git a/erpnext/crm/frappe_crm_api.py b/erpnext/crm/frappe_crm_api.py new file mode 100644 index 000000000000..53f58ed57eb1 --- /dev/null +++ b/erpnext/crm/frappe_crm_api.py @@ -0,0 +1,170 @@ +import json + +import frappe +from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +@frappe.whitelist() +def create_custom_fields_for_frappe_crm(): + frappe.only_for("System Manager") + custom_fields = { + "Quotation": [ + { + "fieldname": "crm_deal", + "fieldtype": "Data", + "label": "Frappe CRM Deal", + "insert_after": "party_name", + } + ], + "Customer": [ + { + "fieldname": "crm_deal", + "fieldtype": "Data", + "label": "Frappe CRM Deal", + "insert_after": "prospect_name", + } + ], + } + create_custom_fields(custom_fields, ignore_validate=True) + + +@frappe.whitelist() +def create_prospect_against_crm_deal(): + frappe.only_for("System Manager") + doc = frappe.form_dict + prospect = frappe.get_doc( + { + "doctype": "Prospect", + "company_name": doc.organization or doc.lead_name, + "no_of_employees": doc.no_of_employees, + "prospect_owner": doc.deal_owner, + "company": doc.erpnext_company, + "crm_deal": doc.crm_deal, + "territory": doc.territory, + "industry": doc.industry, + "website": doc.website, + "annual_revenue": doc.annual_revenue, + } + ) + + try: + prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name}) + if not prospect_name: + prospect.insert() + prospect_name = prospect.name + except Exception: + frappe.log_error( + frappe.get_traceback(), + f"Error while creating prospect against CRM Deal: {frappe.form_dict.get('crm_deal_id')}", + ) + pass + + create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name) + create_address("Prospect", prospect_name, doc.address) + frappe.response["message"] = prospect_name + + +def create_contacts(contacts, organization=None, link_doctype=None, link_docname=None): + for c in contacts: + c = frappe._dict(c) + existing_contact = contact_exists(c.email, c.mobile_no) + if existing_contact: + contact = frappe.get_doc("Contact", existing_contact) + else: + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": c.get("full_name"), + "gender": c.get("gender"), + "company_name": organization, + } + ) + + if c.get("email"): + contact.append("email_ids", {"email_id": c.get("email"), "is_primary": 1}) + + if c.get("mobile_no"): + contact.append("phone_nos", {"phone": c.get("mobile_no"), "is_primary_mobile_no": 1}) + + link_doc(contact, link_doctype, link_docname) + + contact.save(ignore_permissions=True) + + +def create_address(doctype, docname, address): + if not address: + return + try: + _address = frappe.db.exists("Address", address.get("name")) + if not _address: + new_address_doc = frappe.new_doc("Address") + for field in [ + "address_title", + "address_type", + "address_line1", + "address_line2", + "city", + "state", + "pincode", + "country", + ]: + if address.get(field): + new_address_doc.set(field, address.get(field)) + + new_address_doc.append("links", {"link_doctype": doctype, "link_name": docname}) + new_address_doc.insert(ignore_mandatory=True) + return new_address_doc.name + else: + address = frappe.get_doc("Address", _address) + link_doc(address, doctype, docname) + address.save(ignore_permissions=True) + return address.name + except Exception: + frappe.log_error(frappe.get_traceback(), f"Error while creating address for {docname}") + + +def link_doc(doc, link_doctype, link_docname): + already_linked = any( + [(link.link_doctype == link_doctype and link.link_name == link_docname) for link in doc.links] + ) + if not already_linked: + doc.append( + "links", {"link_doctype": link_doctype, "link_name": link_docname, "link_title": link_docname} + ) + + +def contact_exists(email, mobile_no): + email_exist = frappe.db.exists("Contact Email", {"email_id": email}) + mobile_exist = frappe.db.exists("Contact Phone", {"phone": mobile_no}) + + doctype = "Contact Email" if email_exist else "Contact Phone" + name = email_exist or mobile_exist + + if name: + return frappe.db.get_value(doctype, name, "parent") + + return False + + +@frappe.whitelist() +def create_customer(customer_data=None): + frappe.only_for("System Manager") + if not customer_data: + customer_data = frappe.form_dict + + try: + customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")}) + if not customer_name: + customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert( + ignore_permissions=True + ) + customer_name = customer.name + + contacts = json.loads(customer_data.get("contacts")) + create_contacts(contacts, customer_name, "Customer", customer_name) + create_address("Customer", customer_name, customer_data.get("address")) + return customer_name + except Exception: + frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal") + pass diff --git a/erpnext/patches/v14_0/update_reports_with_range.py b/erpnext/patches/v14_0/update_reports_with_range.py index 2bda265ca669..014fba883fce 100644 --- a/erpnext/patches/v14_0/update_reports_with_range.py +++ b/erpnext/patches/v14_0/update_reports_with_range.py @@ -30,6 +30,9 @@ def update_report_json(report): report_json = json.loads(report.json) report_filter = report_json.get("filters") + if not report_filter: + return + keys_to_pop = [key for key in report_filter if key.startswith("range")] report_filter["range"] = ", ".join(str(report_filter.pop(key)) for key in keys_to_pop) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index b09735e56443..7870a2ace738 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -213,6 +213,13 @@ def on_trash(self): frappe.db.set_value("Sales Order", {"project": self.name}, "project", "") def update_percent_complete(self): + if self.status == "Completed": + if ( + len(frappe.get_all("Task", dict(project=self.name))) == 0 + ): # A project without tasks should be able to complete + self.percent_complete_method = "Manual" + self.percent_complete = 100 + if self.percent_complete_method == "Manual": if self.status == "Completed": self.percent_complete = 100 diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 1b7460f7a2a0..e5996c2da9d2 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -199,6 +199,34 @@ def test_project_with_template_tasks_having_common_name(self): if not pt.is_group: self.assertIsNotNone(pt.parent_task) + def test_project_having_no_tasks_complete(self): + project_name = "Test Project - No Tasks Completion" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc("Project", project_name) + + project = frappe.get_doc( + { + "doctype": "Project", + "project_name": project_name, + "status": "Open", + "expected_start_date": nowdate(), + "company": "_Test Company", + } + ).insert() + + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks", "name", "parent_task"], + dict(project=project.name), + order_by="creation asc", + ) + + self.assertEqual(project.status, "Open") + self.assertEqual(len(tasks), 0) + project.status = "Completed" + project.save() + self.assertEqual(project.status, "Completed") + def get_project(name, template): project = frappe.get_doc( diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3dcd36b2cffc..67c61118952b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1258,6 +1258,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "Purchase Receipt": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"], "Purchase Invoice": ["purchase_order_item", "pr_detail", "po_detail"], "Sales Order": ["prevdoc_docname", "quotation_item"], + "Purchase Order": ["supplier_quotation_item"], }; const mappped_fields = mapped_item_field_map[this.frm.doc.doctype] || []; @@ -1501,6 +1502,31 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + batch_no(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.use_serial_batch_fields && row.batch_no) { + var params = this._get_args(row); + params.batch_no = row.batch_no; + params.uom = row.uom; + + frappe.call({ + method: "erpnext.stock.get_item_details.get_batch_based_item_price", + args: { + params: params, + item_code: row.item_code, + }, + callback: function(r) { + if (!r.exc && r.message) { + row.price_list_rate = r.message; + row.rate = r.message; + refresh_field("rate", row.name, row.parentfield); + refresh_field("price_list_rate", row.name, row.parentfield); + } + } + }) + } + } + toggle_item_grid_columns(company_currency) { const me = this; // toggle columns diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 3044d865c0ca..a4c70d7f50f2 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -71,7 +71,7 @@ frappe.ui.form.on("Quotation", { frm.trigger("set_label"); frm.trigger("toggle_reqd_lead_customer"); frm.trigger("set_dynamic_field_label"); - frm.set_value("party_name", ""); + // frm.set_value("party_name", ""); // removed to set party_name from url for crm integration frm.set_value("customer_name", ""); }, diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 2a4b04b9db53..7ebcb3291934 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -220,6 +220,10 @@ def set_customer_name(self): "Lead", self.party_name, ["lead_name", "company_name"] ) self.customer_name = company_name or lead_name + elif self.party_name and self.quotation_to == "Prospect": + self.customer_name = self.party_name + elif self.party_name and self.quotation_to == "CRM Deal": + self.customer_name = frappe.db.get_value("CRM Deal", self.party_name, "organization") def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): 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 694f70d4db5e..b808b4f8828d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -389,28 +389,14 @@ erpnext.PointOfSale.ItemCart = class { placeholder: discount ? discount + "%" : __("Enter discount percentage."), input_class: "input-xs", onchange: function () { - if (flt(this.value) != 0) { - frappe.model.set_value( - frm.doc.doctype, - frm.doc.name, - "additional_discount_percentage", - flt(this.value) - ); - me.hide_discount_control(this.value); - } else { - frappe.model.set_value( - frm.doc.doctype, - frm.doc.name, - "additional_discount_percentage", - 0 - ); - me.$add_discount_elem.css({ - border: "1px dashed var(--gray-500)", - padding: "var(--padding-sm) var(--padding-md)", - }); - me.$add_discount_elem.html(`${me.get_discount_icon()} ${__("Add Discount")}`); - me.discount_field = undefined; - } + this.value = flt(this.value); + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "additional_discount_percentage", + flt(this.value) + ); + me.hide_discount_control(this.value); }, }, parent: this.$add_discount_elem.find(".add-discount-field"), @@ -421,9 +407,13 @@ erpnext.PointOfSale.ItemCart = class { } hide_discount_control(discount) { - if (!discount) { - this.$add_discount_elem.css({ padding: "0px", border: "none" }); - this.$add_discount_elem.html(`
`); + if (!flt(discount)) { + this.$add_discount_elem.css({ + border: "1px dashed var(--gray-500)", + padding: "var(--padding-sm) var(--padding-md)", + }); + this.$add_discount_elem.html(`${this.get_discount_icon()} ${__("Add Discount")}`); + this.discount_field = undefined; } else { this.$add_discount_elem.css({ border: "1px dashed var(--dark-green-500)", @@ -1051,6 +1041,7 @@ erpnext.PointOfSale.ItemCart = class { this.highlight_checkout_btn(false); } + this.hide_discount_control(frm.doc.additional_discount_percentage); this.update_totals_section(frm); if (frm.doc.docstatus === 1) { diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 0216f75a6286..b7920236ce1c 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -149,7 +149,11 @@ def validate_duplicate_date(self): unique_dates = [] for row in self.holidays: if row.holiday_date in unique_dates: - frappe.throw(_("Holiday Date {0} added multiple times").format(frappe.bold(row.holiday_date))) + frappe.throw( + _("Holiday Date {0} added multiple times").format( + frappe.bold(formatdate(row.holiday_date)) + ) + ) unique_dates.append(row.holiday_date) diff --git a/erpnext/stock/doctype/item/templates/item_row.html b/erpnext/stock/doctype/item/templates/item_row.html index f81fc1d8743a..809ac838ccb0 100644 --- a/erpnext/stock/doctype/item/templates/item_row.html +++ b/erpnext/stock/doctype/item/templates/item_row.html @@ -1,4 +1,4 @@ diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 5cfa306066d8..50db899433fb 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -10,7 +10,7 @@ from frappe.model.meta import get_field_precision from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import IfNull, Sum -from frappe.utils import add_days, add_months, cint, cstr, flt, getdate +from frappe.utils import add_days, add_months, cint, cstr, flt, getdate, parse_json from erpnext import get_company_currency from erpnext.accounts.doctype.pricing_rule.pricing_rule import ( @@ -889,7 +889,7 @@ def insert_item_price(args): ) -def get_item_price(args, item_code, ignore_party=False): +def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]: """ Get name, price_list_rate from Item Price based on conditions Check if the desired qty is within the increment of the packing list. @@ -906,13 +906,17 @@ def get_item_price(args, item_code, ignore_party=False): (ip.item_code == item_code) & (ip.price_list == args.get("price_list")) & (IfNull(ip.uom, "").isin(["", args.get("uom")])) - & (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")])) ) .orderby(ip.valid_from, order=frappe.qb.desc) .orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc) .orderby(ip.uom, order=frappe.qb.desc) ) + if force_batch_no: + query = query.where(ip.batch_no == args.get("batch_no")) + else: + query = query.where(IfNull(ip.batch_no, "").isin(["", args.get("batch_no")])) + if not ignore_party: if args.get("customer"): query = query.where(ip.customer == args.get("customer")) @@ -930,6 +934,21 @@ def get_item_price(args, item_code, ignore_party=False): return query.run() +@frappe.whitelist() +def get_batch_based_item_price(params, item_code) -> float: + if isinstance(params, str): + params = parse_json(params) + + item_price = get_item_price(params, item_code, force_batch_no=True) + if not item_price: + item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True) + + if item_price and item_price[0].uom == params.get("uom"): + return item_price[0].price_list_rate + + return 0.0 + + def get_price_list_rate_for(args, item_code): """ :param customer: link to Customer DocType