Merge branch 'develop' of github.com:aerele/erpnext into update_actual_qty

This commit is contained in:
Bhavan23
2024-10-08 16:23:25 +05:30
544 changed files with 207737 additions and 194594 deletions

8
.github/stale.yml vendored
View File

@@ -12,6 +12,14 @@ exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Skip the stale action for draft PRs
exemptDraftPr: true
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- hotfix
- no-stale
pulls:
daysUntilStale: 15
daysUntilClose: 3

View File

@@ -120,7 +120,7 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds 4 --build-number ${{ matrix.container }}'
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
env:
TYPE: server
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}

2
.gitignore vendored
View File

@@ -17,3 +17,5 @@ __pycache__
.helix/
node_modules/
.backportrc.json
# Aider AI Chat
.aider*

View File

@@ -124,7 +124,7 @@
"label": "Account Type",
"oldfieldname": "account_type",
"oldfieldtype": "Select",
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
"search_index": 1
},
{
@@ -194,7 +194,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2024-06-27 16:23:04.444354",
"modified": "2024-08-19 15:19:11.095045",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",

View File

@@ -60,6 +60,7 @@ class Account(NestedSet):
"Payable",
"Receivable",
"Round Off",
"Round Off for Opening",
"Stock",
"Stock Adjustment",
"Stock Received But Not Billed",
@@ -101,14 +102,12 @@ class Account(NestedSet):
self.name = get_autoname_with_number(self.account_number, self.account_name, self.company)
def validate(self):
from erpnext.accounts.utils import validate_field_number
if frappe.local.flags.allow_unverified_charts:
return
self.validate_parent()
self.validate_parent_child_account_type()
self.validate_root_details()
validate_field_number("Account", self.name, self.account_number, self.company, "account_number")
self.validate_account_number()
self.validate_group_or_ledger()
self.set_root_and_report_type()
self.validate_mandatory()
@@ -309,6 +308,22 @@ class Account(NestedSet):
if frappe.db.get_value("GL Entry", {"account": self.name}):
frappe.throw(_("Currency can not be changed after making entries using some other currency"))
def validate_account_number(self, account_number=None):
if not account_number:
account_number = self.account_number
if account_number:
account_with_same_number = frappe.db.get_value(
"Account",
{"account_number": account_number, "company": self.company, "name": ["!=", self.name]},
)
if account_with_same_number:
frappe.throw(
_("Account Number {0} already used in account {1}").format(
account_number, account_with_same_number
)
)
def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name):
for company in descendants:
company_bold = frappe.bold(company)
@@ -462,19 +477,6 @@ def get_account_autoname(account_number, account_name, company):
return " - ".join(parts)
def validate_account_number(name, account_number, company):
if account_number:
account_with_same_number = frappe.db.get_value(
"Account", {"account_number": account_number, "company": company, "name": ["!=", name]}
)
if account_with_same_number:
frappe.throw(
_("Account Number {0} already used in account {1}").format(
account_number, account_with_same_number
)
)
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
account = frappe.get_cached_doc("Account", name)
@@ -515,7 +517,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
frappe.throw(message, title=_("Rename Not Allowed"))
validate_account_number(name, account_number, account.company)
account.validate_account_number(account_number)
if account_number:
frappe.db.set_value("Account", name, "account_number", account_number.strip())
else:

View File

@@ -109,7 +109,8 @@
"Utility Expenses": {},
"Write Off": {},
"Exchange Gain/Loss": {},
"Gain/Loss on Asset Disposal": {}
"Gain/Loss on Asset Disposal": {},
"Impairment": {}
},
"root_type": "Expense"
},
@@ -132,7 +133,8 @@
"Source of Funds (Liabilities)": {
"Capital Account": {
"Reserves and Surplus": {},
"Shareholders Funds": {}
"Shareholders Funds": {},
"Revaluation Surplus": {}
},
"Current Liabilities": {
"Accounts Payable": {

View File

@@ -72,6 +72,7 @@ def get():
_("Write Off"): {},
_("Exchange Gain/Loss"): {},
_("Gain/Loss on Asset Disposal"): {},
_("Impairment"): {},
},
"root_type": "Expense",
},
@@ -104,6 +105,7 @@ def get():
_("Dividends Paid"): {"account_type": "Equity"},
_("Opening Balance Equity"): {"account_type": "Equity"},
_("Retained Earnings"): {"account_type": "Equity"},
_("Revaluation Surplus"): {"account_type": "Equity"},
"root_type": "Equity",
},
}

View File

@@ -1,11 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.test_runner import make_test_records
from frappe.tests import IntegrationTestCase
from frappe.tests.utils import make_test_records
from frappe.utils import nowdate
from erpnext.accounts.doctype.account.account import (
@@ -18,7 +17,7 @@ from erpnext.stock import get_company_default_inventory_account, get_warehouse_a
test_dependencies = ["Company"]
class TestAccount(unittest.TestCase):
class TestAccount(IntegrationTestCase):
def test_rename_account(self):
if not frappe.db.exists("Account", "1210 - Debtors - _TC"):
acc = frappe.new_doc("Account")

View File

@@ -2,8 +2,17 @@
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
class TestAccountClosingBalance(FrappeTestCase):
class UnitTestAccountClosingBalance(UnitTestCase):
"""
Unit tests for AccountClosingBalance.
Use this class for testing individual functions and methods.
"""
pass
class TestAccountClosingBalance(IntegrationTestCase):
pass

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -11,7 +11,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
test_dependencies = ["Cost Center", "Location", "Warehouse", "Department"]
class TestAccountingDimension(unittest.TestCase):
class TestAccountingDimension(IntegrationTestCase):
def setUp(self):
create_dimension()

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import (
@@ -15,7 +15,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
test_dependencies = ["Item"]
class TestAccountingPeriod(unittest.TestCase):
class TestAccountingPeriod(IntegrationTestCase):
def test_overlap(self):
ap1 = create_accounting_period(
start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC"

View File

@@ -1,9 +1,10 @@
import unittest
import frappe
from frappe.tests import IntegrationTestCase
class TestAccountsSettings(unittest.TestCase):
class TestAccountsSettings(IntegrationTestCase):
def tearDown(self):
# Just in case `save` method succeeds, we need to take things back to default so that other tests
# don't break

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestBank(unittest.TestCase):
class TestBank(IntegrationTestCase):
pass

View File

@@ -208,8 +208,54 @@
"label": "Disabled"
}
],
"links": [],
"modified": "2024-03-27 13:06:37.049542",
"links": [
{
"group": "Transactions",
"link_doctype": "Payment Request",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Payment Order",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Bank Guarantee",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Payroll Entry",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Bank Transaction",
"link_fieldname": "bank_account"
},
{
"group": "Accounting",
"link_doctype": "Payment Entry",
"link_fieldname": "bank_account"
},
{
"group": "Accounting",
"link_doctype": "Journal Entry",
"link_fieldname": "bank_account"
},
{
"group": "Party",
"link_doctype": "Customer",
"link_fieldname": "default_bank_account"
},
{
"group": "Party",
"link_doctype": "Supplier",
"link_fieldname": "default_bank_account"
}
],
"modified": "2024-09-24 06:57:41.292970",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",

View File

@@ -1,20 +0,0 @@
from frappe import _
def get_data():
return {
"fieldname": "bank_account",
"non_standard_fieldnames": {
"Customer": "default_bank_account",
"Supplier": "default_bank_account",
},
"transactions": [
{
"label": _("Payments"),
"items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
},
{"label": _("Party"), "items": ["Customer", "Supplier"]},
{"items": ["Bank Guarantee"]},
{"items": ["Journal Entry"]},
],
}

View File

@@ -1,15 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe import ValidationError
from frappe.tests import IntegrationTestCase
# test_records = frappe.get_test_records('Bank Account')
class TestBankAccount(unittest.TestCase):
class TestBankAccount(IntegrationTestCase):
def test_validate_iban(self):
valid_ibans = [
"GB82 WEST 1234 5698 7654 32",

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestBankAccountSubtype(unittest.TestCase):
class TestBankAccountSubtype(IntegrationTestCase):
pass

View File

@@ -1,9 +1,10 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
from frappe.tests import IntegrationTestCase
class TestBankAccountType(unittest.TestCase):
class TestBankAccountType(IntegrationTestCase):
pass

View File

@@ -108,8 +108,18 @@ class BankClearance(Document):
if not d.clearance_date:
d.clearance_date = None
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
payment_entry.db_set("clearance_date", d.clearance_date)
if d.payment_document == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
"clearance_date",
d.clearance_date,
)
else:
frappe.db.set_value(
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
)
clearance_date_updated = True

View File

@@ -1,21 +1,34 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
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.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
class TestBankClearance(unittest.TestCase):
class TestBankClearance(IntegrationTestCase):
@classmethod
def setUpClass(cls):
create_warehouse(
warehouse_name="_Test Warehouse",
properties={"parent_warehouse": "All Warehouses - _TC"},
company="_Test Company",
)
create_item("_Test Item")
create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company")
clear_payment_entries()
clear_loan_transactions()
clear_pos_sales_invoices()
make_bank_account()
add_transactions()
@@ -83,11 +96,41 @@ class TestBankClearance(unittest.TestCase):
bank_clearance.get_payment_entries()
self.assertEqual(len(bank_clearance.payment_entries), 3)
def test_update_clearance_date_on_si(self):
sales_invoice = make_pos_sales_invoice()
date = getdate()
bank_clearance = frappe.get_doc("Bank Clearance")
bank_clearance.account = "_Test Bank Clearance - _TC"
bank_clearance.from_date = add_months(date, -1)
bank_clearance.to_date = date
bank_clearance.include_pos_transactions = 1
bank_clearance.get_payment_entries()
self.assertNotEqual(len(bank_clearance.payment_entries), 0)
for payment in bank_clearance.payment_entries:
if payment.payment_entry == sales_invoice.name:
payment.clearance_date = date
bank_clearance.update_clearance_date()
si_clearance_date = frappe.db.get_value(
"Sales Invoice Payment",
{"parent": sales_invoice.name, "account": bank_clearance.account},
"clearance_date",
)
self.assertEqual(si_clearance_date, date)
def clear_payment_entries():
frappe.db.delete("Payment Entry")
def clear_pos_sales_invoices():
frappe.db.delete("Sales Invoice", {"is_pos": 1})
@if_lending_app_installed
def clear_loan_transactions():
for dt in [
@@ -115,9 +158,45 @@ def add_transactions():
def make_payment_entry():
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
supplier = create_supplier(supplier_name="_Test Supplier")
pi = make_purchase_invoice(
supplier=supplier,
supplier_warehouse="_Test Warehouse - _TC",
expense_account="Cost of Goods Sold - _TC",
uom="Nos",
qty=1,
rate=690,
)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()
def make_pos_sales_invoice():
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
mode_of_payment.append(
"accounts", {"company": "_Test Company", "default_account": "_Test Bank Clearance - _TC"}
)
mode_of_payment.save()
customer = make_customer(customer="_Test Customer")
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
si.set("payments", [])
si.append(
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
)
si.insert()
si.submit()
return si

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestBankGuarantee(unittest.TestCase):
class TestBankGuarantee(IntegrationTestCase):
pass

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, today
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
@@ -15,7 +15,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
class TestBankReconciliationTool(AccountsTestMixin, IntegrationTestCase):
def setUp(self):
self.create_company()
self.create_customer()

View File

@@ -1,9 +1,10 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
from frappe.tests import IntegrationTestCase
class TestBankStatementImport(unittest.TestCase):
class TestBankStatementImport(IntegrationTestCase):
pass

View File

@@ -2,13 +2,22 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import nowdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
class TestAutoMatchParty(FrappeTestCase):
class UnitTestBankTransaction(UnitTestCase):
"""
Unit tests for BankTransaction.
Use this class for testing individual functions and methods.
"""
pass
class TestAutoMatchParty(IntegrationTestCase):
@classmethod
def setUpClass(cls):
create_bank_account()

View File

@@ -6,7 +6,7 @@ import json
import frappe
from frappe import utils
from frappe.model.docstatus import DocStatus
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
get_linked_payments,
@@ -21,7 +21,16 @@ from erpnext.tests.utils import if_lending_app_installed
test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(FrappeTestCase):
class UnitTestBankTransaction(UnitTestCase):
"""
Unit tests for BankTransaction.
Use this class for testing individual functions and methods.
"""
pass
class TestBankTransaction(IntegrationTestCase):
def setUp(self):
for dt in [
"Bank Transaction",

View File

@@ -2,8 +2,17 @@
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
class TestBisectAccountingStatements(FrappeTestCase):
class UnitTestBisectAccountingStatements(UnitTestCase):
"""
Unit tests for BisectAccountingStatements.
Use this class for testing individual functions and methods.
"""
pass
class TestBisectAccountingStatements(IntegrationTestCase):
pass

View File

@@ -2,8 +2,17 @@
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
class TestBisectNodes(FrappeTestCase):
class UnitTestBisectNodes(UnitTestCase):
"""
Unit tests for BisectNodes.
Use this class for testing individual functions and methods.
"""
pass
class TestBisectNodes(IntegrationTestCase):
pass

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import now_datetime, nowdate
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
@@ -14,7 +14,7 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_pur
test_dependencies = ["Monthly Distribution"]
class TestBudget(unittest.TestCase):
class TestBudget(IntegrationTestCase):
def test_monthly_budget_crossed_ignore(self):
set_total_expense_zero(nowdate(), "cost_center")

View File

@@ -13,13 +13,13 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Campaign",
"options": "Campaign"
"options": "UTM Campaign"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:06:44.142625",
"modified": "2024-06-28 11:04:09.815940",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Campaign Item",
@@ -29,4 +29,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestCashierClosing(unittest.TestCase):
class TestCashierClosing(IntegrationTestCase):
pass

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestChartofAccountsImporter(unittest.TestCase):
class TestChartofAccountsImporter(IntegrationTestCase):
pass

View File

@@ -1,10 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
# test_records = frappe.get_test_records('Cheque Print Template')
class TestChequePrintTemplate(unittest.TestCase):
class TestChequePrintTemplate(IntegrationTestCase):
pass

View File

@@ -1,14 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
test_records = frappe.get_test_records("Cost Center")
class TestCostCenter(unittest.TestCase):
class TestCostCenter(IntegrationTestCase):
def test_cost_center_creation_against_child_node(self):
if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
frappe.get_doc(test_records[1]).insert()

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -17,13 +17,15 @@ from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation impo
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
class TestCostCenterAllocation(unittest.TestCase):
class TestCostCenterAllocation(IntegrationTestCase):
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 @@ class TestCostCenterAllocation(unittest.TestCase):
)
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 @@ class TestCostCenterAllocation(unittest.TestCase):
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 @@ class TestCostCenterAllocation(unittest.TestCase):
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,

View File

@@ -13,6 +13,7 @@
"customer",
"column_break_4",
"coupon_code",
"from_external_ecomm_platform",
"pricing_rule",
"uses",
"valid_from",
@@ -61,11 +62,12 @@
"unique": 1
},
{
"depends_on": "eval !doc.from_external_ecomm_platform",
"fieldname": "pricing_rule",
"fieldtype": "Link",
"label": "Pricing Rule",
"options": "Pricing Rule",
"reqd": 1
"mandatory_depends_on": "eval: !doc.from_external_ecomm_platform",
"options": "Pricing Rule"
},
{
"fieldname": "uses",
@@ -114,13 +116,20 @@
"options": "Coupon Code",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "from_external_ecomm_platform",
"fieldtype": "Check",
"label": "From External Ecomm Platform"
}
],
"links": [],
"modified": "2024-03-27 13:06:47.220931",
"modified": "2024-06-28 06:17:01.833399",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Coupon Code",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -177,4 +186,4 @@
"states": [],
"title_field": "coupon_name",
"track_changes": 1
}
}

