mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-12 19:35:09 +00:00
Merge branch 'develop' of github.com:aerele/erpnext into update_actual_qty
This commit is contained in:
8
.github/stale.yml
vendored
8
.github/stale.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -17,3 +17,5 @@ __pycache__
|
||||
.helix/
|
||||
node_modules/
|
||||
.backportrc.json
|
||||
# Aider AI Chat
|
||||
.aider*
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]},
|
||||
],
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'")
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user