Merge pull request #40260 from ruthra-kumar/support_payment_against_payment
refactor: support payment against reverse payment reconciliation
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index c8a35eb..85e9d16 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1588,6 +1588,12 @@
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
+ def test_zero_qty_return_invoice_with_stock_effect(self):
+ cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True)
+ cr_note.update_stock = True
+ cr_note.items[0].qty = 0
+ self.assertRaises(frappe.ValidationError, cr_note.save)
+
def test_return_invoice_with_account_mismatch(self):
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index a0f8af5..de49139 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -182,8 +182,10 @@
}
# check invoice grand total and invoiced column's value for 3 payment terms
- si = self.create_sales_invoice(no_payment_schedule=True)
- name = si.name
+ si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
+ si.set_posting_time = True
+ si.posting_date = add_days(today(), -1)
+ si.save().submit()
report = execute(filters)
@@ -207,30 +209,42 @@
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
cr_note = self.create_credit_note(si.name, do_not_submit=True)
- cr_note.posting_date = add_days(today(), 1)
cr_note.update_outstanding_for_self = True
cr_note.save().submit()
report = execute(filters)
expected_data_after_credit_note = [
- [100.0, 100.0, 40.0, 0.0, 60.0, self.debit_to],
- [0, 0, 100.0, 0.0, -100.0, self.debit_to],
+ [100.0, 100.0, 40.0, 0.0, 60.0, si.name],
+ [0, 0, 100.0, 0.0, -100.0, cr_note.name],
]
self.assertEqual(len(report[1]), 2)
- for i in range(2):
- row = report[1][i - 1]
- # row = report[1][0]
- self.assertEqual(
- expected_data_after_credit_note[i - 1],
- [
- row.invoice_grand_total,
- row.invoiced,
- row.paid,
- row.credit_note,
- row.outstanding,
- row.party_account,
- ],
- )
+ si_row = [
+ [
+ row.invoice_grand_total,
+ row.invoiced,
+ row.paid,
+ row.credit_note,
+ row.outstanding,
+ row.voucher_no,
+ ]
+ for row in report[1]
+ if row.voucher_no == si.name
+ ][0]
+
+ cr_note_row = [
+ [
+ row.invoice_grand_total,
+ row.invoiced,
+ row.paid,
+ row.credit_note,
+ row.outstanding,
+ row.voucher_no,
+ ]
+ for row in report[1]
+ if row.voucher_no == cr_note.name
+ ][0]
+ self.assertEqual(expected_data_after_credit_note[0], si_row)
+ self.assertEqual(expected_data_after_credit_note[1], cr_note_row)
def test_payment_againt_po_in_receivable_report(self):
"""
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index b7e687d..b16e073 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -169,6 +169,13 @@
if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero()
+ if (
+ self.doctype in ["Sales Invoice", "Purchase Invoice"]
+ and self.get("is_return")
+ and self.get("update_stock")
+ ):
+ self.validate_zero_qty_for_return_invoices_with_stock()
+
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
@@ -603,23 +610,31 @@
)
def validate_due_date(self):
- if self.get("is_pos"):
+ if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
return
from erpnext.accounts.party import validate_due_date
- if self.doctype == "Sales Invoice":
+ posting_date = (
+ self.posting_date if self.doctype == "Sales Invoice" else (self.bill_date or self.posting_date)
+ )
+
+ # skip due date validation for records via Data Import
+ if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date):
+ self.due_date = posting_date
+
+ elif self.doctype == "Sales Invoice":
if not self.due_date:
frappe.throw(_("Due Date is mandatory"))
validate_due_date(
- self.posting_date,
+ posting_date,
self.due_date,
self.payment_terms_template,
)
elif self.doctype == "Purchase Invoice":
validate_due_date(
- self.bill_date or self.posting_date,
+ posting_date,
self.due_date,
self.bill_date,
self.payment_terms_template,
@@ -1045,6 +1060,18 @@
else:
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
+ def validate_zero_qty_for_return_invoices_with_stock(self):
+ rows = []
+ for item in self.items:
+ if not flt(item.qty):
+ rows.append(item)
+ if rows:
+ frappe.throw(
+ _(
+ "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}"
+ ).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows])))
+ )
+
def validate_qty_is_not_zero(self):
for item in self.items:
if self.doctype == "Purchase Receipt" and item.rejected_qty:
@@ -2709,14 +2736,20 @@
else:
q = q.where(journal_acc.debit_in_account_currency > 0)
+ reference_or_condition = []
+
if include_unallocated:
- q = q.where((journal_acc.reference_name.isnull()) | (journal_acc.reference_name == ""))
+ reference_or_condition.append(journal_acc.reference_name.isnull())
+ reference_or_condition.append(journal_acc.reference_name == "")
if order_list:
- q = q.where(
+ reference_or_condition.append(
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
)
+ if reference_or_condition:
+ q = q.where(Criterion.any(reference_or_condition))
+
q = q.orderby(journal_entry.posting_date)
journal_entries = q.run(as_dict=True)
diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
index f2f1e4c..42bdf57 100644
--- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
+++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
@@ -197,6 +197,8 @@
):
details[p_key] += r.get(qty_or_amount_field, 0)
details[variance_key] = details.get(p_key) - details.get(target_key)
+ else:
+ details[variance_key] = details.get(p_key) - details.get(target_key)
details["total_achieved"] += details.get(p_key)
details["total_variance"] = details.get("total_achieved") - details.get("total_target")
@@ -209,31 +211,32 @@
parent_doc = frappe.qb.DocType(filters.get("doctype"))
child_doc = frappe.qb.DocType(filters.get("doctype") + " Item")
- sales_team = frappe.qb.DocType("Sales Team")
- query = (
- frappe.qb.from_(parent_doc)
- .inner_join(child_doc)
- .on(child_doc.parent == parent_doc.name)
- .inner_join(sales_team)
- .on(sales_team.parent == parent_doc.name)
- .select(
- child_doc.item_group,
- (child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"),
- (child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"),
- sales_team.sales_person,
- parent_doc[date_field],
- )
- .where(
- (parent_doc.docstatus == 1)
- & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
- )
- )
+ query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name)
if sales_field == "sales_person":
- query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data))
+ sales_team = frappe.qb.DocType("Sales Team")
+ stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100
+ net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100
+ sales_field_col = sales_team[sales_field]
+
+ query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name)
else:
- query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data))
+ stock_qty = child_doc.stock_qty
+ net_amount = child_doc.base_net_amount
+ sales_field_col = parent_doc[sales_field]
+
+ query = query.select(
+ child_doc.item_group,
+ parent_doc[date_field],
+ (stock_qty).as_("stock_qty"),
+ (net_amount).as_("base_net_amount"),
+ sales_field_col,
+ ).where(
+ (parent_doc.docstatus == 1)
+ & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
+ & (sales_field_col.isin(sales_users_or_territory_data))
+ )
return query.run(as_dict=True)
diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py
new file mode 100644
index 0000000..1718668
--- /dev/null
+++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py
@@ -0,0 +1,57 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import flt, nowdate
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import (
+ execute,
+)
+from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
+ create_sales_target_doc,
+ create_target_distribution,
+)
+
+
+class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase):
+ def setUp(self):
+ self.fiscal_year = get_fiscal_year(nowdate())[0]
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def test_achieved_target_and_variance_for_partner(self):
+ # Create a Target Distribution
+ distribution = create_target_distribution(self.fiscal_year)
+
+ # Create Sales Partner with targets for the current fiscal year
+ sales_partner = create_sales_target_doc(
+ "Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name
+ )
+
+ # Create a Sales Invoice for the Partner
+ si = create_sales_invoice(
+ rate=1000,
+ qty=20,
+ do_not_submit=True,
+ )
+ si.sales_partner = sales_partner
+ si.commission_rate = 5
+ si.submit()
+
+ # Check Achieved Target and Variance for the Sales Partner
+ result = execute(
+ frappe._dict(
+ {
+ "fiscal_year": self.fiscal_year,
+ "doctype": "Sales Invoice",
+ "period": "Yearly",
+ "target_on": "Quantity",
+ }
+ )
+ )[1]
+ row = frappe._dict(result[0])
+ self.assertSequenceEqual(
+ [flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)],
+ [50, 20, -30],
+ )
diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
index 4ae5d2b..73ae6d0 100644
--- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
+++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
@@ -18,17 +18,17 @@
def test_achieved_target_and_variance(self):
# Create a Target Distribution
- distribution = frappe.new_doc("Monthly Distribution")
- distribution.distribution_id = "Target Report Distribution"
- distribution.fiscal_year = self.fiscal_year
- distribution.get_months()
- distribution.insert()
+ distribution = create_target_distribution(self.fiscal_year)
- # Create sales people with targets
- person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name)
- person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name)
+ # Create sales people with targets for the current fiscal year
+ person_1 = create_sales_target_doc(
+ "Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name
+ )
+ person_2 = create_sales_target_doc(
+ "Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name
+ )
- # Create a Sales Order with 50-50 contribution
+ # Create a Sales Order with 50-50 contribution between both Sales people
so = make_sales_order(
rate=1000,
qty=20,
@@ -69,10 +69,20 @@
)
-def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id):
- sales_person = frappe.new_doc("Sales Person")
- sales_person.sales_person_name = sales_person_name
- sales_person.append(
+def create_target_distribution(fiscal_year):
+ distribution = frappe.new_doc("Monthly Distribution")
+ distribution.distribution_id = "Target Report Distribution"
+ distribution.fiscal_year = fiscal_year
+ distribution.get_months()
+ return distribution.insert()
+
+
+def create_sales_target_doc(
+ sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id
+):
+ sales_target_doc = frappe.new_doc(sales_field_dt)
+ sales_target_doc.set(sales_field_name, sales_field_value)
+ sales_target_doc.append(
"targets",
{
"fiscal_year": fiscal_year,
@@ -81,4 +91,6 @@
"distribution_id": distribution_id,
},
)
- return sales_person.insert()
+ if sales_field_dt == "Sales Partner":
+ sales_target_doc.commission_rate = 5
+ return sales_target_doc.insert()