View File

@@ -23,8 +23,9 @@ class CouponCode(Document):
coupon_type: DF.Literal["Promotional", "Gift Card"]
customer: DF.Link | None
description: DF.TextEditor | None
from_external_ecomm_platform: DF.Check
maximum_use: DF.Int
pricing_rule: DF.Link
pricing_rule: DF.Link | None
used: DF.Int
valid_from: DF.Date | None
valid_upto: DF.Date | None

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -110,7 +110,7 @@ def test_create_test_data():
coupon_code.insert()
class TestCouponCode(unittest.TestCase):
class TestCouponCode(IntegrationTestCase):
def setUp(self):
test_create_test_data()
@@ -142,3 +142,39 @@ class TestCouponCode(unittest.TestCase):
so.submit()
self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)
def test_coupon_without_max_use(self):
from erpnext.accounts.doctype.pricing_rule.utils import (
update_coupon_code_count,
validate_coupon_code,
)
coupon = frappe.get_doc(
{
"doctype": "Coupon Code",
"coupon_name": "_Test Coupon Without Max Use",
"coupon_code": "TESTUNLIMITED",
"from_external_ecomm_platform": 1, # avoids requirement for pricing rule
"valid_from": frappe.utils.nowdate(),
"maximum_use": 0,
"used": 0,
}
)
coupon.insert(ignore_permissions=True)
# Validate initial state
self.assertEqual(coupon.used, 0)
self.assertEqual(coupon.maximum_use, 0)
# Use coupon multiple times
for _ in range(5):
validate_coupon_code(coupon.name)
update_coupon_code_count(coupon.name, "used")
coupon.reload()
# Check that the coupon is still valid and usage count increased
self.assertEqual(coupon.used, 5)
validate_coupon_code(coupon.name) # This should not raise an error
# Clean up
coupon.delete()

View File

@@ -109,7 +109,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "frankfurter.app/{transaction_date}"
api = "api.frankfurter.app/{transaction_date}"
protocol = "https://"
if use_http:

View File

@@ -1,9 +1,10 @@
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
import unittest
from frappe.tests import IntegrationTestCase
class TestCurrencyExchangeSettings(unittest.TestCase):
class TestCurrencyExchangeSettings(IntegrationTestCase):
pass

View File

@@ -220,19 +220,31 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
@frappe.whitelist()
def get_dunning_letter_text(dunning_type, doc, language=None):
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if isinstance(doc, str):
doc = json.loads(doc)
if not language:
language = doc.get("language")
if language:
filters = {"parent": dunning_type, "language": language}
else:
filters = {"parent": dunning_type, "is_default_language": 1}
letter_text = frappe.db.get_value(
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
)
if letter_text:
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
)
if not letter_text:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
)
if not letter_text:
return {}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}

View File

@@ -1,7 +1,7 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import add_days, nowdate, today
from erpnext import get_default_cost_center
@@ -19,7 +19,16 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
test_dependencies = ["Company", "Cost Center"]
class TestDunning(FrappeTestCase):
class UnitTestDunning(UnitTestCase):
"""
Unit tests for Dunning.
Use this class for testing individual functions and methods.
"""
pass
class TestDunning(IntegrationTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()

View File

@@ -1,9 +1,10 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
from frappe.tests import IntegrationTestCase
class TestDunningType(unittest.TestCase):
class TestDunningType(IntegrationTestCase):
pass

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, flt, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -11,7 +11,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
def setUp(self):
self.create_company()
self.create_usd_receivable_account()
@@ -35,7 +35,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
@@ -88,7 +88,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
)[0]
self.assertEqual(acc_balance.balance, 8500.0)
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
@@ -158,7 +158,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
self.assertEqual(acc_balance.balance, 0.0)
self.assertEqual(acc_balance.balance_in_account_currency, 0.0)
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
@@ -247,7 +247,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0)
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)

View File

@@ -1,14 +1,14 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
class TestFinanceBook(unittest.TestCase):
class TestFinanceBook(IntegrationTestCase):
def test_finance_book(self):
finance_book = create_finance_book()

View File

@@ -1,16 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import now_datetime
test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
class TestFiscalYear(IntegrationTestCase):
def test_extra_year(self):
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")

View File

@@ -1,17 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.model.naming import parse_naming_series
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
class TestGLEntry(unittest.TestCase):
class TestGLEntry(IntegrationTestCase):
def test_round_off_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC")
frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC")

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.account.test_account import create_account
@@ -12,7 +12,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
class TestInvoiceDiscounting(unittest.TestCase):
class TestInvoiceDiscounting(IntegrationTestCase):
def setUp(self):
self.ar_credit = create_account(
account_name="_Test Accounts Receivable Credit",

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestItemTaxTemplate(unittest.TestCase):
class TestItemTaxTemplate(IntegrationTestCase):
pass

View File

@@ -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");
},
});

View File

@@ -195,6 +195,11 @@ class JournalEntry(AccountsController):
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()

View File

@@ -1,10 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.tests.utils import change_settings
from frappe.utils import flt, nowdate
@@ -13,8 +11,19 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInv
from erpnext.exceptions import InvalidAccountCurrency
class TestJournalEntry(unittest.TestCase):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
class UnitTestJournalEntry(UnitTestCase):
"""
Unit tests for JournalEntry.
Use this class for testing individual functions and methods.
"""
pass
class TestJournalEntry(IntegrationTestCase):
@IntegrationTestCase.change_settings(
"Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}
)
def test_journal_entry_with_against_jv(self):
jv_invoice = frappe.copy_doc(test_records[2])
base_jv = frappe.copy_doc(test_records[0])
@@ -515,6 +524,23 @@ class TestJournalEntry(unittest.TestCase):
self.assertEqual(row.debit_in_account_currency, 100)
self.assertEqual(row.credit_in_account_currency, 100)
def test_transaction_exchange_rate_on_journals(self):
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})
jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD", "exchange_rate": 85})
jv.submit()
actual = frappe.db.get_all(
"GL Entry",
filters={"voucher_no": jv.name, "is_cancelled": 0},
fields=["account", "transaction_exchange_rate"],
order_by="account",
)
expected = [
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 1.0},
{"account": "_Test Receivable USD - _TC", "transaction_exchange_rate": 85.0},
]
self.assertEqual(expected, actual)
def make_journal_entry(
account1,

View File

@@ -1,9 +1,10 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
from frappe.tests import IntegrationTestCase
class TestJournalEntryTemplate(unittest.TestCase):
class TestJournalEntryTemplate(IntegrationTestCase):
pass

View File

@@ -3,14 +3,14 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase
from frappe.utils import nowdate
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import run_ledger_health_checks
class TestLedgerHealth(AccountsTestMixin, FrappeTestCase):
class TestLedgerHealth(AccountsTestMixin, IntegrationTestCase):
def setUp(self):
self.create_company()
self.create_customer()

View File

@@ -2,8 +2,17 @@
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
class TestLedgerHealthMonitor(FrappeTestCase):
class UnitTestLedgerHealthMonitor(UnitTestCase):
"""
Unit tests for LedgerHealthMonitor.
Use this class for testing individual functions and methods.
"""
pass
class TestLedgerHealthMonitor(IntegrationTestCase):
pass

View File

@@ -1,14 +1,14 @@
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.ledger_merge.ledger_merge import start_merge
class TestLedgerMerge(unittest.TestCase):
class TestLedgerMerge(IntegrationTestCase):
def test_merge_success(self):
if not frappe.db.exists("Account", "Indirect Expenses - _TC"):
acc = frappe.new_doc("Account")

View File

@@ -15,14 +15,16 @@
"purchase_amount",
"expiry_date",
"posting_date",
"company"
"company",
"discretionary_reason"
],
"fields": [
{
"fieldname": "loyalty_program",
"fieldtype": "Link",
"label": "Loyalty Program",
"options": "Loyalty Program"
"options": "Loyalty Program",
"reqd": 1
},
{
"fieldname": "loyalty_program_tier",
@@ -34,7 +36,8 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer"
"options": "Customer",
"reqd": 1
},
{
"fieldname": "redeem_against",
@@ -46,7 +49,8 @@
"fieldname": "loyalty_points",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Loyalty Points"
"label": "Loyalty Points",
"reqd": 1
},
{
"fieldname": "purchase_amount",
@@ -57,24 +61,28 @@
"fieldname": "expiry_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Expiry Date"
"label": "Expiry Date",
"reqd": 1
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date"
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
"options": "Company",
"reqd": 1
},
{
"fieldname": "invoice_type",
"fieldtype": "Link",
"label": "Invoice Type",
"options": "DocType"
"options": "DocType",
"reqd": 1
},
{
"fieldname": "invoice",
@@ -82,11 +90,16 @@
"in_list_view": 1,
"label": "Invoice",
"options": "invoice_type"
},
{
"fieldname": "discretionary_reason",
"fieldtype": "Data",
"label": "Discretionary Reason"
}
],
"in_create": 1,
"links": [],
"modified": "2024-03-27 13:10:03.015035",
"modified": "2024-07-01 08:51:13.927009",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Loyalty Point Entry",
@@ -123,4 +136,4 @@
"states": [],
"title_field": "customer",
"track_changes": 1
}
}

