Merge branch 'develop' into fix-voucher-types

This commit is contained in:
Smit Vora
2024-11-07 13:11:55 +05:30
committed by GitHub
670 changed files with 389420 additions and 376772 deletions

View File

@@ -12,9 +12,16 @@ pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}} githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"} frappeuser=${FRAPPE_USER:-"frappe"}
frappebranch=${FRAPPE_BRANCH:-$githubbranch} frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
mkdir frappe
pushd frappe
git init
git remote add origin "https://github.com/${frappeuser}/frappe"
git fetch origin "${frappecommitish}" --depth 1
git checkout FETCH_HEAD
popd
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site mkdir ~/frappe-bench/sites/test_site

View File

@@ -137,7 +137,8 @@ jobs:
update_to_version 15 update_to_version 15
echo "Updating to latest version" echo "Updating to latest version"
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/frappe" checkout -q -f FETCH_HEAD
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
pgrep honcho | xargs kill pgrep honcho | xargs kill

View File

@@ -0,0 +1,130 @@
name: Individual
on:
workflow_dispatch:
concurrency:
group: server-individual-tests-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: false
jobs:
discover:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Clone
uses: actions/checkout@v4
- id: set-matrix
run: |
# Use grep and find to get the list of test files
matrix=$(find . -path '*/doctype/*/test_*.py' | xargs grep -l 'def test_' | awk '{
# Remove ./ prefix, file extension, and replace / with .
gsub(/^\.\//, "", $0)
gsub(/\.py$/, "", $0)
gsub(/\//, ".", $0)
# Add to array
tests[NR] = $0
}
END {
# Start JSON array
printf "{\n \"include\": [\n"
# Loop through array and create JSON objects
for (i=1; i<=NR; i++) {
printf " {\"test\": \"%s\"}", tests[i]
if (i < NR) printf ","
printf "\n"
}
# Close JSON array
printf " ]\n}"
}')
# Output the matrix
echo "matrix=$(echo "$matrix" | jq -c)" >> $GITHUB_OUTPUT
# For debugging (optional)
echo "Generated matrix:"
echo "$matrix"
test:
needs: discover
runs-on: ubuntu-latest
timeout-minutes: 60
env:
NODE_ENV: "production"
strategy:
fail-fast: false
matrix: ${{fromJson(needs.discover.outputs.matrix)}}
name: Test
services:
mysql:
image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
run: 'cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --module ${{ matrix.test }}'

View File

@@ -1,6 +1,8 @@
name: Server (Mariadb) name: Server (Mariadb)
on: on:
repository_dispatch:
types: [frappe-framework-change]
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
@@ -117,10 +119,10 @@ jobs:
DB: mariadb DB: mariadb
TYPE: server TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }} FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.inputs.branch }} FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
- name: Run Tests - 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: env:
TYPE: server TYPE: server
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }} CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}

View File

@@ -58,7 +58,7 @@ def build_conditions(process_type, account, company):
) )
if account: if account:
conditions += f"AND {deferred_account}='{account}'" conditions += f"AND {deferred_account}={frappe.db.escape(account)}"
elif company: elif company:
conditions += f"AND p.company = {frappe.db.escape(company)}" conditions += f"AND p.company = {frappe.db.escape(company)}"

View File

@@ -102,14 +102,12 @@ class Account(NestedSet):
self.name = get_autoname_with_number(self.account_number, self.account_name, self.company) self.name = get_autoname_with_number(self.account_number, self.account_name, self.company)
def validate(self): def validate(self):
from erpnext.accounts.utils import validate_field_number
if frappe.local.flags.allow_unverified_charts: if frappe.local.flags.allow_unverified_charts:
return return
self.validate_parent() self.validate_parent()
self.validate_parent_child_account_type() self.validate_parent_child_account_type()
self.validate_root_details() 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.validate_group_or_ledger()
self.set_root_and_report_type() self.set_root_and_report_type()
self.validate_mandatory() self.validate_mandatory()
@@ -310,6 +308,22 @@ class Account(NestedSet):
if frappe.db.get_value("GL Entry", {"account": self.name}): if frappe.db.get_value("GL Entry", {"account": self.name}):
frappe.throw(_("Currency can not be changed after making entries using some other currency")) 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): def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name):
for company in descendants: for company in descendants:
company_bold = frappe.bold(company) company_bold = frappe.bold(company)
@@ -463,19 +477,6 @@ def get_account_autoname(account_number, account_name, company):
return " - ".join(parts) 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() @frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False): def update_account_number(name, account_name, account_number=None, from_descendant=False):
account = frappe.get_cached_doc("Account", name) account = frappe.get_cached_doc("Account", name)
@@ -516,7 +517,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
frappe.throw(message, title=_("Rename Not Allowed")) frappe.throw(message, title=_("Rename Not Allowed"))
validate_account_number(name, account_number, account.company) account.validate_account_number(account_number)
if account_number: if account_number:
frappe.db.set_value("Account", name, "account_number", account_number.strip()) frappe.db.set_value("Account", name, "account_number", account_number.strip())
else: else:

View File

@@ -1,11 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest import unittest
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.tests import IntegrationTestCase
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.accounts.doctype.account.account import ( from erpnext.accounts.doctype.account.account import (
@@ -15,10 +13,10 @@ from erpnext.accounts.doctype.account.account import (
) )
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"] EXTRA_TEST_RECORD_DEPENDENCIES = ["Company"]
class TestAccount(unittest.TestCase): class TestAccount(IntegrationTestCase):
def test_rename_account(self): def test_rename_account(self):
if not frappe.db.exists("Account", "1210 - Debtors - _TC"): if not frappe.db.exists("Account", "1210 - Debtors - _TC"):
acc = frappe.new_doc("Account") acc = frappe.new_doc("Account")
@@ -203,8 +201,6 @@ class TestAccount(unittest.TestCase):
In a parent->child company setup, child should inherit parent account currency if explicitly specified. In a parent->child company setup, child should inherit parent account currency if explicitly specified.
""" """
make_test_records("Company")
frappe.local.flags.pop("ignore_root_company_validation", None) frappe.local.flags.pop("ignore_root_company_validation", None)
def create_bank_account(): def create_bank_account():
@@ -328,7 +324,7 @@ class TestAccount(unittest.TestCase):
def _make_test_records(verbose=None): def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects from frappe.tests.utils import make_test_objects
accounts = [ accounts = [
# [account_name, parent_account, is_group] # [account_name, parent_account, is_group]

View File

@@ -1,6 +0,0 @@
[
{
"doctype": "Account",
"name": "_Test Account 1"
}
]

View File

@@ -0,0 +1,3 @@
[[Account]]
name = "_Test Account 1"

View File

@@ -113,9 +113,9 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
entries = [] entries = []
last_period_closing_voucher = frappe.db.get_all( last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher", "Period Closing Voucher",
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)}, filters={"docstatus": 1, "company": company, "period_end_date": ("<", closing_date)},
fields=["name"], fields=["name"],
order_by="posting_date desc", order_by="period_end_date desc",
limit=1, limit=1,
) )

View File

@@ -2,8 +2,17 @@
# See license.txt # See license.txt
# import frappe # 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 pass

View File

