Compare commits

...

11 Commits

Author SHA1 Message Date
Nabin Hait
486a1c78b0 Merge pull request #56882 from frappe/fix/pos-invoice-reset-mop-attributeerror
fix: reset_mode_of_payments raises AttributeError on a POS Invoice
2026-07-04 19:37:40 +05:30
Nabin Hait
a1f6ae56ff fix: removed unused import
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 18:39:52 +05:30
Nabin Hait
14c1b02025 Merge pull request #56879 from frappe/chore/test-contract-template
test: add coverage for Contract Template
2026-07-04 17:58:32 +05:30
Nabin Hait
3b335db64c Merge pull request #56878 from frappe/chore/test-crm-settings
test: add coverage for CRM Settings
2026-07-04 17:58:18 +05:30
Nabin Hait
99ed620dad fix: reset_mode_of_payments raises AttributeError on POS Invoice 2026-07-04 17:54:46 +05:30
Nabin Hait
4b4afe12df Merge pull request #56832 from frappe/chore/test-repost-payment-ledger
test: add coverage for Repost Payment Ledger
2026-07-04 17:45:10 +05:30
Nabin Hait
34f3870f2a test: add coverage for Contract Template validation and rendering 2026-07-04 17:28:28 +05:30
Nabin Hait
7f903b63dd test: add coverage for CRM Settings sync and contact-us guards 2026-07-04 17:26:58 +05:30
Nabin Hait
4c26ec8cd9 test: make add_manually test distinguish manual mode from auto-loading 2026-07-04 16:48:52 +05:30
Nabin Hait
f68f53dec0 test: cover on-cutoff boundary in voucher loading 2026-07-04 16:48:52 +05:30
Nabin Hait
9901746e02 test: add coverage for Repost Payment Ledger 2026-07-04 16:48:52 +05:30
5 changed files with 155 additions and 10 deletions

View File

@@ -0,0 +1,34 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# Regression test for https://github.com/frappe/erpnext/issues/56501
# AttributeError: 'POSInvoice' object has no attribute 'is_created_using_pos'
# when calling reset_mode_of_payments on a draft POS Invoice.
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import (
POSInvoiceTestMixin,
create_pos_invoice,
)
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
class TestPOSInvoiceResetModeOfPayments(POSInvoiceTestMixin):
def setUp(self):
super().setUp()
create_opening_entry(self.pos_profile, self.test_user.name)
def test_reset_mode_of_payments_does_not_raise_attribute_error(self):
"""Calling reset_mode_of_payments on a draft POS Invoice must not raise
AttributeError for the missing is_created_using_pos attribute.
update_multi_mode_option accesses doc.is_created_using_pos, which is a
field on SalesInvoice but does not exist on POSInvoice, causing the error
reported in #56501 when a user tries to edit a saved draft order.
"""
inv = create_pos_invoice(do_not_submit=True)
# This call must not raise AttributeError on the missing field.
inv.reset_mode_of_payments()
# Payments should have been repopulated from the POS profile.
self.assertTrue(len(inv.payments) > 0, "Payments should be populated after reset")

View File

@@ -1,11 +1,55 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import frappe
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import ERPNextTestSuite
COMPANY = "_Test Company"
class TestRepostPaymentLedger(ERPNextTestSuite):
pass
"""Repost Payment Ledger auto-selects submitted vouchers on/after a cutoff date
(unless rows are added manually) and queues them for a ledger rebuild."""
def setUp(self):
frappe.set_user("Administrator")
def make_repost(self, **args):
args = frappe._dict(args)
doc = frappe.new_doc("Repost Payment Ledger")
doc.company = COMPANY
doc.posting_date = args.get("posting_date", "2026-06-01")
doc.voucher_type = args.get("voucher_type", "Sales Invoice")
doc.add_manually = args.get("add_manually", 0)
return doc
def test_loads_submitted_vouchers_on_or_after_cutoff(self):
after_cutoff = create_sales_invoice(company=COMPANY, posting_date="2026-06-15", rate=100, qty=1)
on_cutoff = create_sales_invoice(company=COMPANY, posting_date="2026-06-01", rate=100, qty=1)
before_cutoff = create_sales_invoice(company=COMPANY, posting_date="2026-01-15", rate=100, qty=1)
doc = self.make_repost(posting_date="2026-06-01", voucher_type="Sales Invoice")
doc.save() # before_validate loads the vouchers and sets status
loaded = {v.voucher_no for v in doc.repost_vouchers}
self.assertIn(after_cutoff.name, loaded)
# the filter is >= so an invoice posted exactly on the cutoff is included
self.assertIn(on_cutoff.name, loaded)
self.assertNotIn(before_cutoff.name, loaded)
self.assertEqual(doc.repost_status, "Queued")
def test_add_manually_preserves_user_rows(self):
# manually add a BEFORE-cutoff invoice (which the filter would never load) while a
# matching after-cutoff invoice also exists. If auto-loading wrongly ran it would
# drop the manual row and pull the after-cutoff one, so this distinguishes the modes.
manual_si = create_sales_invoice(company=COMPANY, posting_date="2026-01-15", rate=100, qty=1)
create_sales_invoice(company=COMPANY, posting_date="2026-06-15", rate=100, qty=1)
doc = self.make_repost(add_manually=1, posting_date="2026-06-01")
doc.append("repost_vouchers", {"voucher_type": "Sales Invoice", "voucher_no": manual_si.name})
doc.save()
rows = [(v.voucher_type, v.voucher_no) for v in doc.repost_vouchers]
self.assertEqual(rows, [("Sales Invoice", manual_si.name)])

