diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 9ffdf186f02..a48ce9b4c63 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
)
if not amount:
- return
-
- gl_posting_date = end_date
- prev_posting_date = None
- # check if books nor frozen till endate:
- if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
- gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
prev_posting_date = end_date
-
- if via_journal_entry:
- book_revenue_via_journal_entry(
- doc,
- credit_account,
- debit_account,
- amount,
- base_amount,
- gl_posting_date,
- project,
- account_currency,
- item.cost_center,
- item,
- deferred_process,
- submit_journal_entry,
- )
else:
- make_gl_entries(
- doc,
- credit_account,
- debit_account,
- against,
- amount,
- base_amount,
- gl_posting_date,
- project,
- account_currency,
- item.cost_center,
- item,
- deferred_process,
- )
+ gl_posting_date = end_date
+ prev_posting_date = None
+ # check if books nor frozen till endate:
+ if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
+ gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
+ prev_posting_date = end_date
+
+ if via_journal_entry:
+ book_revenue_via_journal_entry(
+ doc,
+ credit_account,
+ debit_account,
+ amount,
+ base_amount,
+ gl_posting_date,
+ project,
+ account_currency,
+ item.cost_center,
+ item,
+ deferred_process,
+ submit_journal_entry,
+ )
+ else:
+ make_gl_entries(
+ doc,
+ credit_account,
+ debit_account,
+ against,
+ amount,
+ base_amount,
+ gl_posting_date,
+ project,
+ account_currency,
+ item.cost_center,
+ item,
+ deferred_process,
+ )
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
if frappe.flags.deferred_accounting_error:
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 8ec055e28a4..3d5d030a9ee 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -196,7 +196,7 @@ frappe.ui.form.on('Payment Entry', {
},
hide_unhide_fields: function(frm) {
- var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
+ var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company)?.default_currency: "";
frm.toggle_display("source_exchange_rate",
(frm.doc.paid_amount && frm.doc.paid_from_account_currency != company_currency));
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 0fa2e7835eb..106000a78f3 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -284,6 +284,17 @@ class PaymentRequest(Document):
payment_entry.received_amount = amount
payment_entry.get("references")[0].allocated_amount = amount
+ # Update 'Paid Amount' on Forex transactions
+ if self.currency != ref_doc.company_currency:
+ if (
+ self.payment_request_type == "Outward"
+ and payment_entry.paid_from_account_currency == ref_doc.company_currency
+ and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
+ ):
+ payment_entry.paid_amount = payment_entry.base_paid_amount = (
+ payment_entry.target_exchange_rate * payment_entry.received_amount
+ )
+
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 932060895b0..6d15f84d7cf 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -4,10 +4,12 @@
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
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.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate
@@ -32,7 +34,7 @@ payment_method = [
]
-class TestPaymentRequest(unittest.TestCase):
+class TestPaymentRequest(FrappeTestCase):
def setUp(self):
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
@@ -260,3 +262,19 @@ class TestPaymentRequest(unittest.TestCase):
# Try to make Payment Request more than SO amount, should give validation
pr2.grand_total = 900
self.assertRaises(frappe.ValidationError, pr2.save)
+
+ def test_conversion_on_foreign_currency_accounts(self):
+ po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1)
+ po_doc.conversion_rate = 80
+ po_doc.items[0].qty = 1
+ po_doc.items[0].rate = 10
+ po_doc.save().submit()
+
+ pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
+ pr = frappe.get_doc(pr).save().submit()
+
+ pe = pr.create_payment_entry()
+ self.assertEqual(pe.base_paid_amount, 800)
+ self.assertEqual(pe.paid_amount, 800)
+ self.assertEqual(pe.base_received_amount, 800)
+ self.assertEqual(pe.received_amount, 10)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
index bdbb1419784..8584cbb31cb 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -16,6 +16,7 @@
"cost_center",
"territory",
"ignore_exchange_rate_revaluation_journals",
+ "ignore_cr_dr_notes",
"column_break_14",
"to_date",
"finance_book",
@@ -381,10 +382,16 @@
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
+ },
+ {
+ "default": "0",
+ "fieldname": "ignore_cr_dr_notes",
+ "fieldtype": "Check",
+ "label": "Ignore System Generated Credit / Debit Notes"
}
],
"links": [],
- "modified": "2023-12-18 12:20:08.965120",
+ "modified": "2024-08-13 10:41:18.381165",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index a602e0d7df3..078ac034eeb 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -79,6 +79,9 @@ def get_statement_dict(doc, get_statement_dict=False):
if doc.ignore_exchange_rate_revaluation_journals:
filters.update({"ignore_err": True})
+ if doc.ignore_cr_dr_notes:
+ filters.update({"ignore_cr_dr_notes": True})
+
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
col, res = get_soa(filters)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index a254c917189..9dd62d76fef 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1294,6 +1294,10 @@ class SalesInvoice(SellingController):
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= flt(self.change_amount)
+ against_voucher = self.name
+ if self.is_return and self.return_against and not self.update_outstanding_for_self:
+ against_voucher = self.return_against
+
if payment_mode.base_amount:
# POS, make payment entries
gl_entries.append(
@@ -1307,7 +1311,7 @@ class SalesInvoice(SellingController):
"credit_in_account_currency": payment_mode.base_amount
if self.party_account_currency == self.company_currency
else payment_mode.amount,
- "against_voucher": self.name,
+ "against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
},
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 9e81fcf502f..bb51223db16 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -5,6 +5,7 @@ import copy
import json
import frappe
+from frappe import qb
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname
from frappe.tests.utils import FrappeTestCase, change_settings
@@ -3025,6 +3026,84 @@ class TestSalesInvoice(FrappeTestCase):
party_link.delete()
frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 0)
+ def test_sales_invoice_against_supplier_usd_with_dimensions(self):
+ from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
+ make_customer,
+ )
+ from erpnext.accounts.doctype.party_link.party_link import create_party_link
+ from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+
+ # create a customer
+ customer = make_customer(customer="_Test Common Supplier USD")
+ cust_doc = frappe.get_doc("Customer", customer)
+ cust_doc.default_currency = "USD"
+ cust_doc.save()
+ # create a supplier
+ supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
+ supp_doc = frappe.get_doc("Supplier", supplier)
+ supp_doc.default_currency = "USD"
+ supp_doc.save()
+
+ # create a party link between customer & supplier
+ party_link = create_party_link("Supplier", supplier, customer)
+
+ # enable common party accounting
+ frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
+
+ # create a dimension and make it mandatory
+ if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
+ dim = frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension",
+ "document_type": "Department",
+ "dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
+ }
+ )
+ dim.save()
+ else:
+ dim = frappe.get_doc(
+ "Accounting Dimension",
+ frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
+ )
+ dim.disabled = False
+ dim.dimension_defaults = []
+ dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
+ dim.save()
+
+ # create a sales invoice
+ si = create_sales_invoice(
+ customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
+ )
+ si.department = "All Departments"
+ si.save().submit()
+
+ # check outstanding of sales invoice
+ si.reload()
+ self.assertEqual(si.status, "Paid")
+ self.assertEqual(flt(si.outstanding_amount), 0.0)
+
+ # check creation of journal entry
+ jv = frappe.get_all(
+ "Journal Entry Account",
+ {
+ "account": si.debit_to,
+ "party_type": "Customer",
+ "party": si.customer,
+ "reference_type": si.doctype,
+ "reference_name": si.name,
+ "department": "All Departments",
+ },
+ pluck="credit_in_account_currency",
+ )
+
+ self.assertTrue(jv)
+ self.assertEqual(jv[0], si.grand_total)
+
+ dim.disabled = True
+ dim.save()
+ party_link.delete()
+ frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
+
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -3512,6 +3591,40 @@ class TestSalesInvoice(FrappeTestCase):
]
self.assertEqual(expected, actual)
+ def test_pos_returns_without_update_outstanding_for_self(self):
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
+
+ pos_profile = make_pos_profile()
+ pos_profile.payments = []
+ pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
+ pos_profile.save()
+
+ pos = create_sales_invoice(qty=10, do_not_save=True)
+ pos.is_pos = 1
+ pos.pos_profile = pos_profile.name
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
+ )
+ pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
+ pos.save().submit()
+
+ pos_return = make_sales_return(pos.name)
+ pos_return.update_outstanding_for_self = False
+ pos_return.save().submit()
+
+ gle = qb.DocType("GL Entry")
+ res = (
+ qb.from_(gle)
+ .select(gle.against_voucher)
+ .distinct()
+ .where(
+ gle.is_cancelled.eq(0) & gle.voucher_no.eq(pos_return.name) & gle.against_voucher.notnull()
+ )
+ .run(as_list=1)
+ )
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0][0], pos_return.return_against)
+
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(
diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js
index 60981f4b1d1..caaa60b5757 100644
--- a/erpnext/accounts/doctype/subscription/subscription.js
+++ b/erpnext/accounts/doctype/subscription/subscription.js
@@ -37,6 +37,12 @@ frappe.ui.form.on("Subscription", {
frm.add_custom_button(__("Fetch Subscription Updates"), () =>
frm.events.get_subscription_updates(frm)
);
+
+ frm.add_custom_button(
+ __("Force-Fetch Subscription Updates"),
+ () => frm.trigger("force_fetch_subscription_updates"),
+ __("Actions")
+ );
} else if (frm.doc.status === "Cancelled") {
frm.add_custom_button(__("Restart Subscription"), () =>
frm.events.renew_this_subscription(frm)
@@ -96,4 +102,11 @@ frappe.ui.form.on("Subscription", {
},
});
},
+ force_fetch_subscription_updates: function (frm) {
+ frm.call("force_fetch_subscription_updates").then((r) => {
+ if (!r.exec) {
+ frm.reload_doc();
+ }
+ });
+ },
});
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index fea2ae9bf8e..96c97659ad8 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -674,6 +674,31 @@ class Subscription(Document):
if invoice:
return invoice.precision("grand_total")
+ @frappe.whitelist()
+ def force_fetch_subscription_updates(self):
+ """
+ Process Subscription and create Invoices even if current date doesn't lie between current_invoice_start and currenct_invoice_end
+ It makes use of 'Proces Subscription' to force processing in a specific 'posting_date'
+ """
+
+ # Don't process future subscriptions
+ if nowdate() < self.current_invoice_start:
+ frappe.msgprint(_("Subscription for Future dates cannot be processed."))
+ return
+
+ processing_date = None
+ if self.generate_invoice_at == "Beginning of the current subscription period":
+ processing_date = self.current_invoice_start
+ elif self.generate_invoice_at == "End of the current subscription period":
+ processing_date = self.current_invoice_end
+ elif self.generate_invoice_at == "Days before the current subscription period":
+ processing_date = add_days(self.current_invoice_start, -self.number_of_days)
+
+ process_subscription = frappe.new_doc("Process Subscription")
+ process_subscription.posting_date = processing_date
+ process_subscription.subscription = self.name
+ process_subscription.save().submit()
+
def get_calendar_months(billing_interval):
calendar_months = []
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 2f0b87e9ea2..26dcb2413f4 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -712,3 +712,18 @@ class TestSubscription(FrappeTestCase):
self.assertEqual(pi.total, 55333.33)
subscription.delete()
+
+ def test_future_subscription(self):
+ """Force-Fetch should not process future subscriptions"""
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.generate_invoice_at_period_start = 1
+ subscription.generate_new_invoices_past_due_date = 1
+ subscription.start_date = add_months(nowdate(), 1)
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ subscription.save()
+
+ subscription.force_fetch_subscription_updates()
+ subscription.reload()
+ self.assertEqual(len(subscription.invoices), 0)
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
index f8a965b699c..4ca65dc04e5 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
+
+ @change_settings(
+ "Accounts Settings",
+ {"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
+ )
+ def test_zero_amount(self):
+ self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
+ item = frappe.get_doc("Item", self.item)
+ item.enable_deferred_expense = 1
+ item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
+ item.no_of_months_exp = 12
+ item.save()
+
+ pi = make_purchase_invoice(
+ item=self.item,
+ company=self.company,
+ supplier=self.supplier,
+ is_return=False,
+ update_stock=False,
+ posting_date=frappe.utils.datetime.date(2021, 12, 30),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ do_not_save=True,
+ rate=3910,
+ price_list_rate=3910,
+ warehouse=self.warehouse,
+ qty=1,
+ )
+ pi.set_posting_time = True
+ pi.items[0].enable_deferred_expense = 1
+ pi.items[0].service_start_date = "2021-12-30"
+ pi.items[0].service_end_date = "2022-12-30"
+ pi.items[0].deferred_expense_account = self.deferred_expense_account
+ pi.items[0].expense_account = self.expense_account
+ pi.save()
+ pi.submit()
+
+ pda = frappe.get_doc(
+ doctype="Process Deferred Accounting",
+ posting_date=nowdate(),
+ start_date="2022-01-01",
+ end_date="2022-01-31",
+ type="Expense",
+ company=self.company,
+ )
+ pda.insert()
+ pda.submit()
+
+ # execute report
+ fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
+ self.filters = frappe._dict(
+ {
+ "company": self.company,
+ "filter_based_on": "Date Range",
+ "period_start_date": "2022-01-01",
+ "period_end_date": "2022-01-31",
+ "from_fiscal_year": fiscal_year.year,
+ "to_fiscal_year": fiscal_year.year,
+ "periodicity": "Monthly",
+ "type": "Expense",
+ "with_upcoming_postings": False,
+ }
+ )
+
+ report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
+ report.run()
+
+ # fetch the invoice from deferred invoices list
+ inv = [d for d in report.deferred_invoices if d.name == pi.name]
+ # make sure the list isn't empty
+ self.assertTrue(inv)
+ # calculate the total deferred expense for the period
+ inv = inv[0].calculate_invoice_revenue_expense_for_period()
+ deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
+ # make sure the total deferred expense is greater than 0
+ self.assertLess(deferred_exp, 0)
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 05bbc4a79c9..b7f035b1e80 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -246,7 +246,10 @@ def get_conditions(filters):
as_list=True,
)
if system_generated_cr_dr_journals:
- filters.update({"voucher_no_not_in": [x[0] for x in system_generated_cr_dr_journals]})
+ vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + [
+ x[0] for x in system_generated_cr_dr_journals
+ ]
+ filters.update({"voucher_no_not_in": vouchers_to_ignore})
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index a9494cf6e80..ed4a4f52b8f 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2312,6 +2312,15 @@ class AccountsController(TransactionBase):
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes"
+ # update dimesions
+ dimensions_dict = frappe._dict()
+ active_dimensions = get_dimensions()[0]
+ for dim in active_dimensions:
+ dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
+
+ reconcilation_entry.update(dimensions_dict)
+ advance_entry.update(dimensions_dict)
+
if self.doctype == "Sales Invoice":
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
advance_entry.debit_in_account_currency = self.outstanding_amount
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index a0301f9ea0a..c7b0304203c 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -27,7 +27,26 @@ from erpnext.utilities.transaction_base import TransactionBase
class Opportunity(TransactionBase, CRMNote):
def onload(self):
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
+
load_address_and_contact(ref_doc)
+ load_address_and_contact(self)
+
+ ref_doc_contact_list = ref_doc.get("__onload").get("contact_list")
+ opportunity_doc_contact_list = [
+ contact
+ for contact in self.get("__onload").get("contact_list")
+ if contact not in ref_doc_contact_list
+ ]
+ ref_doc_contact_list.extend(opportunity_doc_contact_list)
+ ref_doc.set_onload("contact_list", ref_doc_contact_list)
+
+ ref_doc_addr_list = ref_doc.get("__onload").get("addr_list")
+ opportunity_doc_addr_list = [
+ addr for addr in self.get("__onload").get("addr_list") if addr not in ref_doc_addr_list
+ ]
+ ref_doc_addr_list.extend(opportunity_doc_addr_list)
+ ref_doc.set_onload("addr_list", ref_doc_addr_list)
+
self.set("__onload", ref_doc.get("__onload"))
def after_insert(self):
diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
index 5a36c999747..2e8ae7e340e 100644
--- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
+++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
@@ -108,7 +108,9 @@ class OpportunitySummaryBySalesStage:
self.grouped_data = []
grouping_key = lambda o: (o["sales_stage"], o[based_on]) # noqa
- for (sales_stage, _based_on), rows in groupby(self.query_result, grouping_key):
+ for (sales_stage, _based_on), rows in groupby(
+ sorted(self.query_result, key=grouping_key), key=grouping_key
+ ):
self.grouped_data.append(
{
"sales_stage": sales_stage,
diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
index e5d34231a87..9c19e9f0148 100644
--- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
+++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
@@ -122,7 +122,9 @@ class SalesPipelineAnalytics:
self.grouped_data = []
grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by]) # noqa
- for (pipeline_by, period_by), rows in groupby(self.query_result, grouping_key):
+ for (pipeline_by, period_by), rows in groupby(
+ sorted(self.query_result, key=grouping_key), grouping_key
+ ):
self.grouped_data.append(
{
self.pipeline_by: pipeline_by,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 1f6363d56cb..3e17a07c5c1 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -357,6 +357,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles
erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
+erpnext.patches.v14_0.update_pos_return_ledger_entries
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
diff --git a/erpnext/patches/v14_0/update_pos_return_ledger_entries.py b/erpnext/patches/v14_0/update_pos_return_ledger_entries.py
new file mode 100644
index 00000000000..60e867d1bcb
--- /dev/null
+++ b/erpnext/patches/v14_0/update_pos_return_ledger_entries.py
@@ -0,0 +1,61 @@
+import frappe
+from frappe import qb
+
+
+def execute():
+ sinv = qb.DocType("Sales Invoice")
+ pos_returns_without_self = (
+ qb.from_(sinv)
+ .select(sinv.name)
+ .where(
+ sinv.docstatus.eq(1)
+ & sinv.is_pos.eq(1)
+ & sinv.is_return.eq(1)
+ & sinv.return_against.notnull()
+ & sinv.update_outstanding_for_self.eq(0)
+ )
+ .run()
+ )
+ if pos_returns_without_self:
+ pos_returns_without_self = [x[0] for x in pos_returns_without_self]
+
+ gle = qb.DocType("GL Entry")
+ gl_against_references = (
+ qb.from_(gle)
+ .select(gle.voucher_no, gle.against_voucher)
+ .where(
+ gle.voucher_no.isin(pos_returns_without_self)
+ & gle.against_voucher.notnull()
+ & gle.against_voucher.eq(gle.voucher_no)
+ & gle.is_cancelled.eq(0)
+ )
+ .run()
+ )
+
+ _vouchers = list(set([x[0] for x in gl_against_references]))
+ invoice_return_against = (
+ qb.from_(sinv)
+ .select(sinv.name, sinv.return_against)
+ .where(sinv.name.isin(_vouchers) & sinv.return_against.notnull())
+ .orderby(sinv.name)
+ .run()
+ )
+
+ valid_references = set(invoice_return_against)
+ actual_references = set(gl_against_references)
+
+ invalid_references = actual_references.difference(valid_references)
+
+ if invalid_references:
+ # Repost Accounting Ledger
+ pos_for_reposting = (
+ qb.from_(sinv)
+ .select(sinv.company, sinv.name)
+ .where(sinv.name.isin([x[0] for x in invalid_references]))
+ .run(as_dict=True)
+ )
+ for x in pos_for_reposting:
+ ral = frappe.new_doc("Repost Accounting Ledger")
+ ral.company = x.company
+ ral.append("vouchers", {"voucher_type": "Sales Invoice", "voucher_no": x.name})
+ ral.save().submit()
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index bf7e5b9e48f..98c1434e4d0 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1139,6 +1139,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"],
"Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
"Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"],
+ "Sales Order Item": ["prevdoc_docname", "quotation_item"],
};
const mappped_fields = mapped_item_field_map[item.doctype] || [];
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js
index 954705b5c32..d9a0be9eedd 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.js
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.js
@@ -248,7 +248,7 @@ erpnext.SalesFunnel = class SalesFunnel {
context.fill();
// draw text
- context.fillStyle = "";
+ context.fillStyle = getComputedStyle(document.body).getPropertyValue("--text-color");
context.textBaseline = "middle";
context.font = "1.1em sans-serif";
context.fillText(__(title), width + 20, y_mid);
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py
index 24bb0d2fe71..73deb8fe66b 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.py
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.py
@@ -93,7 +93,7 @@ def get_opp_by_lead_source(from_date, to_date, company):
summary = {}
sales_stages = set()
group_key = lambda o: (o["source"], o["sales_stage"]) # noqa
- for (source, sales_stage), rows in groupby(cp_opportunities, group_key):
+ for (source, sales_stage), rows in groupby(sorted(cp_opportunities, key=group_key), group_key):
summary.setdefault(source, {})[sales_stage] = sum(r["compound_amount"] for r in rows)
sales_stages.add(sales_stage)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 2a5e527f591..aa25d625ffe 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -837,7 +837,8 @@ def create_delivery_note(source_name, target_doc=None):
)
)
- for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]):
+ group_key = lambda so: so["customer"] # noqa
+ for customer, rows in groupby(sorted(sales_orders, key=group_key), key=group_key):
sales_dict[customer] = {row.sales_order for row in rows}
if sales_dict:
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 2864c00864b..8df785a0c1f 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -4,10 +4,10 @@
import json
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.model.meta import get_field_precision
from frappe.query_builder.functions import Sum
-from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
+from frappe.utils import cint, cstr, flt, format_date, get_link_to_form, getdate, now, nowdate
import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@@ -542,11 +542,31 @@ class update_entries_after:
return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
+ def validate_previous_sle_qty(self, sle):
+ previous_sle = self.data[sle.warehouse].previous_sle
+ if previous_sle and previous_sle.get("qty_after_transaction") < 0 and sle.get("actual_qty") > 0:
+ frappe.msgprint(
+ _(
+ "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the documentation."
+ ).format(
+ bold(sle.item_code),
+ bold(sle.warehouse),
+ bold(format_date(previous_sle.posting_date)),
+ sle.voucher_no,
+ bold(format_date(previous_sle.posting_date)),
+ bold(previous_sle.posting_time),
+ ),
+ title=_("Warning on Negative Stock"),
+ indicator="blue",
+ )
+
def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
# previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse]
+
+ self.validate_previous_sle_qty(sle)
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 0905c06896c..e93fd851fa0 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1351,7 +1351,7 @@ Lead to Quotation,Vom Lead zum Angebot,
Learn,Lernen,
Leave Management,Urlaube verwalten,
Leave and Attendance,Urlaub und Anwesenheit,
-Leave application {0} already exists against the student {1},Verlassen der Anwendung {0} ist bereits für den Schüler {1} vorhanden,
+Leave application {0} already exists against the student {1},Abwesenheitsantrag {0} existiert bereits für den Schüler {1},
Leaves has been granted sucessfully,Urlaub wurde genehmigt,
Leaves must be allocated in multiples of 0.5,"Abwesenheiten müssen ein Vielfaches von 0,5 sein",
Ledger,Hauptbuch,
@@ -5655,10 +5655,10 @@ Guardian Details,Erziehungsberechtigten-Details,
Guardians,Erziehungsberechtigte,
Sibling Details,Geschwister-Details,
Siblings,Geschwister,
-Exit,Verlassen,
+Exit,Austritt,
Date of Leaving,Austrittsdatum,
Leaving Certificate Number,Leaving Certificate Nummer,
-Reason For Leaving,Grund für das Verlassen,
+Reason For Leaving,Grund für den Austritt,
Student Admission,Studenten Eintritt,
Admission Start Date,Stichtag zum Zulassungsbeginn,
Admission End Date,Stichtag für Zulassungsende,