@@ -58,7 +58,7 @@ frappe.ui.form.on("Accounting Dimension", {
}, },
label: function (frm) { label: function (frm) {
frm.set_value("fieldname", frappe.model.scrub(frm.doc.label)); frm.set_value("fieldname", frm.doc.label.replace(/ /g, "_").replace(/-/g, "_").toLowerCase());
}, },
document_type: function (frm) { document_type: function (frm) {

View File

@@ -7,6 +7,7 @@ import json
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.database.schema import validate_column_name
from frappe.model import core_doctypes_list from frappe.model import core_doctypes_list
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr from frappe.utils import cstr
@@ -60,6 +61,7 @@ class AccountingDimension(Document):
if not self.is_new(): if not self.is_new():
self.validate_document_type_change() self.validate_document_type_change()
validate_column_name(self.fieldname)
self.validate_dimension_defaults() self.validate_dimension_defaults()
def validate_document_type_change(self): def validate_document_type_change(self):

View File

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

View File

@@ -12,7 +12,7 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
test_dependencies = ["Location", "Cost Center", "Department"] EXTRA_TEST_RECORD_DEPENDENCIES = ["Location", "Cost Center", "Department"]
class TestAccountingDimensionFilter(unittest.TestCase): class TestAccountingDimensionFilter(unittest.TestCase):

View File

@@ -101,6 +101,8 @@ def validate_accounting_period_on_doc_save(doc, method=None):
date = doc.available_for_use_date date = doc.available_for_use_date
elif doc.doctype == "Asset Repair": elif doc.doctype == "Asset Repair":
date = doc.completion_date date = doc.completion_date
elif doc.doctype == "Period Closing Voucher":
date = doc.period_end_date
else: else:
date = doc.posting_date date = doc.posting_date

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Advance Payment Ledger Entry", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,113 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-10-16 16:57:12.085072",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"company",
"voucher_type",
"voucher_no",
"against_voucher_type",
"against_voucher_no",
"amount",
"currency",
"event"
],
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": "Voucher No",
"options": "voucher_type",
"read_only": 1
},
{
"fieldname": "against_voucher_type",
"fieldtype": "Link",
"label": "Against Voucher Type",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "against_voucher_no",
"fieldtype": "Dynamic Link",
"label": "Against Voucher No",
"options": "against_voucher_type",
"read_only": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "event",
"fieldtype": "Data",
"label": "Event",
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-11-05 10:31:28.736671",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Payment Ledger Entry",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Auditor",
"share": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class AdvancePaymentLedgerEntry(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
against_voucher_no: DF.DynamicLink | None
against_voucher_type: DF.Link | None
amount: DF.Currency
company: DF.Link | None
currency: DF.Link | None
event: DF.Data | None
voucher_no: DF.DynamicLink | None
voucher_type: DF.Link | None
# end: auto-generated types
pass

View File

@@ -0,0 +1,228 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import nowdate, today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
# On IntegrationTestCase, the doctype test records and all
# link-field test record depdendencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, IntegrationTestCase):
"""
Integration tests for AdvancePaymentLedgerEntry.
Use this class for testing interactions between multiple components.
"""
def setUp(self):
self.create_company()
self.create_usd_receivable_account()
self.create_usd_payable_account()
self.create_item()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
"""
Helper method
"""
so = make_sales_order(
company=self.company,
customer=self.customer,
currency=currency,
item=self.item,
qty=qty,
rate=rate,
transaction_date=today(),
do_not_submit=do_not_submit,
)
return so
def create_purchase_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
"""
Helper method
"""
po = create_purchase_order(
company=self.company,
customer=self.supplier,
currency=currency,
item=self.item,
qty=qty,
rate=rate,
transaction_date=today(),
do_not_submit=do_not_submit,
)
return po
def test_so_advance_paid_and_currency_with_payment(self):
self.create_customer("_Test USD Customer", "USD")
so = self.create_sales_order(currency="USD", do_not_submit=True)
so.conversion_rate = 80
so.submit()
pe_exchange_rate = 85
pe = get_payment_entry(so.doctype, so.name, bank_account=self.cash)
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_from = self.debtors_usd
pe.paid_from_account_currency = "USD"
pe.source_exchange_rate = pe_exchange_rate
pe.paid_amount = so.grand_total
pe.received_amount = pe_exchange_rate * pe.paid_amount
pe.references[0].outstanding_amount = 100
pe.references[0].total_amount = 100
pe.references[0].allocated_amount = 100
pe.save().submit()
so.reload()
self.assertEqual(so.advance_paid, 100)
self.assertEqual(so.party_account_currency, "USD")
# cancel advance payment
pe.reload()
pe.cancel()
so.reload()
self.assertEqual(so.advance_paid, 0)
self.assertEqual(so.party_account_currency, "USD")
def test_so_advance_paid_and_currency_with_journal(self):
self.create_customer("_Test USD Customer", "USD")
so = self.create_sales_order(currency="USD", do_not_submit=True)
so.conversion_rate = 80
so.submit()
je_exchange_rate = 85
je = frappe.get_doc(
{
"doctype": "Journal Entry",
"company": self.company,
"voucher_type": "Journal Entry",
"posting_date": so.transaction_date,
"multi_currency": True,
"accounts": [
{
"account": self.debtors_usd,
"party_type": "Customer",
"party": so.customer,
"credit": 8500,
"credit_in_account_currency": 100,
"is_advance": "Yes",
"reference_type": so.doctype,
"reference_name": so.name,
"exchange_rate": je_exchange_rate,
},
{
"account": self.cash,
"debit": 8500,
"debit_in_account_currency": 8500,
},
],
}
)
je.save().submit()
so.reload()
self.assertEqual(so.advance_paid, 100)
self.assertEqual(so.party_account_currency, "USD")
# cancel advance payment
je.reload()
je.cancel()
so.reload()
self.assertEqual(so.advance_paid, 0)
self.assertEqual(so.party_account_currency, "USD")
def test_po_advance_paid_and_currency_with_payment(self):
self.create_supplier("_Test USD Supplier", "USD")
po = self.create_purchase_order(currency="USD", do_not_submit=True)
po.conversion_rate = 80
po.submit()
pe_exchange_rate = 85
pe = get_payment_entry(po.doctype, po.name, bank_account=self.cash)
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_to = self.creditors_usd
pe.paid_to_account_currency = "USD"
pe.target_exchange_rate = pe_exchange_rate
pe.received_amount = po.grand_total
pe.paid_amount = pe_exchange_rate * pe.received_amount
pe.references[0].outstanding_amount = 100
pe.references[0].total_amount = 100
pe.references[0].allocated_amount = 100
pe.save().submit()
po.reload()
self.assertEqual(po.advance_paid, 100)
self.assertEqual(po.party_account_currency, "USD")
# cancel advance payment
pe.reload()
pe.cancel()
po.reload()
self.assertEqual(po.advance_paid, 0)
self.assertEqual(po.party_account_currency, "USD")
def test_po_advance_paid_and_currency_with_journal(self):
self.create_supplier("_Test USD Supplier", "USD")
po = self.create_purchase_order(currency="USD", do_not_submit=True)
po.conversion_rate = 80
po.submit()
je_exchange_rate = 85
je = frappe.get_doc(
{
"doctype": "Journal Entry",
"company": self.company,
"voucher_type": "Journal Entry",
"posting_date": po.transaction_date,
"multi_currency": True,
"accounts": [
{
"account": self.creditors_usd,
"party_type": "Supplier",
"party": po.supplier,
"debit": 8500,
"debit_in_account_currency": 100,
"is_advance": "Yes",
"reference_type": po.doctype,
"reference_name": po.name,
"exchange_rate": je_exchange_rate,
},
{
"account": self.cash,
"credit": 8500,
"credit_in_account_currency": 8500,
},
],
}
)
je.save().submit()
po.reload()
self.assertEqual(po.advance_paid, 100)
self.assertEqual(po.party_account_currency, "USD")
# cancel advance payment
je.reload()
je.cancel()
po.reload()
self.assertEqual(po.advance_paid, 0)
self.assertEqual(po.party_account_currency, "USD")

View File

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

View File

@@ -208,8 +208,49 @@
"label": "Disabled" "label": "Disabled"
} }
], ],
"links": [], "links": [
"modified": "2024-03-27 13:06:37.049542", {
"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": "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-10-30 09:41:14.113414",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Account", "name": "Bank Account",

View File

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

View File

@@ -1,15 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest import unittest
import frappe import frappe
from frappe import ValidationError 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): def test_validate_iban(self):
valid_ibans = [ valid_ibans = [
"GB82 WEST 1234 5698 7654 32", "GB82 WEST 1234 5698 7654 32",

View File

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

View File

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

View File

@@ -108,8 +108,18 @@ class BankClearance(Document):
if not d.clearance_date: if not d.clearance_date:
d.clearance_date = None d.clearance_date = None
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry) if d.payment_document == "Sales Invoice":
payment_entry.db_set("clearance_date", d.clearance_date) 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 clearance_date_updated = True
@@ -158,7 +168,7 @@ def get_payment_entries_for_bank_clearance(
"Payment Entry" as payment_document, name as payment_entry, "Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date, reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit, if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit, if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date, posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry` from `tabPayment Entry`

View File

@@ -1,21 +1,32 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_months, getdate 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.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.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 from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
class TestBankClearance(unittest.TestCase): class TestBankClearance(IntegrationTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
clear_payment_entries() super().setUpClass()
clear_loan_transactions() 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")
make_bank_account() make_bank_account()
add_transactions() add_transactions()
@@ -83,18 +94,31 @@ class TestBankClearance(unittest.TestCase):
bank_clearance.get_payment_entries() bank_clearance.get_payment_entries()
self.assertEqual(len(bank_clearance.payment_entries), 3) self.assertEqual(len(bank_clearance.payment_entries), 3)
def test_update_clearance_date_on_si(self):
sales_invoice = make_pos_sales_invoice()
def clear_payment_entries(): date = getdate()
frappe.db.delete("Payment Entry") 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
@if_lending_app_installed bank_clearance.update_clearance_date()
def clear_loan_transactions():
for dt in [ si_clearance_date = frappe.db.get_value(
"Loan Disbursement", "Sales Invoice Payment",
"Loan Repayment", {"parent": sales_invoice.name, "account": bank_clearance.account},
]: "clearance_date",
frappe.db.delete(dt) )
self.assertEqual(si_clearance_date, date)
def make_bank_account(): def make_bank_account():
@@ -115,9 +139,45 @@ def add_transactions():
def make_payment_entry(): 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 = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
pe.reference_no = "Conrad Oct 18" pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24" pe.reference_date = "2018-10-24"
pe.insert() pe.insert()
pe.submit() pe.submit()
def make_pos_sales_invoice():
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
mode_of_payment.append(
"accounts", {"company": "_Test Company", "default_account": "_Test Bank Clearance - _TC"}
)
mode_of_payment.save()
customer = make_customer(customer="_Test Customer")
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
si.set("payments", [])
si.append(
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
)
si.insert()
si.submit()
return si

View File

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

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, today from frappe.utils import add_days, today
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( 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 from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase): class TestBankReconciliationTool(AccountsTestMixin, IntegrationTestCase):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

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

View File

@@ -2,13 +2,22 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account 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 @classmethod
def setUpClass(cls): def setUpClass(cls):
create_bank_account() create_bank_account()

View File

@@ -6,7 +6,7 @@ import json
import frappe import frappe
from frappe import utils from frappe import utils
from frappe.model.docstatus import DocStatus 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 ( from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
get_linked_payments, get_linked_payments,
@@ -18,19 +18,20 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import if_lending_app_installed from erpnext.tests.utils import if_lending_app_installed
test_dependencies = ["Item", "Cost Center"] EXTRA_TEST_RECORD_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): def setUp(self):
for dt in [
"Bank Transaction",
"Payment Entry",
"Payment Entry Reference",
"POS Profile",
]:
frappe.db.delete(dt)
clear_loan_transactions()
make_pos_profile() make_pos_profile()
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
@@ -222,11 +223,6 @@ class TestBankTransaction(FrappeTestCase):
self.assertEqual(linked_payments[0]["name"], repayment_entry.name) self.assertEqual(linked_payments[0]["name"], repayment_entry.name)
@if_lending_app_installed
def clear_loan_transactions():
frappe.db.delete("Loan Repayment")
def create_bank_account( def create_bank_account(
bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account" bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account"
): ):

View File

@@ -2,8 +2,17 @@
# See license.txt # See license.txt
# import frappe # 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 pass

View File

@@ -2,8 +2,17 @@
# See license.txt # See license.txt
# import frappe # 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 pass

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest import unittest
import frappe 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): 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()
cost_center = frappe.get_doc( cost_center = frappe.get_doc(
{ {
"doctype": "Cost Center", "doctype": "Cost Center",

View File

@@ -1,23 +0,0 @@
[
{
"company": "_Test Company",
"cost_center_name": "_Test Cost Center",
"doctype": "Cost Center",
"is_group": 0,
"parent_cost_center": "_Test Company - _TC"
},
{
"company": "_Test Company",
"cost_center_name": "_Test Cost Center 2",
"doctype": "Cost Center",
"is_group": 0,
"parent_cost_center": "_Test Company - _TC"
},
{
"company": "_Test Company",
"cost_center_name": "_Test Write Off Cost Center",
"doctype": "Cost Center",
"is_group": 0,
"parent_cost_center": "_Test Company - _TC"
}
]

View File

@@ -0,0 +1,18 @@
[["Cost Center"]]
company = "_Test Company"
cost_center_name = "_Test Cost Center"
is_group = 0
parent_cost_center = "_Test Company - _TC"
[["Cost Center"]]
company = "_Test Company"
cost_center_name = "_Test Cost Center 2"
is_group = 0
parent_cost_center = "_Test Company - _TC"
[["Cost Center"]]
company = "_Test Company"
cost_center_name = "_Test Write Off Cost Center"
is_group = 0
parent_cost_center = "_Test Company - _TC"

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, today from frappe.utils import add_days, today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -17,7 +17,7 @@ from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation impo
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
class TestCostCenterAllocation(unittest.TestCase): class TestCostCenterAllocation(IntegrationTestCase):
def setUp(self): def setUp(self):
cost_centers = [ cost_centers = [
"Main Cost Center 1", "Main Cost Center 1",

View File

@@ -1,13 +1,13 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
test_dependencies = ["Item"] EXTRA_TEST_RECORD_DEPENDENCIES = ["Item"]
def test_create_test_data(): def test_create_test_data():
@@ -110,7 +110,7 @@ def test_create_test_data():
coupon_code.insert() coupon_code.insert()
class TestCouponCode(unittest.TestCase): class TestCouponCode(IntegrationTestCase):
def setUp(self): def setUp(self):
test_create_test_data() test_create_test_data()

View File

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

View File

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

View File

@@ -220,19 +220,31 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
@frappe.whitelist() @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): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)
if not language:
language = doc.get("language")
if language: if language:
filters = {"parent": dunning_type, "language": language} letter_text = frappe.db.get_value(
else: DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
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 not letter_text:
) letter_text = frappe.db.get_value(
if letter_text: DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
return { )
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc), if not letter_text:
"language": letter_text.language, return {}
}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}

View File

@@ -1,7 +1,7 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import add_days, nowdate, today from frappe.utils import add_days, nowdate, today
from erpnext import get_default_cost_center from erpnext import get_default_cost_center
@@ -16,10 +16,19 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice_against_cost_center, create_sales_invoice_against_cost_center,
) )
test_dependencies = ["Company", "Cost Center"] EXTRA_TEST_RECORD_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 @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()

View File

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

View File

@@ -1,36 +0,0 @@
[
{
"doctype": "Dunning Type",
"dunning_type": "_Test First Notice",
"company": "_Test Company",
"is_default": 1,
"dunning_fee": 0.0,
"rate_of_interest": 0.0,
"dunning_letter_text": [
{
"language": "en",
"body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
}
],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
},
{
"doctype": "Dunning Type",
"dunning_type": "_Test Second Notice",
"company": "_Test Company",
"is_default": 0,
"dunning_fee": 10.0,
"rate_of_interest": 10.0,
"dunning_letter_text": [
{
"language": "en",
"body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
}
],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
}
]

View File

@@ -0,0 +1,28 @@
[["Dunning Type"]]
dunning_type = "_Test First Notice"
company = "_Test Company"
is_default = 1
dunning_fee = 0.0
rate_of_interest = 0.0
income_account = "Sales - _TC"
cost_center = "_Test Cost Center - _TC"
[["Dunning Type".dunning_letter_text]]
language = "en"
body_text = "We have still not received payment for our invoice"
closing_text = "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
[["Dunning Type"]]
dunning_type = "_Test Second Notice"
company = "_Test Company"
is_default = 0
dunning_fee = 10.0
rate_of_interest = 10.0
income_account = "Sales - _TC"
cost_center = "_Test Cost Center - _TC"
[["Dunning Type".dunning_letter_text]]
language = "en"
body_text = "We have still not received payment for our invoice"
closing_text = "We kindly request that you pay the outstanding amount immediately, including interest and late fees."

View File

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

View File

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

View File

@@ -4,10 +4,7 @@
frappe.ui.form.on("Fiscal Year", { frappe.ui.form.on("Fiscal Year", {
onload: function (frm) { onload: function (frm) {
if (frm.doc.__islocal) { if (frm.doc.__islocal) {
frm.set_value( frm.set_value("year_start_date", frappe.datetime.year_start());
"year_start_date",
frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)
);
} }
}, },
year_start_date: function (frm) { year_start_date: function (frm) {

View File

@@ -1,16 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import now_datetime from frappe.utils import now_datetime
test_ignore = ["Company"] IGNORE_TEST_RECORD_DEPENDENCIES = ["Company"]
class TestFiscalYear(unittest.TestCase): class TestFiscalYear(IntegrationTestCase):
def test_extra_year(self): def test_extra_year(self):
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"): if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000") frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")
@@ -39,8 +38,21 @@ def test_record_generator():
] ]
start = 2012 start = 2012
this_year = now_datetime().year
end = now_datetime().year + 25 end = now_datetime().year + 25
for year in range(start, end): # The current year fails to load with the following error:
# Year start date or end date is overlapping with 2024. To avoid please set company
# This is a quick-fix: if current FY is needed, please refactor test data properly
for year in range(start, this_year):
test_records.append(
{
"doctype": "Fiscal Year",
"year": f"_Test Fiscal Year {year}",
"year_start_date": f"{year}-01-01",
"year_end_date": f"{year}-12-31",
}
)
for year in range(this_year + 1, end):
test_records.append( test_records.append(
{ {
"doctype": "Fiscal Year", "doctype": "Fiscal Year",

View File

@@ -6,38 +6,50 @@
"document_type": "Document", "document_type": "Document",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"dates_section",
"posting_date", "posting_date",
"transaction_date", "transaction_date",
"column_break_avko",
"fiscal_year",
"due_date",
"account_details_section",
"account", "account",
"account_currency",
"column_break_ifvf",
"against",
"party_type", "party_type",
"party", "party",
"cost_center", "transaction_details_section",
"debit", "voucher_type",
"credit", "voucher_no",
"account_currency", "voucher_subtype",
"debit_in_account_currency", "transaction_currency",
"credit_in_account_currency", "column_break_dpsx",
"against",
"against_voucher_type", "against_voucher_type",
"against_voucher", "against_voucher",
"voucher_type",
"voucher_subtype",
"voucher_no",
"voucher_detail_no", "voucher_detail_no",
"transaction_exchange_rate",
"amounts_section",
"debit_in_account_currency",
"debit",
"debit_in_transaction_currency",
"column_break_bm1w",
"credit_in_account_currency",
"credit",
"credit_in_transaction_currency",
"dimensions_section",
"cost_center",
"column_break_lmnm",
"project", "project",
"remarks", "more_info_section",
"finance_book",
"company",
"is_opening", "is_opening",
"is_advance", "is_advance",
"fiscal_year", "column_break_8abq",
"company",
"finance_book",
"to_rename", "to_rename",
"due_date",
"is_cancelled", "is_cancelled",
"transaction_currency", "remarks"
"debit_in_transaction_currency",
"credit_in_transaction_currency",
"transaction_exchange_rate"
], ],
"fields": [ "fields": [
{ {
@@ -285,13 +297,67 @@
"fieldname": "voucher_subtype", "fieldname": "voucher_subtype",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Voucher Subtype" "label": "Voucher Subtype"
},
{
"fieldname": "dates_section",
"fieldtype": "Section Break",
"label": "Dates"
},
{
"fieldname": "column_break_avko",
"fieldtype": "Column Break"
},
{
"fieldname": "account_details_section",
"fieldtype": "Section Break",
"label": "Account Details"
},
{
"fieldname": "column_break_ifvf",
"fieldtype": "Column Break"
},
{
"fieldname": "transaction_details_section",
"fieldtype": "Section Break",
"label": "Transaction Details"
},
{
"fieldname": "amounts_section",
"fieldtype": "Section Break",
"label": "Amounts"
},
{
"fieldname": "column_break_dpsx",
"fieldtype": "Column Break"
},
{
"fieldname": "more_info_section",
"fieldtype": "Section Break",
"label": "More Info"
},
{
"fieldname": "column_break_bm1w",
"fieldtype": "Column Break"
},
{
"fieldname": "dimensions_section",
"fieldtype": "Section Break",
"label": "Dimensions"
},
{
"fieldname": "column_break_lmnm",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_8abq",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-list", "icon": "fa fa-list",
"idx": 1, "idx": 1,
"in_create": 1, "in_create": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:09:45.205364", "modified": "2024-08-22 13:03:39.997475",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "GL Entry", "name": "GL Entry",

View File

@@ -430,8 +430,9 @@ def update_against_account(voucher_type, voucher_no):
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"]) frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
frappe.db.add_index("GL Entry", ["posting_date", "company"])
frappe.db.add_index("GL Entry", ["party_type", "party"])
def rename_gle_sle_docs(): def rename_gle_sle_docs():

View File

@@ -1,17 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest import unittest
import frappe import frappe
from frappe.model.naming import parse_naming_series 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.gl_entry.gl_entry import rename_gle_sle_docs
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry 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): 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_account", "_Test Write Off - _TC")
frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC") frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC")

View File

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

View File

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

View File

@@ -1,79 +0,0 @@
[
{
"doctype": "Item Tax Template",
"title": "_Test Account Excise Duty @ 10",
"company": "_Test Company",
"taxes": [
{
"doctype": "Item Tax Template Detail",
"parentfield": "taxes",
"tax_rate": 10,
"tax_type": "_Test Account Excise Duty - _TC"
}
]
},
{
"doctype": "Item Tax Template",
"title": "_Test Account Excise Duty @ 12",
"company": "_Test Company",
"taxes": [
{
"doctype": "Item Tax Template Detail",
"parentfield": "taxes",
"tax_rate": 12,
"tax_type": "_Test Account Excise Duty - _TC"
}
]
},
{
"doctype": "Item Tax Template",
"title": "_Test Account Excise Duty @ 15",
"company": "_Test Company",
"taxes": [
{
"doctype": "Item Tax Template Detail",
"parentfield": "taxes",
"tax_rate": 15,
"tax_type": "_Test Account Excise Duty - _TC"
}
]
},
{
"doctype": "Item Tax Template",
"title": "_Test Account Excise Duty @ 20",
"company": "_Test Company",
"taxes": [
{
"doctype": "Item Tax Template Detail",
"parentfield": "taxes",
"tax_rate": 20,
"tax_type": "_Test Account Excise Duty - _TC"
}
]
},
{
"doctype": "Item Tax Template",
"title": "_Test Item Tax Template 1",
"company": "_Test Company",
"taxes": [
{
"doctype": "Item Tax Template Detail",
"parentfield": "taxes",
"tax_rate": 5,
"tax_type": "_Test Account Excise Duty - _TC"
},
{
"doctype": "Item Tax Template Detail",
"parentfield": "taxes",
"tax_rate": 10,
"tax_type": "_Test Account Education Cess - _TC"
},
{
"doctype": "Item Tax Template Detail",
"parentfield": "taxes",
"tax_rate": 15,
"tax_type": "_Test Account S&H Education Cess - _TC"
}
]
}
]

View File

@@ -0,0 +1,62 @@
[["Item Tax Template"]]
title = "_Test Account Excise Duty @ 10"
company = "_Test Company"
[["Item Tax Template".taxes]]
doctype = "Item Tax Template Detail"
parentfield = "taxes"
tax_rate = 10
tax_type = "_Test Account Excise Duty - _TC"
[["Item Tax Template"]]
title = "_Test Account Excise Duty @ 12"
company = "_Test Company"
[["Item Tax Template".taxes]]
doctype = "Item Tax Template Detail"
parentfield = "taxes"
tax_rate = 12
tax_type = "_Test Account Excise Duty - _TC"
[["Item Tax Template"]]
title = "_Test Account Excise Duty @ 15"
company = "_Test Company"
[["Item Tax Template".taxes]]
doctype = "Item Tax Template Detail"
parentfield = "taxes"
tax_rate = 15
tax_type = "_Test Account Excise Duty - _TC"
[["Item Tax Template"]]
title = "_Test Account Excise Duty @ 20"
company = "_Test Company"
[["Item Tax Template".taxes]]
doctype = "Item Tax Template Detail"
parentfield = "taxes"
tax_rate = 20
tax_type = "_Test Account Excise Duty - _TC"
[["Item Tax Template"]]
title = "_Test Item Tax Template 1"
company = "_Test Company"
[["Item Tax Template".taxes]]
doctype = "Item Tax Template Detail"
parentfield = "taxes"
tax_rate = 5
tax_type = "_Test Account Excise Duty - _TC"
[["Item Tax Template".taxes]]
doctype = "Item Tax Template Detail"
parentfield = "taxes"
tax_rate = 10
tax_type = "_Test Account Education Cess - _TC"
[["Item Tax Template".taxes]]
doctype = "Item Tax Template Detail"
parentfield = "taxes"
tax_rate = 15
tax_type = "_Test Account S&H Education Cess - _TC"

View File

@@ -188,6 +188,7 @@ class JournalEntry(AccountsController):
self.validate_cheque_info() self.validate_cheque_info()
self.check_credit_limit() self.check_credit_limit()
self.make_gl_entries() self.make_gl_entries()
self.make_advance_payment_ledger_entries()
self.update_advance_paid() self.update_advance_paid()
self.update_asset_value() self.update_asset_value()
self.update_inter_company_jv() self.update_inter_company_jv()
@@ -218,8 +219,10 @@ class JournalEntry(AccountsController):
"Repost Accounting Ledger Items", "Repost Accounting Ledger Items",
"Unreconcile Payment", "Unreconcile Payment",
"Unreconcile Payment Entries", "Unreconcile Payment Entries",
"Advance Payment Ledger Entry",
) )
self.make_gl_entries(1) self.make_gl_entries(1)
self.make_advance_payment_ledger_entries()
self.update_advance_paid() self.update_advance_paid()
self.unlink_advance_entry_reference() self.unlink_advance_entry_reference()
self.unlink_asset_reference() self.unlink_asset_reference()
@@ -262,7 +265,7 @@ class JournalEntry(AccountsController):
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation")) frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
def validate_stock_accounts(self): def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name) stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
for account in stock_accounts: for account in stock_accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
account, self.posting_date, self.company account, self.posting_date, self.company
@@ -1673,6 +1676,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
"debit": "credit", "debit": "credit",
"credit_in_account_currency": "debit_in_account_currency", "credit_in_account_currency": "debit_in_account_currency",
"credit": "debit", "credit": "debit",
"reference_type": "reference_type",
"reference_name": "reference_name",
}, },
}, },
}, },