View File

@@ -344,7 +344,9 @@ def update_multi_mode_option(doc, pos_profile) -> None:
payment.account = payment_mode.default_account
payment.type = payment_mode.type
mop_refetched = bool(doc.payments) and not doc.is_created_using_pos
# is_created_using_pos exists on Sales Invoice but not POS Invoice; use get() so this
# shared helper doesn't raise AttributeError when called on a POS Invoice
mop_refetched = bool(doc.payments) and not doc.get("is_created_using_pos")
doc.set("payments", [])
invalid_modes = []

View File

@@ -1,8 +1,43 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.crm.doctype.contract_template.contract_template import get_contract_template
from erpnext.tests.utils import ERPNextTestSuite
class TestContractTemplate(ERPNextTestSuite):
pass
"""Contract Template validates its Jinja terms and renders them against a doc."""
def test_malformed_contract_terms_are_rejected(self):
doc = frappe.new_doc("Contract Template")
doc.contract_terms = "{% for x in %}" # invalid Jinja
self.assertRaises(frappe.ValidationError, doc.validate)
# a valid template, and no template at all, both pass
doc.contract_terms = "Party: {{ party_name }}"
doc.validate()
doc.contract_terms = None
doc.validate()
def test_get_contract_template_renders_terms(self):
template = frappe.get_doc(
{
"doctype": "Contract Template",
"title": "_Test Contract Template",
"contract_terms": "Party: {{ party_name }}",
}
).insert()
result = get_contract_template(template.name, {"party_name": "Acme"})
self.assertEqual(result["contract_terms"], "Party: Acme")
self.assertEqual(result["contract_template"].name, template.name)
def test_get_contract_template_without_terms_returns_none(self):
template = frappe.get_doc(
{"doctype": "Contract Template", "title": "_Test Empty Contract Template"}
).insert()
result = get_contract_template(template.name, {})
self.assertIsNone(result["contract_terms"])

View File

@@ -1,9 +1,39 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import frappe
from erpnext.tests.utils import ERPNextTestSuite
class TestCRMSettings(ERPNextTestSuite):
pass
"""CRM Settings guards its Frappe-CRM sync and Contact-Us opportunity toggles."""
def make_settings(self, **fields):
doc = frappe.new_doc("CRM Settings")
doc.update(fields)
return doc
def test_data_sync_requires_at_least_one_allowed_user(self):
doc = self.make_settings(enable_frappe_crm_data_synchronization=1)
self.assertRaises(frappe.ValidationError, doc.validate_allowed_users)
# adding a user satisfies the check
doc.append("allowed_users", {"user": "Administrator"})
doc.validate_allowed_users()
def test_disabling_sync_clears_allowed_users(self):
doc = self.make_settings(enable_frappe_crm_data_synchronization=0)
doc.append("allowed_users", {"user": "Administrator"})
doc.clear_allowed_users()
self.assertEqual(doc.allowed_users, [])
# while sync is on, the rows are kept
enabled = self.make_settings(enable_frappe_crm_data_synchronization=1)
enabled.append("allowed_users", {"user": "Administrator"})
enabled.clear_allowed_users()
self.assertEqual(len(enabled.allowed_users), 1)
@ERPNextTestSuite.change_settings("Contact Us Settings", {"is_disabled": 1})
def test_opportunity_from_contact_us_needs_the_form_enabled(self):
doc = self.make_settings(enable_opportunity_creation_from_contact_us=1)
self.assertRaises(frappe.ValidationError, doc.validate_enable_opportunity_creation_from_contact_us)