View File

@@ -18,15 +18,16 @@ class LoyaltyPointEntry(Document):
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link | None
customer: DF.Link | None
expiry_date: DF.Date | None
company: DF.Link
customer: DF.Link
discretionary_reason: DF.Data | None
expiry_date: DF.Date
invoice: DF.DynamicLink | None
invoice_type: DF.Link | None
invoice_type: DF.Link
loyalty_points: DF.Int
loyalty_program: DF.Link | None
loyalty_program: DF.Link
loyalty_program_tier: DF.Data | None
posting_date: DF.Date | None
posting_date: DF.Date
purchase_amount: DF.Currency
redeem_against: DF.Link | None
# end: auto-generated types

View File

@@ -1,8 +1,85 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import today
class TestLoyaltyPointEntry(unittest.TestCase):
pass
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestLoyaltyPointEntry(IntegrationTestCase):
@classmethod
def setUpClass(cls):
# Create test records
create_records()
cls.loyalty_program_name = "Test Single Loyalty"
cls.customer_name = "Test Loyalty Customer"
customer = frappe.get_doc("Customer", cls.customer_name)
customer.db_set("loyalty_program", cls.loyalty_program_name)
@classmethod
def tearDownClass(cls):
# Delete all Loyalty Point Entries
frappe.db.sql("DELETE FROM `tabLoyalty Point Entry` WHERE customer = %s", cls.customer_name)
frappe.db.sql("DELETE FROM `tabSales Invoice` WHERE customer = %s", cls.customer_name)
frappe.db.commit()
# cls.customer.delete()
def create_test_invoice(self, redeem=None):
if redeem:
si = create_sales_invoice(customer=self.customer_name, qty=1, rate=100, do_not_save=True)
si.redeem_loyalty_points = True
si.loyalty_points = redeem
return si.insert().submit()
else:
si = create_sales_invoice(customer=self.customer_name, qty=10, rate=1000, do_not_save=True)
return si.insert().submit()
def test_add_loyalty_points(self):
self.create_test_invoice()
doc = frappe.get_last_doc("Loyalty Point Entry")
self.assertEqual(doc.loyalty_points, 10)
def test_add_loyalty_points_with_discretionary_reason(self):
doc = frappe.get_doc(
{
"doctype": "Loyalty Point Entry",
"loyalty_program": "Test Single Loyalty",
"loyalty_program_tier": "Bronce",
"customer": self.customer_name,
"invoice_type": "Sales Invoice",
"loyalty_points": 75,
"expiry_date": today(),
"posting_date": today(),
"company": "_Test Company",
"discretionary_reason": "Customer Appreciation",
}
)
doc.insert(ignore_permissions=True)
self.assertEqual(doc.loyalty_points, 75)
self.assertEqual(doc.discretionary_reason, "Customer Appreciation")
# Verify the entry in the database
entry = frappe.get_doc("Loyalty Point Entry", doc.name)
self.assertEqual(entry.loyalty_points, 75)
self.assertEqual(entry.discretionary_reason, "Customer Appreciation")
def test_redeem_loyalty_points(self):
self.create_test_invoice(redeem=10)
doc = frappe.get_last_doc("Loyalty Point Entry")
self.assertEqual(doc.loyalty_points, -10)
# Check balance
balance = frappe.db.sql(
"""
SELECT SUM(loyalty_points)
FROM `tabLoyalty Point Entry`
WHERE customer = %s
""",
(self.customer_name,),
)[0][0]
self.assertEqual(balance, 75) # 85 added, 10 redeemed

View File

@@ -36,7 +36,17 @@ class LoyaltyProgram(Document):
to_date: DF.Date | None
# end: auto-generated types
pass
def validate(self):
self.validate_lowest_tier()
def validate_lowest_tier(self):
tiers = sorted(self.collection_rules, key=lambda x: x.min_spent)
if tiers and tiers[0].min_spent != 0:
frappe.throw(
_(
"The lowest tier must have a minimum spent amount of 0. Customers need to be part of a tier as soon as they are enrolled in the program."
)
)
def get_loyalty_details(
@@ -79,17 +89,17 @@ def get_loyalty_program_details_with_points(
):
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
lp_details.update(
get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)
loyalty_details = get_loyalty_details(
customer, loyalty_program.name, expiry_date, company, include_expired_entry
)
lp_details.update(loyalty_details)
tier_spent_level = sorted(
[d.as_dict() for d in loyalty_program.collection_rules],
key=lambda rule: rule.min_spent,
reverse=True,
)
for i, d in enumerate(tier_spent_level):
if i == 0 or (lp_details.total_spent + current_transaction_amount) <= d.min_spent:
if i == 0 or (lp_details.total_spent + current_transaction_amount) >= d.min_spent:
lp_details.tier_name = d.tier_name
lp_details.collection_factor = d.collection_factor
else:
@@ -173,6 +183,8 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
ref_doc.loyalty_amount = loyalty_amount
if not ref_doc.loyalty_points and ref_doc.loyalty_points != points_to_redeem:
ref_doc.loyalty_points = points_to_redeem
if ref_doc.doctype == "Sales Invoice":
ref_doc.loyalty_program = loyalty_program

View File

@@ -1,18 +1,19 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import cint, flt, getdate, today
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_details,
get_loyalty_program_details_with_points,
)
from erpnext.accounts.party import get_dashboard_info
class TestLoyaltyProgram(unittest.TestCase):
class TestLoyaltyProgram(IntegrationTestCase):
@classmethod
def setUpClass(self):
# create relevant item, customer, loyalty program, etc
@@ -38,6 +39,7 @@ class TestLoyaltyProgram(unittest.TestCase):
)
self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program)
self.assertEqual(lpe.get("loyalty_program_tier"), "Bronce") # is always in the first tier
self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier)
self.assertEqual(lpe.loyalty_points, earned_points)
@@ -79,6 +81,7 @@ class TestLoyaltyProgram(unittest.TestCase):
si_original = create_sales_invoice_record()
si_original.insert()
si_original.submit()
customer.reload()
earned_points = get_points_earned(si_original)
@@ -101,8 +104,8 @@ class TestLoyaltyProgram(unittest.TestCase):
si_redeem.loyalty_points = earned_points
si_redeem.insert()
si_redeem.submit()
customer.reload()
customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
earned_after_redemption = get_points_earned(si_redeem)
lpe_redeem = frappe.get_doc(
@@ -197,6 +200,70 @@ class TestLoyaltyProgram(unittest.TestCase):
for d in company_wise_info:
self.assertTrue(d.get("loyalty_points"))
@unittest.mock.patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
def test_tier_selection(self, mock_get_loyalty_details):
# Create a new loyalty program with multiple tiers
loyalty_program = frappe.get_doc(
{
"doctype": "Loyalty Program",
"loyalty_program_name": "Test Tier Selection",
"auto_opt_in": 1,
"from_date": today(),
"loyalty_program_type": "Multiple Tier Program",
"conversion_factor": 1,
"expiry_duration": 10,
"company": "_Test Company",
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [
{"tier_name": "Gold", "collection_factor": 1000, "min_spent": 20000},
{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 10000},
{"tier_name": "Bronze", "collection_factor": 1000, "min_spent": 0},
],
}
)
loyalty_program.insert()
# Test cases with different total_spent and current_transaction_amount combinations
test_cases = [
(0, 6000, "Bronze"),
(0, 15000, "Silver"),
(0, 25000, "Gold"),
(4000, 500, "Bronze"),
(8000, 3000, "Silver"),
(18000, 3000, "Gold"),
(22000, 5000, "Gold"),
]
for total_spent, current_transaction_amount, expected_tier in test_cases:
with self.subTest(total_spent=total_spent, current_transaction_amount=current_transaction_amount):
# Mock the get_loyalty_details function to update the total_spent
def side_effect(*args, **kwargs):
result = get_loyalty_details(*args, **kwargs)
result.update({"total_spent": total_spent})
return result
mock_get_loyalty_details.side_effect = side_effect
lp_details = get_loyalty_program_details_with_points(
"Test Loyalty Customer",
loyalty_program=loyalty_program.name,
company="_Test Company",
current_transaction_amount=current_transaction_amount,
)
# Get the selected tier based on the current implementation
selected_tier = lp_details.tier_name
self.assertEqual(
selected_tier,
expected_tier,
f"Expected tier {expected_tier} for total_spent {total_spent} and current_transaction_amount {current_transaction_amount}, but got {selected_tier}",
)
# Clean up
loyalty_program.delete()
def get_points_earned(self):
def get_returned_amount():
@@ -285,7 +352,7 @@ def create_records():
"company": "_Test Company",
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 1000}],
"collection_rules": [{"tier_name": "Bronce", "collection_factor": 1000, "min_spent": 0}],
}
).insert()
@@ -316,6 +383,7 @@ def create_records():
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [
{"tier_name": "Bronze", "collection_factor": 1000, "min_spent": 0},
{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 10000},
{"tier_name": "Gold", "collection_factor": 1000, "min_spent": 19000},
],

View File

@@ -11,6 +11,7 @@
],
"fields": [
{
"columns": 3,
"fieldname": "tier_name",
"fieldtype": "Data",
"in_list_view": 1,
@@ -18,8 +19,10 @@
"reqd": 1
},
{
"columns": 3,
"fieldname": "min_spent",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Minimum Total Spent"
},
{
@@ -27,6 +30,7 @@
"fieldtype": "Column Break"
},
{
"columns": 3,
"description": "For how much spent = 1 Loyalty Point",
"fieldname": "collection_factor",
"fieldtype": "Currency",
@@ -37,7 +41,7 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:03.536071",
"modified": "2024-09-06 09:26:03.323912",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Loyalty Program Collection",

View File

@@ -1,10 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
# test_records = frappe.get_test_records('Mode of Payment')
class TestModeofPayment(unittest.TestCase):
class TestModeofPayment(IntegrationTestCase):
pass

View File

@@ -1,13 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
test_records = frappe.get_test_records("Monthly Distribution")
class TestMonthlyDistribution(unittest.TestCase):
class TestMonthlyDistribution(IntegrationTestCase):
pass

View File

@@ -28,7 +28,12 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
frm.refresh_fields();
frm.page.clear_indicator();
frm.dashboard.hide_progress();
frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type]));
if (frm.doc.invoice_type == "Sales") {
frappe.msgprint(__("Opening Sales Invoices have been created."));
} else {
frappe.msgprint(__("Opening Purchase Invoices have been created."));
}
},
1500,
data.title
@@ -48,12 +53,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
frm.page.set_primary_action(__("Create Invoices"), () => {
let btn_primary = frm.page.btn_primary.get(0);
let freeze_message;
if (frm.doc.invoice_type == "Sales") {
freeze_message = __("Creating Sales Invoices ...");
} else {
freeze_message = __("Creating Purchase Invoices ...");
}
return frm.call({
doc: frm.doc,
btn: $(btn_primary),
method: "make_invoices",
freeze: 1,
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
freeze_message: freeze_message,
});
});

View File

@@ -2,7 +2,7 @@
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
@@ -15,7 +15,16 @@ from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_crea
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(FrappeTestCase):
class UnitTestOpeningInvoiceCreationTool(UnitTestCase):
"""
Unit tests for OpeningInvoiceCreationTool.
Use this class for testing individual functions and methods.
"""
pass
class TestOpeningInvoiceCreationTool(IntegrationTestCase):
@classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):

View File

@@ -1,9 +1,10 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
from frappe.tests import IntegrationTestCase
class TestPartyLink(unittest.TestCase):
class TestPartyLink(IntegrationTestCase):
pass

View File