View File

@@ -1,11 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import change_settings from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -13,25 +10,36 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInv
from erpnext.exceptions import InvalidAccountCurrency from erpnext.exceptions import InvalidAccountCurrency
class TestJournalEntry(unittest.TestCase): class UnitTestJournalEntry(UnitTestCase):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) """
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): def test_journal_entry_with_against_jv(self):
jv_invoice = frappe.copy_doc(test_records[2]) jv_invoice = frappe.copy_doc(self.globalTestRecords["Journal Entry"][2])
base_jv = frappe.copy_doc(test_records[0]) base_jv = frappe.copy_doc(self.globalTestRecords["Journal Entry"][0])
self.jv_against_voucher_testcase(base_jv, jv_invoice) self.jv_against_voucher_testcase(base_jv, jv_invoice)
def test_jv_against_sales_order(self): def test_jv_against_sales_order(self):
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
sales_order = make_sales_order(do_not_save=True) sales_order = make_sales_order(do_not_save=True)
base_jv = frappe.copy_doc(test_records[0]) base_jv = frappe.copy_doc(self.globalTestRecords["Journal Entry"][0])
self.jv_against_voucher_testcase(base_jv, sales_order) self.jv_against_voucher_testcase(base_jv, sales_order)
def test_jv_against_purchase_order(self): def test_jv_against_purchase_order(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
purchase_order = create_purchase_order(do_not_save=True) purchase_order = create_purchase_order(do_not_save=True)
base_jv = frappe.copy_doc(test_records[1]) base_jv = frappe.copy_doc(self.globalTestRecords["Journal Entry"][1])
self.jv_against_voucher_testcase(base_jv, purchase_order) self.jv_against_voucher_testcase(base_jv, purchase_order)
def jv_against_voucher_testcase(self, base_jv, test_voucher): def jv_against_voucher_testcase(self, base_jv, test_voucher):
@@ -515,6 +523,23 @@ class TestJournalEntry(unittest.TestCase):
self.assertEqual(row.debit_in_account_currency, 100) self.assertEqual(row.debit_in_account_currency, 100)
self.assertEqual(row.credit_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( def make_journal_entry(
account1, account1,
@@ -563,6 +588,3 @@ def make_journal_entry(
jv.submit() jv.submit()
return jv return jv
test_records = frappe.get_test_records("Journal Entry")

View File

@@ -1,94 +0,0 @@
[
{
"cheque_date": "2013-03-14",
"cheque_no": "33",
"company": "_Test Company",
"doctype": "Journal Entry",
"accounts": [
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 400.0,
"debit_in_account_currency": 0.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "_Test Cost Center - _TC"
},
{
"account": "_Test Bank - _TC",
"credit_in_account_currency": 0.0,
"debit_in_account_currency": 400.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "_Test Cost Center - _TC"
}
],
"naming_series": "_T-Journal Entry-",
"posting_date": "2013-02-14",
"user_remark": "test",
"voucher_type": "Bank Entry"
},
{
"cheque_date": "2013-02-14",
"cheque_no": "33",
"company": "_Test Company",
"doctype": "Journal Entry",
"accounts": [
{
"account": "_Test Payable - _TC",
"party_type": "Supplier",
"party": "_Test Supplier",
"credit_in_account_currency": 0.0,
"debit_in_account_currency": 400.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "_Test Cost Center - _TC"
},
{
"account": "_Test Bank - _TC",
"credit_in_account_currency": 400.0,
"debit_in_account_currency": 0.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "_Test Cost Center - _TC"
}
],
"naming_series": "_T-Journal Entry-",
"posting_date": "2013-02-14",
"user_remark": "test",
"voucher_type": "Bank Entry"
},
{
"cheque_date": "2013-02-14",
"cheque_no": "33",
"company": "_Test Company",
"doctype": "Journal Entry",
"accounts": [
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 0.0,
"debit_in_account_currency": 400.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "_Test Cost Center - _TC"
},
{
"account": "Sales - _TC",
"credit_in_account_currency": 400.0,
"debit_in_account_currency": 0.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "_Test Cost Center - _TC"
}
],
"naming_series": "_T-Journal Entry-",
"posting_date": "2013-02-14",
"user_remark": "test",
"voucher_type": "Bank Entry"
}
]

View File

@@ -0,0 +1,81 @@
[["Journal Entry"]]
cheque_date = "2013-03-14"
cheque_no = "33"
company = "_Test Company"
naming_series = "_T-Journal Entry-"
posting_date = "2013-02-14"
user_remark = "test"
voucher_type = "Bank Entry"
[["Journal Entry".accounts]]
account = "Debtors - _TC"
party_type = "Customer"
party = "_Test Customer"
credit_in_account_currency = 400.0
debit_in_account_currency = 0.0
doctype = "Journal Entry Account"
parentfield = "accounts"
cost_center = "_Test Cost Center - _TC"
[["Journal Entry".accounts]]
account = "_Test Bank - _TC"
credit_in_account_currency = 0.0
debit_in_account_currency = 400.0
doctype = "Journal Entry Account"
parentfield = "accounts"
cost_center = "_Test Cost Center - _TC"
[["Journal Entry"]]
cheque_date = "2013-02-14"
cheque_no = "33"
company = "_Test Company"
naming_series = "_T-Journal Entry-"
posting_date = "2013-02-14"
user_remark = "test"
voucher_type = "Bank Entry"
[["Journal Entry".accounts]]
account = "_Test Payable - _TC"
party_type = "Supplier"
party = "_Test Supplier"
credit_in_account_currency = 0.0
debit_in_account_currency = 400.0
doctype = "Journal Entry Account"
parentfield = "accounts"
cost_center = "_Test Cost Center - _TC"
[["Journal Entry".accounts]]
account = "_Test Bank - _TC"
credit_in_account_currency = 400.0
debit_in_account_currency = 0.0
doctype = "Journal Entry Account"
parentfield = "accounts"
cost_center = "_Test Cost Center - _TC"
[["Journal Entry"]]
cheque_date = "2013-02-14"
cheque_no = "33"
company = "_Test Company"
naming_series = "_T-Journal Entry-"
posting_date = "2013-02-14"
user_remark = "test"
voucher_type = "Bank Entry"
[["Journal Entry".accounts]]
account = "Debtors - _TC"
party_type = "Customer"
party = "_Test Customer"
credit_in_account_currency = 0.0
debit_in_account_currency = 400.0
doctype = "Journal Entry Account"
parentfield = "accounts"
cost_center = "_Test Cost Center - _TC"
[["Journal Entry".accounts]]
account = "Sales - _TC"
credit_in_account_currency = 400.0
debit_in_account_currency = 0.0
doctype = "Journal Entry Account"
parentfield = "accounts"
cost_center = "_Test Cost Center - _TC"

View File

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

View File

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

View File

@@ -2,8 +2,17 @@
# See license.txt # See license.txt
# import frappe # 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 pass

View File

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

View File

@@ -1,18 +1,19 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import today from frappe.utils import today
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records 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 from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestLoyaltyPointEntry(unittest.TestCase): class TestLoyaltyPointEntry(IntegrationTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass()
# Create test records # Create test records
create_records() create_records()
cls.loyalty_program_name = "Test Single Loyalty" cls.loyalty_program_name = "Test Single Loyalty"

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import cint, flt, getdate, today from frappe.utils import cint, flt, getdate, today
from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
@@ -13,9 +13,10 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
from erpnext.accounts.party import get_dashboard_info from erpnext.accounts.party import get_dashboard_info
class TestLoyaltyProgram(unittest.TestCase): class TestLoyaltyProgram(IntegrationTestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(cls):
super().setUpClass()
# create relevant item, customer, loyalty program, etc # create relevant item, customer, loyalty program, etc
create_records() create_records()

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
[{
"doctype": "Monthly Distribution",
"distribution_id": "_Test Distribution",
"fiscal_year": "_Test Fiscal Year 2013",
"percentages": [
{
"month": "January",
"percentage_allocation": "8"
}, {
"month": "February",
"percentage_allocation": "8"
}, {
"month": "March",
"percentage_allocation": "8"
}, {
"month": "April",
"percentage_allocation": "8"
}, {
"month": "May",
"percentage_allocation": "8"
}, {
"month": "June",
"percentage_allocation": "8"
}, {
"month": "July",
"percentage_allocation": "8"
}, {
"month": "August",
"percentage_allocation": "8"
}, {
"month": "September",
"percentage_allocation": "8"
}, {
"month": "October",
"percentage_allocation": "8"
}, {
"month": "November",
"percentage_allocation": "10"
}, {
"month": "December",
"percentage_allocation": "10"
}
]
}]

View File

@@ -0,0 +1,52 @@
[["Monthly Distribution"]]
distribution_id = "_Test Distribution"
fiscal_year = "_Test Fiscal Year 2013"
[["Monthly Distribution".percentages]]
month = "January"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "February"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "March"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "April"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "May"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "June"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "July"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "August"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "September"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "October"
percentage_allocation = "8"
[["Monthly Distribution".percentages]]
month = "November"
percentage_allocation = "10"
[["Monthly Distribution".percentages]]
month = "December"
percentage_allocation = "10"

View File

@@ -2,7 +2,7 @@
# See license.txt # See license.txt
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests import IntegrationTestCase, UnitTestCase
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension, create_dimension,
@@ -12,12 +12,21 @@ from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_crea
get_temporary_opening_account, get_temporary_opening_account,
) )
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] EXTRA_TEST_RECORD_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 @classmethod
def setUpClass(self): def setUpClass(cls):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"): if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company() make_company()
create_dimension() create_dimension()

View File

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

View File

@@ -174,6 +174,17 @@ frappe.ui.form.on("Payment Entry", {
}; };
}); });
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
return {
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
filters: {
reference_doctype: row.reference_doctype,
reference_name: row.reference_name,
},
};
});
frm.set_query("sales_taxes_and_charges_template", function () { frm.set_query("sales_taxes_and_charges_template", function () {
return { return {
filters: { 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) { refresh: function (frm) {
erpnext.hide_company(frm); erpnext.hide_company(frm);
frm.events.hide_unhide_fields(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); erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
frappe.flags.allocate_payment_amount = true;
}, },
validate_company: (frm) => { validate_company: (frm) => {
@@ -797,7 +817,7 @@ frappe.ui.form.on("Payment Entry", {
); );
if (frm.doc.payment_type == "Pay") 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); else frm.events.set_unallocated_amount(frm);
frm.set_paid_amount_based_on_received_amount = false; frm.set_paid_amount_based_on_received_amount = false;
@@ -818,7 +838,7 @@ frappe.ui.form.on("Payment Entry", {
} }
if (frm.doc.payment_type == "Receive") 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); else frm.events.set_unallocated_amount(frm);
}, },
@@ -989,6 +1009,7 @@ frappe.ui.form.on("Payment Entry", {
c.outstanding_amount = d.outstanding_amount; c.outstanding_amount = d.outstanding_amount;
c.bill_no = d.bill_no; c.bill_no = d.bill_no;
c.payment_term = d.payment_term; c.payment_term = d.payment_term;
c.payment_term_outstanding = d.payment_term_outstanding;
c.allocated_amount = d.allocated_amount; c.allocated_amount = d.allocated_amount;
c.account = d.account; c.account = d.account;
@@ -1038,7 +1059,8 @@ frappe.ui.form.on("Payment Entry", {
frm.events.allocate_party_amount_against_ref_docs( frm.events.allocate_party_amount_against_ref_docs(
frm, 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
); );
}, },
}); });
@@ -1052,93 +1074,13 @@ frappe.ui.form.on("Payment Entry", {
return ["Sales Invoice", "Purchase Invoice"]; return ["Sales Invoice", "Purchase Invoice"];
}, },
allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) { allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0; await frm.call("allocate_amount_to_references", {
var total_negative_outstanding = 0; paid_amount: paid_amount,
var total_deductions = frappe.utils.sum( paid_amount_change: paid_amount_change,
$.map(frm.doc.deductions || [], function (d) { allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
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));
}); });
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); frm.events.set_total_allocated_amount(frm);
}, },
@@ -1686,6 +1628,62 @@ frappe.ui.form.on("Payment Entry", {
return current_tax_amount; 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", { frappe.ui.form.on("Payment Entry Reference", {
@@ -1778,35 +1776,3 @@ frappe.ui.form.on("Payment Entry Deduction", {
frm.events.set_unallocated_amount(frm); frm.events.set_unallocated_amount(frm);
}, },
}); });
frappe.ui.form.on("Payment Entry", {
cost_center: function (frm) {
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
args: {
company: frm.doc.company,
date: frm.doc.posting_date,
paid_from: frm.doc.paid_from,
paid_to: frm.doc.paid_to,
ptype: frm.doc.party_type,
pty: frm.doc.party,
cost_center: frm.doc.cost_center,
},
callback: function (r, rt) {
if (r.message) {
frappe.run_serially([
() => {
frm.set_value(
"paid_from_account_balance",
r.message.paid_from_account_balance
);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
},
]);
}
},
});
}
},
});

View File

@@ -7,8 +7,10 @@ from functools import reduce
import frappe import frappe
from frappe import ValidationError, _, qb, scrub, throw 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 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 import Case
from pypika.functions import Coalesce, Sum from pypika.functions import Coalesce, Sum
@@ -180,14 +182,18 @@ class PaymentEntry(AccountsController):
self.set_status() self.set_status()
self.set_total_in_words() self.set_total_in_words()
def before_save(self):
self.set_matched_unset_payment_requests_to_response()
def on_submit(self): def on_submit(self):
if self.difference_amount: if self.difference_amount:
frappe.throw(_("Difference Amount must be zero")) frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries() self.make_gl_entries()
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid()
self.update_payment_schedule() self.update_payment_schedule()
self.set_payment_req_status() self.update_payment_requests()
self.make_advance_payment_ledger_entries()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status() self.set_status()
def set_liability_account(self): def set_liability_account(self):
@@ -228,9 +234,21 @@ class PaymentEntry(AccountsController):
self.is_opening = "No" self.is_opening = "No"
return return
liability_account = get_party_account( accounts = get_party_account(self.party_type, self.party, self.company, include_advance=True)
self.party_type, self.party, self.company, include_advance=True
)[1] 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) self.set(self.party_account_field, liability_account)
@@ -255,34 +273,40 @@ class PaymentEntry(AccountsController):
"Repost Accounting Ledger Items", "Repost Accounting Ledger Items",
"Unreconcile Payment", "Unreconcile Payment",
"Unreconcile Payment Entries", "Unreconcile Payment Entries",
"Advance Payment Ledger Entry",
) )
super().on_cancel() super().on_cancel()
self.make_gl_entries(cancel=1) self.make_gl_entries(cancel=1)
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid()
self.delink_advance_entry_references() self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1) self.update_payment_schedule(cancel=1)
self.set_payment_req_status() self.update_payment_requests(cancel=True)
self.make_advance_payment_ledger_entries()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status() self.set_status()
def set_payment_req_status(self): def update_payment_requests(self, cancel=False):
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status 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): def update_outstanding_amounts(self):
self.set_missing_ref_details(force=True) self.set_missing_ref_details(force=True)
def validate_duplicate_entry(self): def validate_duplicate_entry(self):
reference_names = [] reference_names = set()
for d in self.get("references"): 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( frappe.throw(
_("Row #{0}: Duplicate entry in References {1} {2}").format( _("Row #{0}: Duplicate entry in References {1} {2}").format(
d.idx, d.reference_doctype, d.reference_name 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): def set_bank_account_data(self):
if self.bank_account: if self.bank_account:
@@ -308,6 +332,8 @@ class PaymentEntry(AccountsController):
if self.payment_type == "Internal Transfer": if self.payment_type == "Internal Transfer":
return return
self.validate_allocated_amount_as_per_payment_request()
if self.party_type in ("Customer", "Supplier"): if self.party_type in ("Customer", "Supplier"):
self.validate_allocated_amount_with_latest_data() self.validate_allocated_amount_with_latest_data()
else: else:
@@ -320,6 +346,27 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(fail_message.format(d.idx)) 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( def term_based_allocation_enabled_for_reference(
self, reference_doctype: str, reference_name: str self, reference_doctype: str, reference_name: str
) -> bool: ) -> bool:
@@ -524,7 +571,10 @@ class PaymentEntry(AccountsController):
continue continue
if field == "exchange_rate" or not d.get(field) or force: 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): def validate_payment_type(self):
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"): if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
@@ -1216,6 +1266,10 @@ class PaymentEntry(AccountsController):
if not self.party_account: if not self.party_account:
return return
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if self.payment_type == "Receive": if self.payment_type == "Receive":
against_account = self.paid_to against_account = self.paid_to
else: else:
@@ -1261,11 +1315,30 @@ class PaymentEntry(AccountsController):
{ {
dr_or_cr: allocated_amount_in_company_currency, dr_or_cr: allocated_amount_in_company_currency,
dr_or_cr + "_in_account_currency": d.allocated_amount, dr_or_cr + "_in_account_currency": d.allocated_amount,
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
"cost_center": cost_center, "cost_center": cost_center,
} }
) )
if self.book_advance_payments_in_separate_party_account:
if d.reference_doctype in advance_payment_doctypes:
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
gle.update(
{
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
}
)
else:
# Do not reference Invoices while Advance is in separate party account
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
else:
gle.update(
{
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
}
)
gl_entries.append(gle) gl_entries.append(gle)
if self.unallocated_amount: if self.unallocated_amount:
@@ -1692,6 +1765,380 @@ class PaymentEntry(AccountsController):
return current_tax_fraction 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 validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range): def _on_previous_row_error(row_range):
@@ -2144,7 +2591,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
account_balance = get_balance_on(party_account, date, cost_center=cost_center) account_balance = get_balance_on(party_account, date, cost_center=cost_center)
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name" _party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
party_name = frappe.db.get_value(party_type, party, _party_name) party_name = frappe.db.get_value(party_type, party, _party_name)
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center) party_balance = get_balance_on(
party_type=party_type, party=party, company=company, cost_center=cost_center
)
if party_type in ["Customer", "Supplier"]: if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party) party_bank_account = get_party_bank_account(party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party) bank_account = get_default_company_bank_account(company, party_type, party)
@@ -2323,6 +2772,7 @@ def get_payment_entry(
payment_type=None, payment_type=None,
reference_date=None, reference_date=None,
ignore_permissions=False, ignore_permissions=False,
created_from_payment_request=False,
): ):
doc = frappe.get_doc(dt, dn) doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
@@ -2472,9 +2922,179 @@ def get_payment_entry(
pe.set_difference_amount() 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 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): def update_accounting_dimensions(pe, doc):
""" """
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import qb 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 frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.account.test_account import create_account
@@ -25,10 +25,19 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.doctype.employee.test_employee import make_employee from erpnext.setup.doctype.employee.test_employee import make_employee
test_dependencies = ["Item"] EXTRA_TEST_RECORD_DEPENDENCIES = ["Item", "Currency Exchange"]
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): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
@@ -383,7 +392,7 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].outstanding, 0) self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 50) self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
@change_settings( @IntegrationTestCase.change_settings(
"Accounts Settings", "Accounts Settings",
{ {
"allow_multi_currency_invoices_against_single_party_account": 1, "allow_multi_currency_invoices_against_single_party_account": 1,
@@ -610,12 +619,9 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(flt(pe.references[0].exchange_gain_loss, 2), -94.74) self.assertEqual(flt(pe.references[0].exchange_gain_loss, 2), -94.74)
def test_payment_entry_retrieves_last_exchange_rate(self): def test_payment_entry_retrieves_last_exchange_rate(self):
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import ( from erpnext.setup.doctype.currency_exchange.test_currency_exchange import save_new_records
save_new_records,
test_records,
)
save_new_records(test_records) save_new_records(self.globalTestRecords["Currency Exchange"])
pe = frappe.new_doc("Payment Entry") pe = frappe.new_doc("Payment Entry")
pe.payment_type = "Pay" pe.payment_type = "Pay"
@@ -1090,7 +1096,7 @@ class TestPaymentEntry(FrappeTestCase):
} }
self.assertDictEqual(ref_details, expected_response) self.assertDictEqual(ref_details, expected_response)
@change_settings( @IntegrationTestCase.change_settings(
"Accounts Settings", "Accounts Settings",
{ {
"unlink_payment_on_cancellation_of_invoice": 1, "unlink_payment_on_cancellation_of_invoice": 1,
@@ -1185,7 +1191,7 @@ class TestPaymentEntry(FrappeTestCase):
si3.cancel() si3.cancel()
si3.delete() si3.delete()
@change_settings( @IntegrationTestCase.change_settings(
"Accounts Settings", "Accounts Settings",
{ {
"unlink_payment_on_cancellation_of_invoice": 1, "unlink_payment_on_cancellation_of_invoice": 1,
@@ -1791,7 +1797,7 @@ class TestPaymentEntry(FrappeTestCase):
# 'Is Opening' should always be 'No' for normal advance payments # 'Is Opening' should always be 'No' for normal advance payments
self.assertEqual(gl_with_opening_set, []) self.assertEqual(gl_with_opening_set, [])
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1}) @IntegrationTestCase.change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
def test_delete_linked_exchange_gain_loss_journal(self): def test_delete_linked_exchange_gain_loss_journal(self):
from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (

View File

@@ -10,6 +10,7 @@
"due_date", "due_date",
"bill_no", "bill_no",
"payment_term", "payment_term",
"payment_term_outstanding",
"account_type", "account_type",
"payment_type", "payment_type",
"column_break_4", "column_break_4",
@@ -18,7 +19,9 @@
"allocated_amount", "allocated_amount",
"exchange_rate", "exchange_rate",
"exchange_gain_loss", "exchange_gain_loss",
"account" "account",
"payment_request",
"payment_request_outstanding"
], ],
"fields": [ "fields": [
{ {
@@ -120,12 +123,33 @@
"fieldname": "payment_type", "fieldname": "payment_type",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Payment Type" "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, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-04-05 09:44:08.310593", "modified": "2024-09-16 18:11:50.019343",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry Reference", "name": "Payment Entry Reference",

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry 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 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): def setUp(self):
self.ple = qb.DocType("Payment Ledger Entry") self.ple = qb.DocType("Payment Ledger Entry")
self.create_company() 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[0], expected_values[0])
self.assertEqual(pl_entries_for_crnote[1], expected_values[1]) self.assertEqual(pl_entries_for_crnote[1], expected_values[1])
@change_settings( @IntegrationTestCase.change_settings(
"Accounts Settings", "Accounts Settings",
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1}, {"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
) )
@@ -474,7 +483,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
si.delete() si.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name)
@change_settings( @IntegrationTestCase.change_settings(
"Accounts Settings", "Accounts Settings",
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1}, {"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
) )
@@ -507,7 +516,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
si.delete() si.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name)
@change_settings( @IntegrationTestCase.change_settings(
"Accounts Settings", "Accounts Settings",
{ {
"unlink_payment_on_cancellation_of_invoice": 1, "unlink_payment_on_cancellation_of_invoice": 1,

View File

@@ -3,7 +3,7 @@
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import ( 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 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): def setUp(self):
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error # 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) uniq_identifier = frappe.generate_hash(length=10)

View File

@@ -323,6 +323,7 @@ class PaymentReconciliation(Document):
"posting_date": inv.posting_date, "posting_date": inv.posting_date,
"currency": inv.currency, "currency": inv.currency,
"cost_center": inv.cost_center, "cost_center": inv.cost_center,
"remarks": inv.remarks,
} }
) )
) )

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
from erpnext import get_default_cost_center from erpnext import get_default_cost_center
@@ -17,10 +17,19 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
test_dependencies = ["Item"] EXTRA_TEST_RECORD_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): def setUp(self):
self.create_company() self.create_company()
self.create_item() self.create_item()
@@ -1104,7 +1113,7 @@ class TestPaymentReconciliation(FrappeTestCase):
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name]) self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
@change_settings( @IntegrationTestCase.change_settings(
"Accounts Settings", "Accounts Settings",
{ {
"allow_multi_currency_invoices_against_single_party_account": 1, "allow_multi_currency_invoices_against_single_party_account": 1,
@@ -1986,13 +1995,15 @@ def make_period_closing_voucher(company, cost_center, posting_date=None, submit=
parent_account=parent_account, parent_account=parent_account,
doctype="Account", doctype="Account",
) )
fy = get_fiscal_year(posting_date, company=company)
pcv = frappe.get_doc( pcv = frappe.get_doc(
{ {
"doctype": "Period Closing Voucher", "doctype": "Period Closing Voucher",
"transaction_date": posting_date or today(), "transaction_date": posting_date or today(),
"posting_date": posting_date or today(), "period_start_date": fy[1],
"period_end_date": fy[2],
"company": company, "company": company,
"fiscal_year": get_fiscal_year(posting_date or today(), company=company)[0], "fiscal_year": fy[0],
"cost_center": cost_center, "cost_center": cost_center,
"closing_account_head": surplus_account, "closing_account_head": surplus_account,
"remarks": "test", "remarks": "test",

View File

@@ -14,7 +14,7 @@
"amount", "amount",
"difference_amount", "difference_amount",
"sec_break1", "sec_break1",
"remark", "remarks",
"currency", "currency",
"exchange_rate", "exchange_rate",
"cost_center" "cost_center"
@@ -74,12 +74,6 @@
"fieldname": "sec_break1", "fieldname": "sec_break1",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{
"fieldname": "remark",
"fieldtype": "Small Text",
"label": "Remark",
"read_only": 1
},
{ {
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Link", "fieldtype": "Link",
@@ -105,12 +99,18 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center" "options": "Cost Center"
},
{
"fieldname": "remarks",
"fieldtype": "Small Text",
"label": "Remarks",
"read_only": 1
} }
], ],
"is_virtual": 1, "is_virtual": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:10:10.980445", "modified": "2024-10-29 16:24:43.021230",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Payment", "name": "Payment Reconciliation Payment",

View File

@@ -27,7 +27,7 @@ class PaymentReconciliationPayment(Document):
reference_name: DF.DynamicLink | None reference_name: DF.DynamicLink | None
reference_row: DF.Data | None reference_row: DF.Data | None
reference_type: DF.Link | None reference_type: DF.Link | None
remark: DF.SmallText | None remarks: DF.SmallText | None
# end: auto-generated types # end: auto-generated types
@staticmethod @staticmethod

View File

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

View File

@@ -15,14 +15,17 @@
"party_details", "party_details",
"party_type", "party_type",
"party", "party",
"party_name",
"column_break_4", "column_break_4",
"reference_doctype", "reference_doctype",
"reference_name", "reference_name",
"transaction_details", "transaction_details",
"grand_total", "grand_total",
"currency",
"is_a_subscription", "is_a_subscription",
"column_break_18", "column_break_18",
"currency", "outstanding_amount",
"party_account_currency",
"subscription_section", "subscription_section",
"subscription_plans", "subscription_plans",
"bank_account_details", "bank_account_details",
@@ -71,6 +74,7 @@
{ {
"fieldname": "transaction_date", "fieldname": "transaction_date",
"fieldtype": "Date", "fieldtype": "Date",
"in_preview": 1,
"label": "Transaction Date" "label": "Transaction Date"
}, },
{ {
@@ -135,7 +139,8 @@
"no_copy": 1, "no_copy": 1,
"options": "reference_doctype", "options": "reference_doctype",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "transaction_details", "fieldname": "transaction_details",
@@ -143,12 +148,14 @@
"label": "Transaction Details" "label": "Transaction Details"
}, },
{ {
"description": "Amount in customer's currency", "description": "Amount in transaction currency",
"fieldname": "grand_total", "fieldname": "grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_preview": 1,
"label": "Amount", "label": "Amount",
"non_negative": 1, "non_negative": 1,
"options": "currency" "options": "currency",
"reqd": 1
}, },
{ {
"default": "0", "default": "0",
@@ -403,6 +410,17 @@
"print_hide": 1, "print_hide": 1,
"read_only": 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", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
@@ -413,13 +431,26 @@
{ {
"fieldname": "column_break_pnyv", "fieldname": "column_break_pnyv",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "party_account_currency",
"fieldtype": "Link",
"label": "Party Account Currency",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "party_name",
"fieldtype": "Data",
"label": "Party Name",
"read_only": 1
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-08-07 16:39:54.288002", "modified": "2024-10-23 12:23:40.117336",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Request", "name": "Payment Request",
@@ -454,6 +485,7 @@
"write": 1 "write": 1
} }
], ],
"show_preview_popup": 1,
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []

View File

@@ -7,6 +7,7 @@ from frappe.query_builder.functions import Sum
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
) )
@@ -19,6 +20,15 @@ from erpnext.accounts.party import get_party_account, get_party_bank_account
from erpnext.accounts.utils import get_account_currency, get_currency_precision from erpnext.accounts.utils import get_account_currency, get_currency_precision
from erpnext.utilities import payment_app_import_guard from erpnext.utilities import payment_app_import_guard
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [
"Sales Order",
"Purchase Order",
"Sales Invoice",
"Purchase Invoice",
"POS Invoice",
"Fees",
]
def _get_payment_gateway_controller(*args, **kwargs): def _get_payment_gateway_controller(*args, **kwargs):
with payment_app_import_guard(): with payment_app_import_guard():
@@ -46,9 +56,11 @@ class PaymentRequest(Document):
bank_account: DF.Link | None bank_account: DF.Link | None
bank_account_no: DF.ReadOnly | None bank_account_no: DF.ReadOnly | None
branch_code: DF.ReadOnly | None branch_code: DF.ReadOnly | None
company: DF.Link | None
cost_center: DF.Link | None cost_center: DF.Link | None
currency: DF.Link | None currency: DF.Link | None
email_to: DF.Data | None email_to: DF.Data | None
failed_reason: DF.Data | None
grand_total: DF.Currency grand_total: DF.Currency
iban: DF.ReadOnly | None iban: DF.ReadOnly | None
is_a_subscription: DF.Check is_a_subscription: DF.Check
@@ -57,16 +69,19 @@ class PaymentRequest(Document):
mode_of_payment: DF.Link | None mode_of_payment: DF.Link | None
mute_email: DF.Check mute_email: DF.Check
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"] naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
outstanding_amount: DF.Currency
party: DF.DynamicLink | None party: DF.DynamicLink | None
party_account_currency: DF.Link | None
party_name: DF.Data | None
party_type: DF.Link | None party_type: DF.Link | None
payment_account: DF.ReadOnly | 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: DF.ReadOnly | None
payment_gateway_account: DF.Link | None payment_gateway_account: DF.Link | None
payment_order: DF.Link | None payment_order: DF.Link | None
payment_request_type: DF.Literal["Outward", "Inward"] payment_request_type: DF.Literal["Outward", "Inward"]
payment_url: DF.Data | None payment_url: DF.Data | None
print_format: DF.Literal print_format: DF.Literal[None]
project: DF.Link | None project: DF.Link | None
reference_doctype: DF.Link | None reference_doctype: DF.Link | None
reference_name: DF.DynamicLink | None reference_name: DF.DynamicLink | None
@@ -85,7 +100,6 @@ class PaymentRequest(Document):
subscription_plans: DF.Table[SubscriptionPlanDetail] subscription_plans: DF.Table[SubscriptionPlanDetail]
swift_number: DF.ReadOnly | None swift_number: DF.ReadOnly | None
transaction_date: DF.Date | None transaction_date: DF.Date | None
company: DF.Link | None
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
@@ -101,6 +115,12 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required")) frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self): 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( existing_payment_request_amount = flt(
get_existing_payment_request_amount(self.reference_doctype, self.reference_name) get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
) )
@@ -150,16 +170,28 @@ class PaymentRequest(Document):
).format(self.grand_total, amount) ).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): 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": if self.payment_request_type == "Outward":
self.status = "Initiated" self.status = "Initiated"
elif self.payment_request_type == "Inward": elif self.payment_request_type == "Inward":
@@ -174,6 +206,9 @@ class PaymentRequest(Document):
self.send_email() self.send_email()
self.make_communication_entry() self.make_communication_entry()
def on_submit(self):
self.update_reference_advance_payment_status()
def request_phone_payment(self): def request_phone_payment(self):
controller = _get_payment_gateway_controller(self.payment_gateway) controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount() request_amount = self.get_request_amount()
@@ -211,6 +246,7 @@ class PaymentRequest(Document):
def on_cancel(self): def on_cancel(self):
self.check_if_payment_entry_exists() self.check_if_payment_entry_exists()
self.set_as_cancelled() self.set_as_cancelled()
self.update_reference_advance_payment_status()
def make_invoice(self): def make_invoice(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
@@ -254,12 +290,12 @@ class PaymentRequest(Document):
return controller.get_payment_url( return controller.get_payment_url(
**{ **{
"amount": flt(self.grand_total, self.precision("grand_total")), "amount": flt(self.grand_total, self.precision("grand_total")),
"title": data.company.encode("utf-8"), "title": data.company,
"description": self.subject.encode("utf-8"), "description": self.subject,
"reference_doctype": "Payment Request", "reference_doctype": "Payment Request",
"reference_docname": self.name, "reference_docname": self.name,
"payer_email": self.email_to or frappe.session.user, "payer_email": self.email_to or frappe.session.user,
"payer_name": frappe.safe_encode(data.customer_name), "payer_name": data.customer_name,
"order_id": self.name, "order_id": self.name,
"currency": self.currency, "currency": self.currency,
} }
@@ -267,7 +303,7 @@ class PaymentRequest(Document):
def set_as_paid(self): def set_as_paid(self):
if self.payment_channel == "Phone": if self.payment_channel == "Phone":
self.db_set("status", "Paid") self.db_set({"status": "Paid", "outstanding_amount": 0})
else: else:
payment_entry = self.create_payment_entry() payment_entry = self.create_payment_entry()
@@ -289,26 +325,32 @@ class PaymentRequest(Document):
else: else:
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company) 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: 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") exchange_rate = ref_doc.get("conversion_rate")
else: bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
party_amount = self.grand_total
# outstanding amount is already in Part's account currency
payment_entry = get_payment_entry( payment_entry = get_payment_entry(
self.reference_doctype, self.reference_doctype,
self.reference_name, self.reference_name,
party_amount=party_amount, party_amount=party_amount,
bank_account=self.payment_account, bank_account=self.payment_account,
bank_amount=bank_amount, bank_amount=bank_amount,
created_from_payment_request=True,
) )
payment_entry.update( payment_entry.update(
{ {
"mode_of_payment": self.mode_of_payment, "mode_of_payment": self.mode_of_payment,
"reference_no": self.name, "reference_no": self.name, # to prevent validation error
"reference_date": nowdate(), "reference_date": nowdate(),
"remarks": "Payment Entry against {} {} via Payment Request {}".format( "remarks": "Payment Entry against {} {} via Payment Request {}".format(
self.reference_doctype, self.reference_name, self.name self.reference_doctype, self.reference_name, self.name
@@ -316,6 +358,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 # Update dimensions
payment_entry.update( payment_entry.update(
{ {
@@ -324,14 +369,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 # Update 'Paid Amount' on Forex transactions
if self.currency != ref_doc.company_currency: if self.currency != ref_doc.company_currency:
if ( if (
@@ -426,25 +463,80 @@ class PaymentRequest(Document):
return create_stripe_subscription(gateway_controller, data) 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) @frappe.whitelist(allow_guest=True)
def make_payment_request(**args): def make_payment_request(**args):
"""Make payment request""" """Make payment request"""
args = frappe._dict(args) args = frappe._dict(args)
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn) if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
if ref_doc.doctype not in [ ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
"Sales Order",
"Purchase Order",
"Sales Invoice",
"Purchase Invoice",
"POS Invoice",
"Fees",
]:
frappe.throw(
_("Payment Requests cannot be created against: {0}").format(frappe.bold(ref_doc.doctype))
)
gateway_account = get_gateway_details(args) or frappe._dict() gateway_account = get_gateway_details(args) or frappe._dict()
@@ -468,11 +560,15 @@ def make_payment_request(**args):
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0}, {"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
) )
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name) existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
if existing_payment_request_amount: if existing_payment_request_amount:
grand_total -= 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: if draft_payment_request:
frappe.db.set_value( frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
@@ -486,6 +582,13 @@ def make_payment_request(**args):
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward" "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.get("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( pr.update(
{ {
"payment_gateway_account": gateway_account.get("name"), "payment_gateway_account": gateway_account.get("name"),
@@ -494,6 +597,7 @@ def make_payment_request(**args):
"payment_channel": gateway_account.get("payment_channel"), "payment_channel": gateway_account.get("payment_channel"),
"payment_request_type": args.get("payment_request_type"), "payment_request_type": args.get("payment_request_type"),
"currency": ref_doc.currency, "currency": ref_doc.currency,
"party_account_currency": party_account_currency,
"grand_total": grand_total, "grand_total": grand_total,
"mode_of_payment": args.mode_of_payment, "mode_of_payment": args.mode_of_payment,
"email_to": args.recipient_id or ref_doc.owner, "email_to": args.recipient_id or ref_doc.owner,
@@ -502,9 +606,10 @@ def make_payment_request(**args):
"reference_doctype": ref_doc.doctype, "reference_doctype": ref_doc.doctype,
"reference_name": ref_doc.name, "reference_name": ref_doc.name,
"company": ref_doc.get("company"), "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"), "party": args.get("party") or ref_doc.get("customer"),
"bank_account": bank_account, "bank_account": bank_account,
"party_name": args.get("party_name") or ref_doc.get("customer_name"),
"make_sales_invoice": ( "make_sales_invoice": (
args.make_sales_invoice # new standard args.make_sales_invoice # new standard
or args.order_type == "Shopping Cart" # compat for webshop app or args.order_type == "Shopping Cart" # compat for webshop app
@@ -551,13 +656,14 @@ def get_amount(ref_doc, payment_account=None):
dt = ref_doc.doctype dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]: if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) 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"]: elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"): if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency: 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: 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": elif dt == "Sales Invoice":
for pay in ref_doc.payments: for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account: if pay.type == "Phone" and pay.account == payment_account:
@@ -579,24 +685,20 @@ def get_amount(ref_doc, payment_account=None):
def get_existing_payment_request_amount(ref_dt, ref_dn): 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 Return the total amount of Payment Requests against a reference document.
and get the summation of existing paid payment request for Phone payment channel.
""" """
existing_payment_request_amount = frappe.db.sql( PR = frappe.qb.DocType("Payment Request")
"""
select sum(grand_total) response = (
from `tabPayment Request` frappe.qb.from_(PR)
where .select(Sum(PR.grand_total))
reference_doctype = %s .where(PR.reference_doctype == ref_dt)
and reference_name = %s .where(PR.reference_name == ref_dn)
and docstatus = 1 .where(PR.docstatus == 1)
and (status != 'Paid' .run()
or (payment_channel = 'Phone'
and status = 'Paid'))
""",
(ref_dt, ref_dn),
) )
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 def get_gateway_details(args): # nosemgrep
@@ -638,41 +740,66 @@ def make_payment_entry(docname):
return doc.create_payment_entry(submit=False).as_dict() return doc.create_payment_entry(submit=False).as_dict()
def update_payment_req_status(doc, method): def update_payment_requests_as_per_pe_references(references=None, cancel=False):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details """
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: precision = references[0].precision("allocated_amount")
payment_request_name = frappe.db.get_value(
"Payment Request", referenced_payment_requests = frappe.get_all(
{ "Payment Request",
"reference_doctype": ref.reference_doctype, filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
"reference_name": ref.reference_name, fields=[
"docstatus": 1, "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: # to handle same payment request for the multiple allocations
ref_details = get_reference_details( payment_request["outstanding_amount"] = new_outstanding_amount
ref.reference_doctype,
ref.reference_name, if not cancel and new_outstanding_amount < 0:
doc.party_account_currency, frappe.throw(
doc.party_type, msg=_(
doc.party, "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: # update status
status = "Paid" if new_outstanding_amount == payment_request["grand_total"]:
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount: status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
status = "Partially Paid" elif new_outstanding_amount == 0:
elif ref_details.outstanding_amount == ref_details.total_amount: status = "Paid"
if pay_req_doc.payment_request_type == "Outward": elif new_outstanding_amount > 0:
status = "Initiated" status = "Partially Paid"
elif pay_req_doc.payment_request_type == "Inward":
status = "Requested"
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): def get_dummy_message(doc):
@@ -758,28 +885,33 @@ def validate_payment(doc, method=None):
) )
def get_paid_amount_against_order(dt, dn): @frappe.whitelist()
pe_ref = frappe.qb.DocType("Payment Entry Reference") def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
if dt == "Sales Order": # permission checks in `get_list()`
inv_dt, inv_field = "Sales Invoice Item", "sales_order" reference_doctype = filters.get("reference_doctype")
else: reference_name = filters.get("reference_doctype")
inv_dt, inv_field = "Purchase Invoice Item", "purchase_order"
inv_item = frappe.qb.DocType(inv_dt) if not reference_doctype or not reference_name:
return ( return []
frappe.qb.from_(pe_ref)
.select( open_payment_requests = frappe.get_list(
Sum(pe_ref.allocated_amount), "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( for pr in open_payment_requests
(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

View File

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

View File

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

View File

@@ -1,34 +0,0 @@
[
{
"doctype":"Payment Term",
"due_date_based_on":"Day(s) after invoice date",
"payment_term_name":"_Test N30",
"description":"_Test Net 30 Days",
"invoice_portion":50,
"credit_days":30
},
{
"doctype":"Payment Term",
"due_date_based_on":"Day(s) after invoice date",
"payment_term_name":"_Test COD",
"description":"_Test Cash on Delivery",
"invoice_portion":50,
"credit_days":0
},
{
"doctype":"Payment Term",
"due_date_based_on":"Month(s) after the end of the invoice month",
"payment_term_name":"_Test EONM",
"description":"_Test End of Next Month",
"invoice_portion":100,
"credit_months":1
},
{
"doctype":"Payment Term",
"due_date_based_on":"Day(s) after invoice date",
"payment_term_name":"_Test N30 1",
"description":"_Test Net 30 Days",
"invoice_portion":100,
"credit_days":30
}
]

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