@@ -174,6 +174,17 @@ frappe.ui.form.on("Payment Entry", {
};
});
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
return {
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
filters: {
reference_doctype: row.reference_doctype,
reference_name: row.reference_name,
},
};
});
frm.set_query("sales_taxes_and_charges_template", function () {
return {
filters: {
@@ -191,7 +202,15 @@ frappe.ui.form.on("Payment Entry", {
},
};
});
frm.add_fetch(
"payment_request",
"outstanding_amount",
"payment_request_outstanding",
"Payment Entry Reference"
);
},
refresh: function (frm) {
erpnext.hide_company(frm);
frm.events.hide_unhide_fields(frm);
@@ -216,6 +235,7 @@ frappe.ui.form.on("Payment Entry", {
);
}
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
frappe.flags.allocate_payment_amount = true;
},
validate_company: (frm) => {
@@ -385,7 +405,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);
}
@@ -789,7 +817,7 @@ frappe.ui.form.on("Payment Entry", {
);
if (frm.doc.payment_type == "Pay")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
else frm.events.set_unallocated_amount(frm);
frm.set_paid_amount_based_on_received_amount = false;
@@ -810,7 +838,7 @@ frappe.ui.form.on("Payment Entry", {
}
if (frm.doc.payment_type == "Receive")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true);
else frm.events.set_unallocated_amount(frm);
},
@@ -981,6 +1009,7 @@ frappe.ui.form.on("Payment Entry", {
c.outstanding_amount = d.outstanding_amount;
c.bill_no = d.bill_no;
c.payment_term = d.payment_term;
c.payment_term_outstanding = d.payment_term_outstanding;
c.allocated_amount = d.allocated_amount;
c.account = d.account;
@@ -1030,7 +1059,8 @@ frappe.ui.form.on("Payment Entry", {
frm.events.allocate_party_amount_against_ref_docs(
frm,
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount,
false
);
},
});
@@ -1044,93 +1074,13 @@ frappe.ui.form.on("Payment Entry", {
return ["Sales Invoice", "Purchase Invoice"];
},
allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0;
var total_deductions = frappe.utils.sum(
$.map(frm.doc.deductions || [], function (d) {
return flt(d.amount);
})
);
paid_amount -= total_deductions;
$.each(frm.doc.references || [], function (i, row) {
if (flt(row.outstanding_amount) > 0)
total_positive_outstanding_including_order += flt(row.outstanding_amount);
else total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", {
paid_amount: paid_amount,
paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
});
var allocated_negative_outstanding = 0;
if (
(frm.doc.payment_type == "Receive" && frm.doc.party_type == "Customer") ||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Supplier") ||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Employee")
) {
if (total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
allocated_negative_outstanding =
total_negative_outstanding < remaining_outstanding
? total_negative_outstanding
: remaining_outstanding;
}
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"));
if (paid_amount > total_negative_outstanding) {
if (total_negative_outstanding == 0) {
frappe.msgprint(
__("Cannot {0} {1} {2} without any negative outstanding invoice", [
frm.doc.payment_type,
frm.doc.party_type == "Customer" ? "to" : "from",
frm.doc.party_type,
])
);
return false;
} else {
frappe.msgprint(
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [
total_negative_outstanding,
])
);
return false;
}
} else {
allocated_positive_outstanding = total_negative_outstanding - paid_amount;
allocated_negative_outstanding =
paid_amount +
(total_positive_outstanding_including_order < allocated_positive_outstanding
? total_positive_outstanding_including_order
: allocated_positive_outstanding);
}
}
$.each(frm.doc.references || [], function (i, row) {
if (frappe.flags.allocate_payment_amount == 0) {
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
row.allocated_amount = 0;
} else if (
frappe.flags.allocate_payment_amount != 0 &&
(!row.allocated_amount || paid_amount_change)
) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
row.allocated_amount =
row.outstanding_amount >= allocated_positive_outstanding
? allocated_positive_outstanding
: row.outstanding_amount;
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
row.allocated_amount =
Math.abs(row.outstanding_amount) >= allocated_negative_outstanding
? -1 * allocated_negative_outstanding
: row.outstanding_amount;
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}
}
});
frm.refresh_fields();
frm.events.set_total_allocated_amount(frm);
},
@@ -1678,6 +1628,62 @@ frappe.ui.form.on("Payment Entry", {
return current_tax_amount;
},
cost_center: function (frm) {
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
args: {
company: frm.doc.company,
date: frm.doc.posting_date,
paid_from: frm.doc.paid_from,
paid_to: frm.doc.paid_to,
ptype: frm.doc.party_type,
pty: frm.doc.party,
cost_center: frm.doc.cost_center,
},
callback: function (r, rt) {
if (r.message) {
frappe.run_serially([
() => {
frm.set_value(
"paid_from_account_balance",
r.message.paid_from_account_balance
);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
},
]);
}
},
});
}
},
after_save: function (frm) {
const { matched_payment_requests } = frappe.last_response;
if (!matched_payment_requests) return;
const COLUMN_LABEL = [
[__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")],
];
frappe.msgprint({
title: __("Unset Matched Payment Request"),
message: COLUMN_LABEL.concat(matched_payment_requests),
as_table: true,
wide: true,
primary_action: {
label: __("Allocate Payment Request"),
action() {
frappe.hide_msgprint();
frm.call("set_matched_payment_requests", { matched_payment_requests }, () => {
frm.dirty();
});
},
},
});
},
});
frappe.ui.form.on("Payment Entry Reference", {
@@ -1770,35 +1776,3 @@ frappe.ui.form.on("Payment Entry Deduction", {
frm.events.set_unallocated_amount(frm);
},
});
frappe.ui.form.on("Payment Entry", {
cost_center: function (frm) {
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
args: {
company: frm.doc.company,
date: frm.doc.posting_date,
paid_from: frm.doc.paid_from,
paid_to: frm.doc.paid_to,
ptype: frm.doc.party_type,
pty: frm.doc.party,
cost_center: frm.doc.cost_center,
},
callback: function (r, rt) {
if (r.message) {
frappe.run_serially([
() => {
frm.set_value(
"paid_from_account_balance",
r.message.paid_from_account_balance
);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
},
]);
}
},
});
}
},
});

View File

@@ -7,8 +7,10 @@ from functools import reduce
import frappe
from frappe import ValidationError, _, qb, scrub, throw
from frappe.query_builder import Tuple
from frappe.query_builder.functions import Count
from frappe.utils import cint, comma_or, flt, getdate, nowdate
from frappe.utils.data import comma_and, fmt_money
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
from pypika import Case
from pypika.functions import Coalesce, Sum
@@ -180,14 +182,17 @@ class PaymentEntry(AccountsController):
self.set_status()
self.set_total_in_words()
def before_save(self):
self.set_matched_unset_payment_requests_to_response()
def on_submit(self):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries()
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_payment_schedule()
self.set_payment_req_status()
self.update_payment_requests()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def set_liability_account(self):
@@ -228,9 +233,21 @@ class PaymentEntry(AccountsController):
self.is_opening = "No"
return
liability_account = get_party_account(
self.party_type, self.party, self.company, include_advance=True
)[1]
accounts = get_party_account(self.party_type, self.party, self.company, include_advance=True)
liability_account = accounts[1] if len(accounts) > 1 else None
fieldname = (
"default_advance_received_account"
if self.party_type == "Customer"
else "default_advance_paid_account"
)
if not liability_account:
throw(
_("Please set default {0} in Company {1}").format(
frappe.bold(frappe.get_meta("Company").get_label(fieldname)), frappe.bold(self.company)
)
)
self.set(self.party_account_field, liability_account)
@@ -259,30 +276,34 @@ class PaymentEntry(AccountsController):
super().on_cancel()
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_payment_req_status()
self.update_payment_requests(cancel=True)
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def set_payment_req_status(self):
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
def update_payment_requests(self, cancel=False):
from erpnext.accounts.doctype.payment_request.payment_request import (
update_payment_requests_as_per_pe_references,
)
update_payment_req_status(self, None)
update_payment_requests_as_per_pe_references(self.references, cancel=cancel)
def update_outstanding_amounts(self):
self.set_missing_ref_details(force=True)
def validate_duplicate_entry(self):
reference_names = []
reference_names = set()
for d in self.get("references"):
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request)
if key in reference_names:
frappe.throw(
_("Row #{0}: Duplicate entry in References {1} {2}").format(
d.idx, d.reference_doctype, d.reference_name
)
)
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
reference_names.add(key)
def set_bank_account_data(self):
if self.bank_account:
@@ -308,6 +329,8 @@ class PaymentEntry(AccountsController):
if self.payment_type == "Internal Transfer":
return
self.validate_allocated_amount_as_per_payment_request()
if self.party_type in ("Customer", "Supplier"):
self.validate_allocated_amount_with_latest_data()
else:
@@ -320,6 +343,27 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
def validate_allocated_amount_as_per_payment_request(self):
"""
Allocated amount should not be greater than the outstanding amount of the Payment Request.
"""
if not self.references:
return
pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references)
if not pr_outstanding_amounts:
return
for ref in self.references:
if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]:
frappe.throw(
msg=_(
"Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}"
).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)),
title=_("Invalid Allocated Amount"),
)
def term_based_allocation_enabled_for_reference(
self, reference_doctype: str, reference_name: str
) -> bool:
@@ -524,7 +568,10 @@ class PaymentEntry(AccountsController):
continue
if field == "exchange_rate" or not d.get(field) or force:
d.db_set(field, value)
if self.get("_action") in ("submit", "cancel"):
d.db_set(field, value)
else:
d.set(field, value)
def validate_payment_type(self):
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
@@ -1692,6 +1739,380 @@ class PaymentEntry(AccountsController):
return current_tax_fraction
def set_matched_unset_payment_requests_to_response(self):
"""
Find matched Payment Requests for those references which have no Payment Request set.\n
And set to `frappe.response` to show in the frontend for allocation.
"""
if not self.references:
return
matched_payment_requests = get_matched_payment_request_of_references(
[row for row in self.references if not row.payment_request]
)
if not matched_payment_requests:
return
frappe.response["matched_payment_requests"] = matched_payment_requests
@frappe.whitelist()
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
"""
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
:param paid_amount: Paid Amount / Received Amount.
:param paid_amount_change: Flag to check if `Paid Amount` is changed or not.
:param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag)
"""
if not self.references:
return
if not allocate_payment_amount:
for ref in self.references:
ref.allocated_amount = 0
return
# calculating outstanding amounts
precision = self.precision("paid_amount")
total_positive_outstanding_including_order = 0
total_negative_outstanding = 0
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
for ref in self.references:
reference_outstanding_amount = ref.outstanding_amount
abs_outstanding_amount = abs(reference_outstanding_amount)
if reference_outstanding_amount > 0:
total_positive_outstanding_including_order += abs_outstanding_amount
else:
total_negative_outstanding += abs_outstanding_amount
# calculating allocated outstanding amounts
allocated_negative_outstanding = 0
allocated_positive_outstanding = 0
# checking party type and payment type
if (self.payment_type == "Receive" and self.party_type == "Customer") or (
self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee")
):
if total_positive_outstanding_including_order > paid_amount:
remaining_outstanding = flt(
total_positive_outstanding_including_order - paid_amount, precision
)
allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding)
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
elif self.party_type in ("Supplier", "Employee"):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
_("Cannot {0} from {2} without any negative outstanding invoice").format(
self.payment_type,
self.party_type,
)
)
else:
frappe.msgprint(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
)
)
return
else:
allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision)
allocated_negative_outstanding = paid_amount + min(
total_positive_outstanding_including_order, allocated_positive_outstanding
)
# inner function to set `allocated_amount` to those row which have no PR
def _allocation_to_unset_pr_row(
row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding
):
if outstanding_amount > 0 and allocated_positive_outstanding >= 0:
row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount)
allocated_positive_outstanding = flt(
allocated_positive_outstanding - row.allocated_amount, precision
)
elif outstanding_amount < 0 and allocated_negative_outstanding:
row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1
allocated_negative_outstanding = flt(
allocated_negative_outstanding - abs(row.allocated_amount), precision
)
return allocated_positive_outstanding, allocated_negative_outstanding
# allocate amount based on `paid_amount` is changed or not
if not paid_amount_change:
for ref in self.references:
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
ref,
ref.outstanding_amount,
allocated_positive_outstanding,
allocated_negative_outstanding,
)
allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount"))
else:
payment_request_outstanding_amounts = (
get_payment_request_outstanding_set_in_references(self.references) or {}
)
references_outstanding_amounts = get_references_outstanding_amount(self.references) or {}
remaining_references_allocated_amounts = references_outstanding_amounts.copy()
# Re allocate amount to those references which have PR set (Higher priority)
for ref in self.references:
if not ref.payment_request:
continue
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
reference_outstanding_amount = references_outstanding_amounts[key]
pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request]
if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0:
# allocate amount according to outstanding amounts
outstanding_amounts = (
allocated_positive_outstanding,
reference_outstanding_amount,
pr_outstanding_amount,
)
ref.allocated_amount = min(outstanding_amounts)
# update amounts to track allocation
allocated_amount = ref.allocated_amount
allocated_positive_outstanding = flt(
allocated_positive_outstanding - allocated_amount, precision
)
remaining_references_allocated_amounts[key] = flt(
remaining_references_allocated_amounts[key] - allocated_amount, precision
)
payment_request_outstanding_amounts[ref.payment_request] = flt(
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
)
elif reference_outstanding_amount < 0 and allocated_negative_outstanding:
# allocate amount according to outstanding amounts
outstanding_amounts = (
allocated_negative_outstanding,
abs(reference_outstanding_amount),
pr_outstanding_amount,
)
ref.allocated_amount = min(outstanding_amounts) * -1
# update amounts to track allocation
allocated_amount = abs(ref.allocated_amount)
allocated_negative_outstanding = flt(
allocated_negative_outstanding - allocated_amount, precision
)
remaining_references_allocated_amounts[key] += allocated_amount # negative amount
payment_request_outstanding_amounts[ref.payment_request] = flt(
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
)
# Re allocate amount to those references which have no PR (Lower priority)
for ref in self.references:
if ref.payment_request:
continue
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
reference_outstanding_amount = remaining_references_allocated_amounts[key]
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
ref,
reference_outstanding_amount,
allocated_positive_outstanding,
allocated_negative_outstanding,
)
@frappe.whitelist()
def set_matched_payment_requests(self, matched_payment_requests):
"""
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
:param matched_payment_requests: List of tuple of matched Payment Requests.
---
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
"""
if not self.references or not matched_payment_requests:
return
if isinstance(matched_payment_requests, str):
matched_payment_requests = json.loads(matched_payment_requests)
# modify matched_payment_requests
# like (reference_doctype, reference_name, allocated_amount): payment_request
payment_requests = {}
for row in matched_payment_requests:
key = tuple(row[:3])
payment_requests[key] = row[3]
for ref in self.references:
if ref.payment_request:
continue
key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount)
if key in payment_requests:
ref.payment_request = payment_requests[key]
del payment_requests[key] # to avoid duplicate allocation
def get_matched_payment_request_of_references(references=None):
"""
Get those `Payment Requests` which are matched with `References`.\n
- Amount must be same.
- Only single `Payment Request` available for this amount.
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
"""
if not references:
return
# to fetch matched rows
refs = {
(row.reference_doctype, row.reference_name, row.allocated_amount)
for row in references
if row.reference_doctype and row.reference_name and row.allocated_amount
}
if not refs:
return
PR = frappe.qb.DocType("Payment Request")
# query to group by reference_doctype, reference_name, outstanding_amount
subquery = (
frappe.qb.from_(PR)
.select(
PR.reference_doctype,
PR.reference_name,
PR.outstanding_amount.as_("allocated_amount"),
PR.name.as_("payment_request"),
Count("*").as_("count"),
)
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
)
# query to fetch matched rows which are single
matched_prs = (
frappe.qb.from_(subquery)
.select(
subquery.reference_doctype,
subquery.reference_name,
subquery.allocated_amount,
subquery.payment_request,
)
.where(subquery.count == 1)
.run()
)
return matched_prs if matched_prs else None
def get_references_outstanding_amount(references=None):
"""
Fetch accurate outstanding amount of `References`.\n
- If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`.
- If `Payment Term` is not set, then fetch outstanding amount from `References` it self.
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
"""
if not references:
return
refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {}
refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {}
return {**refs_with_payment_term, **refs_without_payment_term}
def get_outstanding_of_references_with_payment_term(references=None):
"""
Fetch outstanding amount of `References` which have `Payment Term` set.\n
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
"""
if not references:
return
refs = {
(row.reference_doctype, row.reference_name, row.payment_term)
for row in references
if row.reference_doctype and row.reference_name and row.payment_term
}
if not refs:
return
PS = frappe.qb.DocType("Payment Schedule")
response = (
frappe.qb.from_(PS)
.select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding)
.where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs))
).run(as_dict=True)
if not response:
return
return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response}
def get_outstanding_of_references_with_no_payment_term(references):
"""
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
- Fetch outstanding amount from `References` it self.
Note: `None` is used for allocation of `Payment Request`
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
"""
if not references:
return
outstanding_amounts = {}
for ref in references:
if ref.payment_term:
continue
key = (ref.reference_doctype, ref.reference_name, None)
if key not in outstanding_amounts:
outstanding_amounts[key] = ref.outstanding_amount
return outstanding_amounts
def get_payment_request_outstanding_set_in_references(references=None):
"""
Fetch outstanding amount of `Payment Request` which are set in `References`.\n
Example: {payment_request: outstanding_amount, ...}
"""
if not references:
return
referenced_payment_requests = {row.payment_request for row in references if row.payment_request}
if not referenced_payment_requests:
return
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(PR.name, PR.outstanding_amount)
.where(PR.name.isin(referenced_payment_requests))
).run()
return dict(response) if response else None
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
@@ -2323,6 +2744,7 @@ def get_payment_entry(
payment_type=None,
reference_date=None,
ignore_permissions=False,
created_from_payment_request=False,
):
doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
@@ -2472,9 +2894,179 @@ def get_payment_entry(
pe.set_difference_amount()
# If PE is created from PR directly, then no need to find open PRs for the references
if not created_from_payment_request:
allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount"))
return pe
def get_open_payment_requests_for_references(references=None):
"""
Fetch all unpaid Payment Requests for the references. \n
- Each reference can have multiple Payment Requests. \n
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
"""
if not references:
return
refs = {
(row.reference_doctype, row.reference_name)
for row in references
if row.reference_doctype and row.reference_name and row.allocated_amount
}
if not refs:
return
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
).run(as_dict=True)
if not response:
return
reference_payment_requests = {}
for row in response:
key = (row.reference_doctype, row.reference_name)
if key not in reference_payment_requests:
reference_payment_requests[key] = {row.name: row.outstanding_amount}
else:
reference_payment_requests[key][row.name] = row.outstanding_amount
return reference_payment_requests
def allocate_open_payment_requests_to_references(references=None, precision=None):
"""
Allocate unpaid Payment Requests to the references. \n
---
- Allocation based on below factors
- Reference Allocated Amount
- Reference Outstanding Amount (With Payment Terms or without Payment Terms)
- Reference Payment Request's outstanding amount
---
- Allocation based on below scenarios
- Reference's Allocated Amount == Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- This PR will not be allocated further
- Reference's Allocated Amount < Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- Reduce the PR's outstanding amount by the allocated amount
- This PR can be allocated further
- Reference's Allocated Amount > Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- Reduce Allocated Amount of the reference by the PR's outstanding amount
- Create a new row for the remaining amount until the Allocated Amount is 0
- Allocate PR if available
---
- Note:
- Priority is given to the first Payment Request of respective references.
- Single Reference can have multiple rows.
- With Payment Terms or without Payment Terms
- With Payment Request or without Payment Request
"""
if not references:
return
# get all unpaid payment requests for the references
references_open_payment_requests = get_open_payment_requests_for_references(references)
if not references_open_payment_requests:
return
if not precision:
precision = references[0].precision("allocated_amount")
# to manage new rows
row_number = 1
MOVE_TO_NEXT_ROW = 1
TO_SKIP_NEW_ROW = 2
while row_number <= len(references):
row = references[row_number - 1]
reference_key = (row.reference_doctype, row.reference_name)
# update the idx to maintain the order
row.idx = row_number
# unpaid payment requests for the reference
reference_payment_requests = references_open_payment_requests.get(reference_key)
if not reference_payment_requests:
row_number += MOVE_TO_NEXT_ROW # to move to next reference row
continue
# get the first payment request and its outstanding amount
payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items()))
allocated_amount = row.allocated_amount
# allocate the payment request to the reference and PR's outstanding amount
row.payment_request = payment_request
if pr_outstanding_amount == allocated_amount:
del reference_payment_requests[payment_request]
row_number += MOVE_TO_NEXT_ROW
elif pr_outstanding_amount > allocated_amount:
# reduce the outstanding amount of the payment request
reference_payment_requests[payment_request] -= allocated_amount
row_number += MOVE_TO_NEXT_ROW
else:
# split the reference row to allocate the remaining amount
del reference_payment_requests[payment_request]
row.allocated_amount = pr_outstanding_amount
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
# set the remaining amount to the next row
while allocated_amount:
# create a new row for the remaining amount
new_row = frappe.copy_doc(row)
references.insert(row_number, new_row)
# get the first payment request and its outstanding amount
payment_request, pr_outstanding_amount = next(
iter(reference_payment_requests.items()), (None, None)
)
# update new row
new_row.idx = row_number + 1
new_row.payment_request = payment_request
new_row.allocated_amount = min(
pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount
)
if not payment_request or not pr_outstanding_amount:
row_number += TO_SKIP_NEW_ROW
break
elif pr_outstanding_amount == allocated_amount:
del reference_payment_requests[payment_request]
row_number += TO_SKIP_NEW_ROW
break
elif pr_outstanding_amount > allocated_amount:
reference_payment_requests[payment_request] -= allocated_amount
row_number += TO_SKIP_NEW_ROW
break
else:
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
del reference_payment_requests[payment_request]
row_number += MOVE_TO_NEXT_ROW
def update_accounting_dimensions(pe, doc):
"""
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.account.test_account import create_account
@@ -28,7 +28,16 @@ from erpnext.setup.doctype.employee.test_employee import make_employee
test_dependencies = ["Item"]
class TestPaymentEntry(FrappeTestCase):
class UnitTestPaymentEntry(UnitTestCase):
"""
Unit tests for PaymentEntry.
Use this class for testing individual functions and methods.
"""
pass
class TestPaymentEntry(IntegrationTestCase):
def tearDown(self):
frappe.db.rollback()
@@ -383,7 +392,7 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{
"allow_multi_currency_invoices_against_single_party_account": 1,
@@ -1090,7 +1099,7 @@ class TestPaymentEntry(FrappeTestCase):
}
self.assertDictEqual(ref_details, expected_response)
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{
"unlink_payment_on_cancellation_of_invoice": 1,
@@ -1185,7 +1194,7 @@ class TestPaymentEntry(FrappeTestCase):
si3.cancel()
si3.delete()
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{
"unlink_payment_on_cancellation_of_invoice": 1,
@@ -1791,6 +1800,79 @@ class TestPaymentEntry(FrappeTestCase):
# 'Is Opening' should always be 'No' for normal advance payments
self.assertEqual(gl_with_opening_set, [])
@IntegrationTestCase.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")

View File

@@ -10,6 +10,7 @@
"due_date",
"bill_no",
"payment_term",
"payment_term_outstanding",
"account_type",
"payment_type",
"column_break_4",
@@ -18,7 +19,9 @@
"allocated_amount",
"exchange_rate",
"exchange_gain_loss",
"account"
"account",
"payment_request",
"payment_request_outstanding"
],
"fields": [
{
@@ -120,12 +123,33 @@
"fieldname": "payment_type",
"fieldtype": "Data",
"label": "Payment Type"
},
{
"fieldname": "payment_request",
"fieldtype": "Link",
"label": "Payment Request",
"options": "Payment Request"
},
{
"depends_on": "eval: doc.payment_term",
"fieldname": "payment_term_outstanding",
"fieldtype": "Float",
"label": "Payment Term Outstanding",
"read_only": 1
},
{
"depends_on": "eval: doc.payment_request && doc.payment_request_outstanding",
"fieldname": "payment_request_outstanding",
"fieldtype": "Float",
"is_virtual": 1,
"label": "Payment Request Outstanding",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-04-05 09:44:08.310593",
"modified": "2024-09-16 18:11:50.019343",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -1,7 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
@@ -25,11 +25,19 @@ class PaymentEntryReference(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
payment_request: DF.Link | None
payment_request_outstanding: DF.Float
payment_term: DF.Link | None
payment_term_outstanding: DF.Float
payment_type: DF.Data | None
reference_doctype: DF.Link
reference_name: DF.DynamicLink
total_amount: DF.Float
# end: auto-generated types
pass
@property
def payment_request_outstanding(self):
if not self.payment_request:
return
return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount")

View File

@@ -1,12 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
# test_records = frappe.get_test_records('Payment Gateway Account')
test_ignore = ["Payment Gateway"]
class TestPaymentGatewayAccount(unittest.TestCase):
class TestPaymentGatewayAccount(IntegrationTestCase):
pass

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -13,7 +13,16 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.stock.doctype.item.test_item import create_item
class TestPaymentLedgerEntry(FrappeTestCase):
class UnitTestPaymentLedgerEntry(UnitTestCase):
"""
Unit tests for PaymentLedgerEntry.
Use this class for testing individual functions and methods.
"""
pass
class TestPaymentLedgerEntry(IntegrationTestCase):
def setUp(self):
self.ple = qb.DocType("Payment Ledger Entry")
self.create_company()
@@ -445,7 +454,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
self.assertEqual(pl_entries_for_crnote[0], expected_values[0])
self.assertEqual(pl_entries_for_crnote[1], expected_values[1])
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
)
@@ -474,7 +483,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
si.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name)
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
)
@@ -507,7 +516,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
si.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name)
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{
"unlink_payment_on_cancellation_of_invoice": 1,

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import getdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
@@ -17,7 +17,16 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
class TestPaymentOrder(FrappeTestCase):
class UnitTestPaymentOrder(UnitTestCase):
"""
Unit tests for PaymentOrder.
Use this class for testing individual functions and methods.
"""
pass
class TestPaymentOrder(IntegrationTestCase):
def setUp(self):
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
uniq_identifier = frappe.generate_hash(length=10)

View File

@@ -4,8 +4,8 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate
from frappe.tests import IntegrationTestCase, UnitTestCase
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,13 +13,23 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
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
test_dependencies = ["Item"]
class TestPaymentReconciliation(FrappeTestCase):
class UnitTestPaymentReconciliation(UnitTestCase):
"""
Unit tests for PaymentReconciliation.
Use this class for testing individual functions and methods.
"""
pass
class TestPaymentReconciliation(IntegrationTestCase):
def setUp(self):
self.create_company()
self.create_item()
@@ -1103,7 +1113,7 @@ class TestPaymentReconciliation(FrappeTestCase):
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
@change_settings(
@IntegrationTestCase.change_settings(
"Accounts Settings",
{
"allow_multi_currency_invoices_against_single_party_account": 1,
@@ -1845,6 +1855,78 @@ class TestPaymentReconciliation(FrappeTestCase):
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 +1954,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

View File

@@ -52,8 +52,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
}
if (
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
frm.doc.status == "Initiated"
frm.doc.payment_request_type == "Outward" &&
["Initiated", "Partially Paid"].includes(frm.doc.status)
) {
frm.add_custom_button(__("Create Payment Entry"), function () {
frappe.call({

View File

@@ -20,9 +20,11 @@
"reference_name",
"transaction_details",
"grand_total",
"currency",
"is_a_subscription",
"column_break_18",
"currency",
"outstanding_amount",
"party_account_currency",
"subscription_section",
"subscription_plans",
"bank_account_details",
@@ -50,13 +52,14 @@
"message",
"message_examples",
"mute_email",
"payment_url",
"section_break_7",
"payment_gateway",
"payment_account",
"payment_channel",
"payment_order",
"amended_from"
"amended_from",
"column_break_pnyv",
"payment_url"
],
"fields": [
{
@@ -70,6 +73,7 @@
{
"fieldname": "transaction_date",
"fieldtype": "Date",
"in_preview": 1,
"label": "Transaction Date"
},
{
@@ -134,7 +138,8 @@
"no_copy": 1,
"options": "reference_doctype",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "transaction_details",
@@ -142,12 +147,14 @@
"label": "Transaction Details"
},
{
"description": "Amount in customer's currency",
"description": "Amount in transaction currency",
"fieldname": "grand_total",
"fieldtype": "Currency",
"in_preview": 1,
"label": "Amount",
"non_negative": 1,
"options": "currency"
"options": "currency",
"reqd": 1
},
{
"default": "0",
@@ -343,7 +350,7 @@
{
"fieldname": "payment_url",
"fieldtype": "Data",
"hidden": 1,
"label": "Payment URL",
"length": 500,
"options": "URL",
"read_only": 1
@@ -402,19 +409,41 @@
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval: doc.docstatus === 1",
"description": "Amount in party's bank account currency",
"fieldname": "outstanding_amount",
"fieldtype": "Currency",
"in_preview": 1,
"label": "Outstanding Amount",
"non_negative": 1,
"options": "party_account_currency",
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"fieldname": "column_break_pnyv",
"fieldtype": "Column Break"
},
{
"fieldname": "party_account_currency",
"fieldtype": "Link",
"label": "Party Account Currency",
"options": "Currency",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-08-07 16:39:54.288002",
"modified": "2024-09-16 17:50:54.440090",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
@@ -449,6 +478,7 @@
"write": 1
}
],
"show_preview_popup": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -7,6 +7,7 @@ from frappe.query_builder.functions import Sum
from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue
from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
@@ -46,9 +47,11 @@ class PaymentRequest(Document):
bank_account: DF.Link | None
bank_account_no: DF.ReadOnly | None
branch_code: DF.ReadOnly | None
company: DF.Link | None
cost_center: DF.Link | None
currency: DF.Link | None
email_to: DF.Data | None
failed_reason: DF.Data | None
grand_total: DF.Currency
iban: DF.ReadOnly | None
is_a_subscription: DF.Check
@@ -57,16 +60,18 @@ class PaymentRequest(Document):
mode_of_payment: DF.Link | None
mute_email: DF.Check
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
outstanding_amount: DF.Currency
party: DF.DynamicLink | None
party_account_currency: DF.Link | None
party_type: DF.Link | None
payment_account: DF.ReadOnly | None
payment_channel: DF.Literal["", "Email", "Phone"]
payment_channel: DF.Literal["", "Email", "Phone", "Other"]
payment_gateway: DF.ReadOnly | None
payment_gateway_account: DF.Link | None
payment_order: DF.Link | None
payment_request_type: DF.Literal["Outward", "Inward"]
payment_url: DF.Data | None
print_format: DF.Literal
print_format: DF.Literal[None]
project: DF.Link | None
reference_doctype: DF.Link | None
reference_name: DF.DynamicLink | None
@@ -85,7 +90,6 @@ class PaymentRequest(Document):
subscription_plans: DF.Table[SubscriptionPlanDetail]
swift_number: DF.ReadOnly | None
transaction_date: DF.Date | None
company: DF.Link | None
# end: auto-generated types
def validate(self):
@@ -101,6 +105,12 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self):
if self.grand_total == 0:
frappe.throw(
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
title=_("Invalid Amount"),
)
existing_payment_request_amount = flt(
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
)
@@ -150,16 +160,28 @@ class PaymentRequest(Document):
).format(self.grand_total, amount)
)
def on_change(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_advance_payment_status()
def before_submit(self):
if (
self.currency != self.party_account_currency
and self.party_account_currency == get_company_currency(self.company)
):
# set outstanding amount in party account currency
invoice = frappe.get_value(
self.reference_doctype,
self.reference_name,
["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
as_dict=1,
)
grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
self.outstanding_amount = flt(
self.grand_total / grand_total * base_grand_total,
self.precision("outstanding_amount"),
)
else:
self.outstanding_amount = self.grand_total
if self.payment_request_type == "Outward":
self.status = "Initiated"
elif self.payment_request_type == "Inward":
@@ -174,6 +196,9 @@ class PaymentRequest(Document):
self.send_email()
self.make_communication_entry()
def on_submit(self):
self.update_reference_advance_payment_status()
def request_phone_payment(self):
controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount()
@@ -211,6 +236,7 @@ class PaymentRequest(Document):
def on_cancel(self):
self.check_if_payment_entry_exists()
self.set_as_cancelled()
self.update_reference_advance_payment_status()
def make_invoice(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
@@ -267,7 +293,7 @@ class PaymentRequest(Document):
def set_as_paid(self):
if self.payment_channel == "Phone":
self.db_set("status", "Paid")
self.db_set({"status": "Paid", "outstanding_amount": 0})
else:
payment_entry = self.create_payment_entry()
@@ -289,26 +315,32 @@ class PaymentRequest(Document):
else:
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
party_account_currency = (
self.get("party_account_currency")
or ref_doc.get("party_account_currency")
or get_account_currency(party_account)
)
party_amount = bank_amount = self.outstanding_amount
bank_amount = self.grand_total
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
else:
party_amount = self.grand_total
exchange_rate = ref_doc.get("conversion_rate")
bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
# outstanding amount is already in Part's account currency
payment_entry = get_payment_entry(
self.reference_doctype,
self.reference_name,
party_amount=party_amount,
bank_account=self.payment_account,
bank_amount=bank_amount,
created_from_payment_request=True,
)
payment_entry.update(
{
"mode_of_payment": self.mode_of_payment,
"reference_no": self.name,
"reference_no": self.name, # to prevent validation error
"reference_date": nowdate(),
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
self.reference_doctype, self.reference_name, self.name
@@ -316,6 +348,9 @@ class PaymentRequest(Document):
}
)
# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
self._allocate_payment_request_to_pe_references(references=payment_entry.references)
# Update dimensions
payment_entry.update(
{
@@ -324,14 +359,6 @@ class PaymentRequest(Document):
}
)
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
amount = payment_entry.base_paid_amount
else:
amount = self.grand_total
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 (
@@ -426,13 +453,79 @@ class PaymentRequest(Document):
return create_stripe_subscription(gateway_controller, data)
def update_reference_advance_payment_status(self):
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if self.reference_doctype in advance_payment_doctypes:
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
ref_doc.set_advance_payment_status()
def _allocate_payment_request_to_pe_references(self, references):
"""
Allocate the Payment Request to the Payment Entry references based on\n
- Allocated Amount.
- Outstanding Amount of Payment Request.\n
Payment Request is doc itself and references are the rows of Payment Entry.
"""
if len(references) == 1:
references[0].payment_request = self.name
return
precision = references[0].precision("allocated_amount")
outstanding_amount = self.outstanding_amount
# to manage rows
row_number = 1
MOVE_TO_NEXT_ROW = 1
TO_SKIP_NEW_ROW = 2
NEW_ROW_ADDED = False
while row_number <= len(references):
row = references[row_number - 1]
# update the idx to maintain the order
row.idx = row_number
if outstanding_amount == 0:
if not NEW_ROW_ADDED:
break
row_number += MOVE_TO_NEXT_ROW
continue
# allocate the payment request to the row
row.payment_request = self.name
if row.allocated_amount <= outstanding_amount:
outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
row_number += MOVE_TO_NEXT_ROW
else:
remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
row.allocated_amount = outstanding_amount
outstanding_amount = 0
# create a new row without PR for remaining unallocated amount
new_row = frappe.copy_doc(row)
references.insert(row_number, new_row)
# update new row
new_row.idx = row_number + 1
new_row.payment_request = None
new_row.allocated_amount = remaining_allocated_amount
NEW_ROW_ADDED = True
row_number += TO_SKIP_NEW_ROW
@frappe.whitelist(allow_guest=True)
def make_payment_request(**args):
"""Make payment request"""
args = frappe._dict(args)
if args.dt not in [
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if ref_doc.doctype not in [
"Sales Order",
"Purchase Order",
"Sales Invoice",
@@ -440,22 +533,21 @@ def make_payment_request(**args):
"POS Invoice",
"Fees",
]:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
frappe.throw(
_("Payment Requests cannot be created against: {0}").format(frappe.bold(ref_doc.doctype))
)
ref_doc = frappe.get_doc(args.dt, args.dn)
gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if not grand_total:
frappe.throw(_("Payment Entry is already created"))
if args.loyalty_points and args.dt == "Sales Order":
if args.loyalty_points and ref_doc.doctype == "Sales Order":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
frappe.db.set_value(
"Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False
)
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) # sets fields on ref_doc
ref_doc.db_update()
grand_total = grand_total - loyalty_amount
bank_account = (
@@ -464,14 +556,18 @@ def make_payment_request(**args):
draft_payment_request = frappe.db.get_value(
"Payment Request",
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
)
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
if not grand_total:
frappe.throw(_("Payment Request is already created"))
if draft_payment_request:
frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
@@ -485,6 +581,13 @@ def make_payment_request(**args):
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
)
party_type = args.get("party_type") or "Customer"
party_account_currency = ref_doc.party_account_currency
if not party_account_currency:
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
party_account_currency = get_account_currency(party_account)
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -493,15 +596,16 @@ def make_payment_request(**args):
"payment_channel": gateway_account.get("payment_channel"),
"payment_request_type": args.get("payment_request_type"),
"currency": ref_doc.currency,
"party_account_currency": party_account_currency,
"grand_total": grand_total,
"mode_of_payment": args.mode_of_payment,
"email_to": args.recipient_id or ref_doc.owner,
"subject": _("Payment Request for {0}").format(args.dn),
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
"reference_doctype": args.dt,
"reference_name": args.dn,
"reference_doctype": ref_doc.doctype,
"reference_name": ref_doc.name,
"company": ref_doc.get("company"),
"party_type": args.get("party_type") or "Customer",
"party_type": party_type,
"party": args.get("party") or ref_doc.get("customer"),
"bank_account": bank_account,
"make_sales_invoice": (
@@ -530,6 +634,8 @@ def make_payment_request(**args):
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
pr.insert(ignore_permissions=True)
if args.submit_doc:
if pr.get("__unsaved"):
pr.insert(ignore_permissions=True)
pr.submit()
if args.order_type == "Shopping Cart":
@@ -548,13 +654,14 @@ def get_amount(ref_doc, payment_account=None):
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
grand_total -= get_paid_amount_against_order(dt, ref_doc.name)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.grand_total)
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
else:
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
grand_total = flt(
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
)
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
@@ -576,24 +683,20 @@ def get_amount(ref_doc, payment_account=None):
def get_existing_payment_request_amount(ref_dt, ref_dn):
"""
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
and get the summation of existing paid payment request for Phone payment channel.
Return the total amount of Payment Requests against a reference document.
"""
existing_payment_request_amount = frappe.db.sql(
"""
select sum(grand_total)
from `tabPayment Request`
where
reference_doctype = %s
and reference_name = %s
and docstatus = 1
and (status != 'Paid'
or (payment_channel = 'Phone'
and status = 'Paid'))
""",
(ref_dt, ref_dn),
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(Sum(PR.grand_total))
.where(PR.reference_doctype == ref_dt)
.where(PR.reference_name == ref_dn)
.where(PR.docstatus == 1)
.run()
)
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
return response[0][0] if response[0] else 0
def get_gateway_details(args): # nosemgrep
@@ -635,41 +738,66 @@ def make_payment_entry(docname):
return doc.create_payment_entry(submit=False).as_dict()
def update_payment_req_status(doc, method):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
def update_payment_requests_as_per_pe_references(references=None, cancel=False):
"""
Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
"""
if not references:
return
for ref in doc.references:
payment_request_name = frappe.db.get_value(
"Payment Request",
{
"reference_doctype": ref.reference_doctype,
"reference_name": ref.reference_name,
"docstatus": 1,
},
precision = references[0].precision("allocated_amount")
referenced_payment_requests = frappe.get_all(
"Payment Request",
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
fields=[
"name",
"grand_total",
"outstanding_amount",
"payment_request_type",
],
)
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
for ref in references:
if not ref.payment_request:
continue
payment_request = referenced_payment_requests[ref.payment_request]
pr_outstanding = payment_request["outstanding_amount"]
# update outstanding amount
new_outstanding_amount = flt(
pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
precision,
)
if payment_request_name:
ref_details = get_reference_details(
ref.reference_doctype,
ref.reference_name,
doc.party_account_currency,
doc.party_type,
doc.party,
# to handle same payment request for the multiple allocations
payment_request["outstanding_amount"] = new_outstanding_amount
if not cancel and new_outstanding_amount < 0:
frappe.throw(
msg=_(
"The allocated amount is greater than the outstanding amount of Payment Request {0}"
).format(ref.payment_request),
title=_("Invalid Allocated Amount"),
)
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
status = pay_req_doc.status
if status != "Paid" and not ref_details.outstanding_amount:
status = "Paid"
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
status = "Partially Paid"
elif ref_details.outstanding_amount == ref_details.total_amount:
if pay_req_doc.payment_request_type == "Outward":
status = "Initiated"
elif pay_req_doc.payment_request_type == "Inward":
status = "Requested"
# update status
if new_outstanding_amount == payment_request["grand_total"]:
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
elif new_outstanding_amount == 0:
status = "Paid"
elif new_outstanding_amount > 0:
status = "Partially Paid"
pay_req_doc.db_set("status", status)
# update database
frappe.db.set_value(
"Payment Request",
ref.payment_request,
{"outstanding_amount": new_outstanding_amount, "status": status},
)
def get_dummy_message(doc):
@@ -755,28 +883,33 @@ def validate_payment(doc, method=None):
)
def get_paid_amount_against_order(dt, dn):
pe_ref = frappe.qb.DocType("Payment Entry Reference")
if dt == "Sales Order":
inv_dt, inv_field = "Sales Invoice Item", "sales_order"
else:
inv_dt, inv_field = "Purchase Invoice Item", "purchase_order"
inv_item = frappe.qb.DocType(inv_dt)
return (
frappe.qb.from_(pe_ref)
.select(
Sum(pe_ref.allocated_amount),
@frappe.whitelist()
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
# permission checks in `get_list()`
reference_doctype = filters.get("reference_doctype")
reference_name = filters.get("reference_doctype")
if not reference_doctype or not reference_name:
return []
open_payment_requests = frappe.get_list(
"Payment Request",
filters={
"reference_doctype": filters["reference_doctype"],
"reference_name": filters["reference_name"],
"status": ["!=", "Paid"],
"outstanding_amount": ["!=", 0], # for compatibility with old data
"docstatus": 1,
},
fields=["name", "grand_total", "outstanding_amount"],
order_by="transaction_date ASC,creation ASC",
)
return [
(
pr.name,
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
)
.where(
(pe_ref.docstatus == 1)
& (
(pe_ref.reference_name == dn)
| pe_ref.reference_name.isin(
frappe.qb.from_(inv_item)
.select(inv_item.parent)
.where(inv_item[inv_field] == dn)
.distinct()
)
)
)
).run()[0][0] or 0
for pr in open_payment_requests
]

View File

@@ -1,12 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import re
import unittest
from unittest.mock import patch
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase, UnitTestCase
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
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
@@ -55,7 +57,16 @@ payment_method = [
]
class TestPaymentRequest(FrappeTestCase):
class UnitTestPaymentRequest(UnitTestCase):
"""
Unit tests for PaymentRequest.
Use this class for testing individual functions and methods.
"""
pass
class TestPaymentRequest(IntegrationTestCase):
def setUp(self):
for payment_gateway in payment_gateways:
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
@@ -216,7 +227,7 @@ class TestPaymentRequest(FrappeTestCase):
def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice(
customer="_Test Supplier USD",
supplier="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
@@ -241,7 +252,7 @@ class TestPaymentRequest(FrappeTestCase):
def test_multiple_payment_entry_against_purchase_invoice(self):
purchase_invoice = make_purchase_invoice(
customer="_Test Supplier USD",
supplier="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
@@ -415,3 +426,266 @@ class TestPaymentRequest(FrappeTestCase):
self.assertEqual(pe.paid_amount, 800)
self.assertEqual(pe.base_received_amount, 800)
self.assertEqual(pe.received_amount, 10)
def test_multiple_payment_if_partially_paid_for_same_currency(self):
so = make_sales_order(currency="INR", qty=1, rate=1000)
self.assertEqual(so.advance_payment_status, "Not Requested")
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.grand_total, 1000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
self.assertEqual(pr.party_account_currency, pr.currency) # INR
self.assertEqual(pr.status, "Requested")
so.load_from_db()
self.assertEqual(so.advance_payment_status, "Requested")
# to make partial payment
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 200
pe.references[0].allocated_amount = 200
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
self.assertEqual(so.advance_payment_status, "Partially Paid")
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 800)
self.assertEqual(pr.grand_total, 1000)
# complete payment
pe = pr.create_payment_entry()
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
self.assertEqual(pe.references[0].allocated_amount, 800)
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
self.assertEqual(so.advance_payment_status, "Fully Paid")
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 1000)
# creating a more payment Request must not allowed
self.assertRaisesRegex(
frappe.exceptions.ValidationError,
re.compile(r"Payment Request is already created"),
make_payment_request,
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
@IntegrationTestCase.change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
)
def test_multiple_payment_if_partially_paid_for_multi_currency(self):
pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1)
pi.credit_to = "Creditors - _TC"
pi.submit()
pr = make_payment_request(
dt="Purchase Invoice",
dn=pi.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
# 100 USD -> 5000 INR
self.assertEqual(pr.grand_total, 100)
self.assertEqual(pr.outstanding_amount, 5000)
self.assertEqual(pr.currency, "USD")
self.assertEqual(pr.party_account_currency, "INR")
self.assertEqual(pr.status, "Initiated")
# to make partial payment
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 2000
pe.references[0].allocated_amount = 2000
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 3000)
self.assertEqual(pr.grand_total, 100)
# complete payment
pe = pr.create_payment_entry()
self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount
self.assertEqual(pe.references[0].allocated_amount, 3000)
self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero
self.assertEqual(pe.references[0].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 100)
# creating a more payment Request must not allowed
self.assertRaisesRegex(
frappe.exceptions.ValidationError,
re.compile(r"Payment Request is already created"),
make_payment_request,
dt="Purchase Invoice",
dn=pi.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
def test_single_payment_with_payment_term_for_same_currency(self):
create_payment_terms_template()
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000)
po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
po.save()
po.submit()
self.assertEqual(po.advance_payment_status, "Not Initiated")
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.grand_total, 20000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
self.assertEqual(pr.party_account_currency, pr.currency) # INR
self.assertEqual(pr.status, "Initiated")
po.load_from_db()
self.assertEqual(po.advance_payment_status, "Initiated")
pe = pr.create_payment_entry()
self.assertEqual(len(pe.references), 2)
self.assertEqual(pe.paid_amount, 20000)
# check 1st payment term
self.assertEqual(pe.references[0].allocated_amount, 16949.2)
self.assertEqual(pe.references[0].payment_request, pr.name)
# check 2nd payment term
self.assertEqual(pe.references[1].allocated_amount, 3050.8)
self.assertEqual(pe.references[1].payment_request, pr.name)
po.load_from_db()
self.assertEqual(po.advance_payment_status, "Fully Paid")
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 20000)
@IntegrationTestCase.change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
)
def test_single_payment_with_payment_term_for_multi_currency(self):
create_payment_terms_template()
si = create_sales_invoice(
do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50
)
si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
si.save()
si.submit()
pr = make_payment_request(
dt="Sales Invoice",
dn=si.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
# 200 USD -> 10000 INR
self.assertEqual(pr.grand_total, 200)
self.assertEqual(pr.outstanding_amount, 10000)
self.assertEqual(pr.currency, "USD")
self.assertEqual(pr.party_account_currency, "INR")
self.assertEqual(pr.status, "Requested")
pe = pr.create_payment_entry()
self.assertEqual(len(pe.references), 2)
self.assertEqual(pe.paid_amount, 10000)
# check 1st payment term
# convert it via dollar and conversion_rate
self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion
self.assertEqual(pe.references[0].payment_request, pr.name)
# check 2nd payment term
self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion
self.assertEqual(pe.references[1].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 200)
def test_payment_cancel_process(self):
so = make_sales_order(currency="INR", qty=1, rate=1000)
self.assertEqual(so.advance_payment_status, "Not Requested")
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.status, "Requested")
self.assertEqual(pr.grand_total, 1000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
so.load_from_db()
self.assertEqual(so.advance_payment_status, "Requested")
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 800
pe.references[0].allocated_amount = 800
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
self.assertEqual(so.advance_payment_status, "Partially Paid")
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 200)
self.assertEqual(pr.grand_total, 1000)
# cancelling PE
pe.cancel()
pr.load_from_db()
self.assertEqual(pr.status, "Requested")
self.assertEqual(pr.outstanding_amount, 1000)
self.assertEqual(pr.grand_total, 1000)
so.load_from_db()
self.assertEqual(so.advance_payment_status, "Requested")

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestPaymentTerm(unittest.TestCase):
class TestPaymentTerm(IntegrationTestCase):
pass

View File

@@ -1,12 +1,12 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
class TestPaymentTermsTemplate(unittest.TestCase):
class TestPaymentTermsTemplate(IntegrationTestCase):
def tearDown(self):
frappe.delete_doc("Payment Terms Template", "_Test Payment Terms Template For Test", force=1)

View File

@@ -392,8 +392,7 @@ def process_closing_entries(gl_entries, closing_entries, voucher_name, company,
)
try:
if gl_entries + closing_entries:
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)

View File

@@ -1,10 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import today
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
@@ -13,7 +12,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.utils import get_fiscal_year
class TestPeriodClosingVoucher(unittest.TestCase):
class TestPeriodClosingVoucher(IntegrationTestCase):
def test_closing_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")

View File

@@ -194,7 +194,9 @@ function refresh_payments(d, frm) {
}
if (payment) {
payment.expected_amount += flt(p.amount);
payment.closing_amount = payment.expected_amount;
if (payment.closing_amount === 0) {
payment.closing_amount = payment.expected_amount;
}
payment.difference = payment.closing_amount - payment.expected_amount;
} else {
frm.add_child("payment_reconciliation", {

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
@@ -24,7 +24,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestPOSClosingEntry(unittest.TestCase):
class TestPOSClosingEntry(IntegrationTestCase):
def setUp(self):
# Make stock available for POS Sales
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)

View File

@@ -40,6 +40,19 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
};
});
this.frm.set_query("item_code", "items", function (doc) {
return {
query: "erpnext.accounts.doctype.pos_invoice.pos_invoice.item_query",
filters: {
has_variants: ["=", 0],
is_sales_item: ["=", 1],
disabled: ["=", 0],
is_fixed_asset: ["=", 0],
pos_profile: ["=", doc.pos_profile],
},
};
});
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}

View File

@@ -156,11 +156,13 @@
"more_information",
"inter_company_invoice_reference",
"customer_group",
"campaign",
"is_discounted",
"col_break23",
"utm_source",
"utm_campaign",
"utm_medium",
"column_break_gpiw",
"status",
"source",
"more_info",
"debit_to",
"party_account_currency",
@@ -1318,15 +1320,6 @@
"options": "Customer Group",
"print_hide": 1
},
{
"fieldname": "campaign",
"fieldtype": "Link",
"label": "Campaign",
"oldfieldname": "campaign",
"oldfieldtype": "Link",
"options": "Campaign",
"print_hide": 1
},
{
"default": "0",
"fieldname": "is_discounted",
@@ -1351,15 +1344,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "source",
"fieldtype": "Link",
"label": "Source",
"oldfieldname": "source",
"oldfieldtype": "Select",
"options": "Lead Source",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "more_info",
@@ -1558,12 +1542,41 @@
"fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note"
},
{
"fieldname": "column_break_gpiw",
"fieldtype": "Column Break"
},
{
"fieldname": "utm_medium",
"print_hide": 1,
"fieldtype": "Link",
"label": "Medium",
"options": "UTM Medium"
},
{
"fieldname": "utm_campaign",
"fieldtype": "Link",
"label": "Campaign",
"oldfieldname": "campaign",
"oldfieldtype": "Link",
"options": "UTM Campaign",
"print_hide": 1
},
{
"fieldname": "utm_source",
"fieldtype": "Link",
"label": "Source",
"oldfieldname": "source",
"oldfieldtype": "Select",
"options": "UTM Source",
"print_hide": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:14.787999",
"modified": "2024-06-28 10:55:34.941200",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1617,4 +1630,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _, bold
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
from frappe.utils.nestedset import get_descendants_of
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
@@ -15,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_multi_mode_option,
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.controllers.queries import item_query as _item_query
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -66,7 +68,6 @@ class POSInvoice(SalesInvoice):
base_total: DF.Currency
base_total_taxes_and_charges: DF.Currency
base_write_off_amount: DF.Currency
campaign: DF.Link | None
cash_bank_account: DF.Link | None
change_amount: DF.Currency
commission_rate: DF.Float
@@ -141,7 +142,6 @@ class POSInvoice(SalesInvoice):
shipping_address: DF.TextEditor | None
shipping_address_name: DF.Link | None
shipping_rule: DF.Link | None
source: DF.Link | None
status: DF.Literal[
"",
"Draft",
@@ -176,6 +176,9 @@ class POSInvoice(SalesInvoice):
update_billed_amount_in_delivery_note: DF.Check
update_billed_amount_in_sales_order: DF.Check
update_stock: DF.Check
utm_campaign: DF.Link | None
utm_medium: DF.Link | None
utm_source: DF.Link | None
write_off_account: DF.Link | None
write_off_amount: DF.Currency
write_off_cost_center: DF.Link | None
@@ -449,7 +452,7 @@ class POSInvoice(SalesInvoice):
if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
if self.is_return:
if self.is_return and self.docstatus != 0:
invoice_total = self.rounded_total or self.grand_total
total_amount_in_payments = flt(total_amount_in_payments, self.precision("grand_total"))
if total_amount_in_payments and total_amount_in_payments < invoice_total:
@@ -642,7 +645,9 @@ class POSInvoice(SalesInvoice):
if profile:
return {
"print_format": print_format,
"campaign": profile.get("campaign"),
"utm_source": profile.get("utm_source"),
"utm_campaign": profile.get("utm_campaign"),
"utm_medium": profile.get("utm_medium"),
"allow_print_before_pay": profile.get("allow_print_before_pay"),
}
@@ -837,3 +842,30 @@ def add_return_modes(doc, pos_profile):
]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
append_payment(payment_mode[0])
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
if pos_profile := filters.get("pos_profile")[1]:
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
if item_groups := get_item_group(pos_profile):
filters["item_group"] = ["in", tuple(item_groups)]
del filters["pos_profile"]
else:
filters.pop("pos_profile", None)
return _item_query(doctype, txt, searchfield, start, page_len, filters, as_dict)
def get_item_group(pos_profile):
item_groups = []
if pos_profile.get("item_groups"):
# Get items based on the item groups defined in the POS profile
for row in pos_profile.get("item_groups"):
item_groups.append(row.item_group)
item_groups.extend(get_descendants_of("Item Group", row.item_group))
return list(set(item_groups))

View File

@@ -1,11 +1,11 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import copy
import unittest
import frappe
from frappe import _
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
@@ -20,7 +20,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestPOSInvoice(unittest.TestCase):
class TestPOSInvoice(IntegrationTestCase):
@classmethod
def setUpClass(cls):
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)

View File

@@ -37,6 +37,7 @@
"column_break_19",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"section_break1",
"rate",
@@ -847,6 +848,12 @@
{
"fieldname": "column_break_ciit",
"fieldtype": "Column Break"
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
}
],
"istable": 1,

View File

@@ -39,6 +39,7 @@ class POSInvoiceItem(SalesInvoiceItem):
description: DF.TextEditor
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
dn_detail: DF.Data | None
enable_deferred_revenue: DF.Check
expense_account: DF.Link | None

View File

@@ -1,10 +1,9 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import json
import unittest
import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.tests.utils import change_settings
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
@@ -19,7 +18,16 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestPOSInvoiceMergeLog(unittest.TestCase):
class UnitTestPosInvoiceMergeLog(UnitTestCase):
"""
Unit tests for PosInvoiceMergeLog.
Use this class for testing individual functions and methods.
"""
pass
class TestPOSInvoiceMergeLog(IntegrationTestCase):
def test_consolidated_invoice_creation(self):
frappe.db.sql("delete from `tabPOS Invoice`")
@@ -288,7 +296,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
@change_settings(
@IntegrationTestCase.change_settings(
"System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3}
)
def test_consolidation_round_off_error_3(self):

View File

@@ -1,12 +1,12 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
class TestPOSOpeningEntry(unittest.TestCase):
class TestPOSOpeningEntry(IntegrationTestCase):
pass

View File

@@ -12,7 +12,9 @@
"disabled",
"column_break_9",
"warehouse",
"campaign",
"utm_source",
"utm_campaign",
"utm_medium",
"company_address",
"section_break_15",
"applicable_for_users",
@@ -88,12 +90,6 @@
"fieldtype": "Read Only",
"label": "Country"
},
{
"fieldname": "campaign",
"fieldtype": "Link",
"label": "Campaign",
"options": "Campaign"
},
{
"fieldname": "company_address",
"fieldtype": "Link",
@@ -375,6 +371,27 @@
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
},
{
"fieldname": "utm_campaign",
"print_hide": 1,
"fieldtype": "Link",
"label": "Campaign",
"options": "UTM Campaign"
},
{
"fieldname": "utm_source",
"print_hide": 1,
"fieldtype": "Link",
"label": "Source",
"options": "UTM Source"
},
{
"fieldname": "utm_medium",
"print_hide": 1,
"fieldtype": "Link",
"label": "Medium",
"options": "UTM Campaign"
}
],
"icon": "icon-cog",
@@ -402,7 +419,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2024-03-27 13:10:16.476972",
"modified": "2024-06-28 10:51:48.543766",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
@@ -431,4 +448,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -28,7 +28,6 @@ class POSProfile(Document):
applicable_for_users: DF.Table[POSProfileUser]
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
auto_add_item_to_cart: DF.Check
campaign: DF.Link | None
company: DF.Link
company_address: DF.Link | None
cost_center: DF.Link | None
@@ -53,6 +52,9 @@ class POSProfile(Document):
taxes_and_charges: DF.Link | None
tc_name: DF.Link | None
update_stock: DF.Check
utm_campaign: DF.Link | None
utm_medium: DF.Link | None
utm_source: DF.Link | None
validate_stock_on_save: DF.Check
warehouse: DF.Link
write_off_account: DF.Link

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.pos_profile.pos_profile import (
get_child_nodes,
@@ -13,7 +13,7 @@ from erpnext.stock.get_item_details import get_pos_profile
test_dependencies = ["Item"]
class TestPOSProfile(unittest.TestCase):
class TestPOSProfile(IntegrationTestCase):
def test_pos_profile(self):
make_pos_profile()
@@ -50,7 +50,7 @@ def get_customers_list(pos_profile=None):
customer_groups.extend(
[d.get("name") for d in get_child_nodes("Customer Group", d.get("customer_group"))]
)
cond = "customer_group in (%s)" % (", ".join(["%s"] * len(customer_groups)))
cond = "customer_group in ({})".format(", ".join(["%s"] * len(customer_groups)))
return (
frappe.db.sql(
@@ -72,7 +72,7 @@ def get_items_list(pos_profile, company):
for d in pos_profile.get("item_groups"):
args_list.extend([d.name for d in get_child_nodes("Item Group", d.item_group)])
if args_list:
cond = "and i.item_group in (%s)" % (", ".join(["%s"] * len(args_list)))
cond = "and i.item_group in ({})".format(", ".join(["%s"] * len(args_list)))
return frappe.db.sql(
f"""

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestPOSProfileUser(unittest.TestCase):
class TestPOSProfileUser(IntegrationTestCase):
pass

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestPOSSettings(unittest.TestCase):
class TestPOSSettings(IntegrationTestCase):
pass

View File

@@ -282,7 +282,7 @@
"fieldname": "campaign",
"fieldtype": "Link",
"label": "Campaign",
"options": "Campaign"
"options": "UTM Campaign"
},
{
"depends_on": "eval:doc.applicable_for==\"Supplier\"",
@@ -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",

View File

@@ -5,6 +5,7 @@
import unittest
import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
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,16 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.get_item_details import get_item_details
class TestPricingRule(unittest.TestCase):
class UnitTestPricingRule(UnitTestCase):
"""
Unit tests for PricingRule.
Use this class for testing individual functions and methods.
"""
pass
class TestPricingRule(IntegrationTestCase):
def setUp(self):
delete_existing_pricing_rules()
setup_pricing_rule_data()
@@ -1377,7 +1387,7 @@ class TestPricingRule(unittest.TestCase):
pi.cancel()
test_dependencies = ["Campaign"]
test_dependencies = ["UTM Campaign"]
def make_pricing_rule(**args):
@@ -1437,9 +1447,9 @@ def make_pricing_rule(**args):
def setup_pricing_rule_data():
if not frappe.db.exists("Campaign", "_Test Campaign"):
if not frappe.db.exists("UTM Campaign", "_Test Campaign"):
frappe.get_doc(
{"doctype": "Campaign", "campaign_name": "_Test Campaign", "name": "_Test Campaign"}
{"doctype": "UTM Campaign", "description": "_Test Campaign", "name": "_Test Campaign"}
).insert()

Some files were not shown because too many files have changed in this diff Show More