mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-19 12:44:03 +00:00
Compare commits
10 Commits
refactor/j
...
feat-purch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87b65d09df | ||
|
|
1631985726 | ||
|
|
5284b8c7a8 | ||
|
|
a674d9216a | ||
|
|
9dfb60f826 | ||
|
|
1138effd7c | ||
|
|
c8cb70dbd2 | ||
|
|
5249274bcd | ||
|
|
56ed9e8e43 | ||
|
|
0147312951 |
51
.github/helper/install.sh
vendored
51
.github/helper/install.sh
vendored
@@ -4,46 +4,24 @@ set -e
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
sudo apt update
|
||||
sudo apt remove mysql-server mysql-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||
frappeuser=${FRAPPE_USER:-"frappe"}
|
||||
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 1 — parallelise the three slow, independent setup steps:
|
||||
# a) system packages b) frappe-bench pip install c) frappe git fetch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
sudo apt update
|
||||
|
||||
# apt remove/install must run sequentially but can overlap with pip and git.
|
||||
sudo apt remove mysql-server mysql-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev &
|
||||
apt_pid=$!
|
||||
|
||||
pip install frappe-bench &
|
||||
pip_pid=$!
|
||||
|
||||
mkdir frappe
|
||||
(
|
||||
cd frappe
|
||||
git init
|
||||
git remote add origin "https://github.com/${frappeuser}/frappe"
|
||||
git fetch origin "${frappecommitish}" --depth 1
|
||||
) &
|
||||
clone_pid=$!
|
||||
|
||||
wait $apt_pid
|
||||
wait $pip_pid
|
||||
wait $clone_pid
|
||||
|
||||
pushd frappe
|
||||
git init
|
||||
git remote add origin "https://github.com/${frappeuser}/frappe"
|
||||
git fetch origin "${frappecommitish}" --depth 1
|
||||
git checkout FETCH_HEAD
|
||||
popd
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2 — bench init and site setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
|
||||
|
||||
mkdir ~/frappe-bench/sites/test_site
|
||||
@@ -59,11 +37,6 @@ if [ "$DB" == "mariadb" ];then
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
|
||||
# Belt-and-suspenders: also set performance variables at runtime in case
|
||||
# MARIADB_EXTRA_FLAGS was not honoured by the container image.
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -proot \
|
||||
-e "SET GLOBAL innodb_flush_log_at_trx_commit=0; SET GLOBAL sync_binlog=0;"
|
||||
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
||||
@@ -78,11 +51,9 @@ fi
|
||||
|
||||
|
||||
install_whktml() {
|
||||
# Re-use the .deb if the wkhtmltopdf cache step already restored it.
|
||||
if [ ! -f /tmp/wkhtmltox.deb ]; then
|
||||
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
|
||||
fi
|
||||
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
|
||||
sudo apt install /tmp/wkhtmltox.deb
|
||||
|
||||
}
|
||||
install_whktml &
|
||||
wkpid=$!
|
||||
|
||||
21
.github/workflows/server-tests-mariadb.yml
vendored
21
.github/workflows/server-tests-mariadb.yml
vendored
@@ -59,10 +59,6 @@ jobs:
|
||||
env:
|
||||
TZ: 'Asia/Kolkata'
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
# Disable durability guarantees that are unnecessary in a throwaway CI container.
|
||||
# innodb_flush_log_at_trx_commit=0 avoids an fsync on every commit (biggest win).
|
||||
# sync_binlog=0 skips binary-log syncs; innodb_doublewrite=0 skips the doublewrite buffer.
|
||||
MARIADB_EXTRA_FLAGS: --innodb-flush-log-at-trx-commit=0 --sync-binlog=0 --innodb-doublewrite=0
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
@@ -126,12 +122,6 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Cache wkhtmltopdf
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/wkhtmltox.deb
|
||||
key: wkhtmltox-0.12.6.1-2-jammy-amd64
|
||||
|
||||
- name: Install
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
@@ -141,14 +131,7 @@ jobs:
|
||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
coverage_flag=""
|
||||
if [ "$WITH_COVERAGE" = "true" ]; then coverage_flag="--with-coverage"; fi
|
||||
bench --site test_site run-parallel-tests --lightmode --app erpnext \
|
||||
--total-builds ${{ strategy.job-total }} \
|
||||
--build-number ${{ matrix.container }} \
|
||||
$coverage_flag
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --lightmode --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
|
||||
env:
|
||||
TYPE: server
|
||||
|
||||
@@ -158,7 +141,6 @@ jobs:
|
||||
run: cat ~/frappe-bench/bench_start.log || true
|
||||
|
||||
- name: Upload coverage data
|
||||
if: ${{ env.WITH_COVERAGE == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
@@ -167,7 +149,6 @@ jobs:
|
||||
coverage:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"disabledLabels": [
|
||||
"conflicts"
|
||||
],
|
||||
"context": {
|
||||
"repos": [
|
||||
"frappe/frappe"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vite": "^8.0.16"
|
||||
"vite": "^8.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
@@ -250,7 +250,7 @@ const ClosingBalanceForm = ({ defaultBalance, date, bankAccount, onClose }: { de
|
||||
{_("Enter the closing balance you see in your bank statement for {0} as of the {1}", [bankAccount?.account_name ?? bankAccount?.name ?? '', formatDate(date, 'Do MMM YYYY')])}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{error && <div className="py-2"><ErrorBanner error={error} /></div>}
|
||||
{error && <ErrorBanner error={error} />}
|
||||
<div className="py-4">
|
||||
<CurrencyFormField
|
||||
name="balance"
|
||||
|
||||
@@ -33,16 +33,6 @@ export const getErrorMessages = (error?: FrappeError | null): ParsedErrorMessage
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-expect-error - some errors have _error_message
|
||||
if (error?._error_message) {
|
||||
eMessages.push({
|
||||
// @ts-expect-error - some errors have _error_message
|
||||
message: error?._error_message,
|
||||
title: "Error",
|
||||
indicator: "red"
|
||||
})
|
||||
}
|
||||
|
||||
if (eMessages.length === 0) {
|
||||
// Get the message from the exception by removing the exc_type
|
||||
const indexOfFirstColon = error?.exception?.indexOf(':')
|
||||
|
||||
@@ -358,10 +358,10 @@
|
||||
dependencies:
|
||||
"@tybys/wasm-util" "^0.10.1"
|
||||
|
||||
"@oxc-project/types@=0.133.0":
|
||||
version "0.133.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8"
|
||||
integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==
|
||||
"@oxc-project/types@=0.128.0":
|
||||
version "0.128.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.128.0.tgz#efc7524f948ff9e8ab1404ecad1823849c6fe149"
|
||||
integrity sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==
|
||||
|
||||
"@radix-ui/number@1.1.1":
|
||||
version "1.1.1"
|
||||
@@ -1042,95 +1042,95 @@
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb"
|
||||
integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==
|
||||
|
||||
"@rolldown/binding-android-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592"
|
||||
integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==
|
||||
"@rolldown/binding-android-arm64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz#3af8b2242086125934a85c1915b76e0a6a2054c1"
|
||||
integrity sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==
|
||||
|
||||
"@rolldown/binding-darwin-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc"
|
||||
integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==
|
||||
"@rolldown/binding-darwin-arm64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz#ae0b4467d24ecd6c6589f03d4d4699616ee9649c"
|
||||
integrity sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==
|
||||
|
||||
"@rolldown/binding-darwin-x64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad"
|
||||
integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==
|
||||
"@rolldown/binding-darwin-x64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz#23cf24b0a7b96c8990bbdd8a91e7fd3ba82b00e7"
|
||||
integrity sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==
|
||||
|
||||
"@rolldown/binding-freebsd-x64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1"
|
||||
integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==
|
||||
"@rolldown/binding-freebsd-x64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz#a047a770f94dc451c062b729e5d1cf82e5c6f9c4"
|
||||
integrity sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f"
|
||||
integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==
|
||||
"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz#c0b7f346cbf50301cea669a4632bc63aabe6a72c"
|
||||
integrity sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1"
|
||||
integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==
|
||||
"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz#af56373c7996ebe6379207cd699c9f7f705e235d"
|
||||
integrity sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069"
|
||||
integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==
|
||||
"@rolldown/binding-linux-arm64-musl@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz#a8f5acd21fcffc8991aa84710e3ae603c4240ea4"
|
||||
integrity sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8"
|
||||
integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==
|
||||
"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz#1d4a89e040ff82141fc46e717cfab80b05f7c13f"
|
||||
integrity sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763"
|
||||
integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==
|
||||
"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz#97c21feeb2ed87d07820f0b2dcc5dd663e7a7f3b"
|
||||
integrity sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7"
|
||||
integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==
|
||||
"@rolldown/binding-linux-x64-gnu@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz#06310d40fe139ccc3c433b361120d337c66ebec2"
|
||||
integrity sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==
|
||||
|
||||
"@rolldown/binding-linux-x64-musl@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3"
|
||||
integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==
|
||||
"@rolldown/binding-linux-x64-musl@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz#6a711258841f42609b238050cfcd5db13ac136d0"
|
||||
integrity sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==
|
||||
|
||||
"@rolldown/binding-openharmony-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309"
|
||||
integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==
|
||||
"@rolldown/binding-openharmony-arm64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz#15cb644beeafdbec930d79ed45c2a7c2573eac70"
|
||||
integrity sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==
|
||||
|
||||
"@rolldown/binding-wasm32-wasi@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b"
|
||||
integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==
|
||||
"@rolldown/binding-wasm32-wasi@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz#ca3a56d11dfd533d743711141b3bb4c1ec10110e"
|
||||
integrity sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==
|
||||
dependencies:
|
||||
"@emnapi/core" "1.10.0"
|
||||
"@emnapi/runtime" "1.10.0"
|
||||
"@napi-rs/wasm-runtime" "^1.1.4"
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad"
|
||||
integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==
|
||||
"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz#8c2117d68331d7de59d24631146d538fc203d27c"
|
||||
integrity sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb"
|
||||
integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==
|
||||
"@rolldown/binding-win32-x64-msvc@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz#bb5c28df3095046778cc1b020ef52fc5ee7b7e70"
|
||||
integrity sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==
|
||||
|
||||
"@rolldown/pluginutils@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz#51cf2589596a179ebe8cbf313f1358c7b51a2fdc"
|
||||
integrity sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==
|
||||
|
||||
"@rolldown/pluginutils@1.0.0-rc.7":
|
||||
version "1.0.0-rc.7"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz#0414869467f0e471a6515d4f506c85fde867e022"
|
||||
integrity sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==
|
||||
|
||||
"@rolldown/pluginutils@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be"
|
||||
integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==
|
||||
|
||||
"@socket.io/component-emitter@~3.1.0":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
|
||||
@@ -3031,10 +3031,10 @@ ms@^2.1.3:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
nanoid@^3.3.12:
|
||||
version "3.3.12"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05"
|
||||
integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==
|
||||
nanoid@^3.3.11:
|
||||
version "3.3.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -3119,17 +3119,22 @@ picocolors@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
|
||||
picomatch@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
|
||||
postcss@^8.5.15:
|
||||
version "8.5.15"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c"
|
||||
integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==
|
||||
postcss@^8.5.14:
|
||||
version "8.5.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
|
||||
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
|
||||
dependencies:
|
||||
nanoid "^3.3.12"
|
||||
nanoid "^3.3.11"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
@@ -3389,29 +3394,29 @@ resolve-from@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
rolldown@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac"
|
||||
integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==
|
||||
rolldown@1.0.0-rc.18:
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.18.tgz#c597f89a4ce12e6fc918fa91e4f892b340aa92f0"
|
||||
integrity sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==
|
||||
dependencies:
|
||||
"@oxc-project/types" "=0.133.0"
|
||||
"@rolldown/pluginutils" "^1.0.0"
|
||||
"@oxc-project/types" "=0.128.0"
|
||||
"@rolldown/pluginutils" "1.0.0-rc.18"
|
||||
optionalDependencies:
|
||||
"@rolldown/binding-android-arm64" "1.0.3"
|
||||
"@rolldown/binding-darwin-arm64" "1.0.3"
|
||||
"@rolldown/binding-darwin-x64" "1.0.3"
|
||||
"@rolldown/binding-freebsd-x64" "1.0.3"
|
||||
"@rolldown/binding-linux-arm-gnueabihf" "1.0.3"
|
||||
"@rolldown/binding-linux-arm64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-arm64-musl" "1.0.3"
|
||||
"@rolldown/binding-linux-ppc64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-s390x-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-x64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-x64-musl" "1.0.3"
|
||||
"@rolldown/binding-openharmony-arm64" "1.0.3"
|
||||
"@rolldown/binding-wasm32-wasi" "1.0.3"
|
||||
"@rolldown/binding-win32-arm64-msvc" "1.0.3"
|
||||
"@rolldown/binding-win32-x64-msvc" "1.0.3"
|
||||
"@rolldown/binding-android-arm64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-darwin-arm64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-darwin-x64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-freebsd-x64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-arm64-musl" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-x64-gnu" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-x64-musl" "1.0.0-rc.18"
|
||||
"@rolldown/binding-openharmony-arm64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-wasm32-wasi" "1.0.0-rc.18"
|
||||
"@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.18"
|
||||
"@rolldown/binding-win32-x64-msvc" "1.0.0-rc.18"
|
||||
|
||||
scheduler@^0.27.0:
|
||||
version "0.27.0"
|
||||
@@ -3535,10 +3540,18 @@ tapable@^2.3.3:
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160"
|
||||
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
|
||||
|
||||
tinyglobby@^0.2.15, tinyglobby@^0.2.17:
|
||||
version "0.2.17"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
|
||||
integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==
|
||||
tinyglobby@^0.2.15:
|
||||
version "0.2.15"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
|
||||
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.3"
|
||||
|
||||
tinyglobby@^0.2.16:
|
||||
version "0.2.16"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"
|
||||
integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.4"
|
||||
@@ -3712,16 +3725,16 @@ vfile@^6.0.0:
|
||||
"@types/unist" "^3.0.0"
|
||||
vfile-message "^4.0.0"
|
||||
|
||||
vite@^8.0.16:
|
||||
version "8.0.16"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6"
|
||||
integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==
|
||||
vite@^8.0.11:
|
||||
version "8.0.11"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.11.tgz#d128fe82a0dd24da5127d20560735f1cd7ade0a6"
|
||||
integrity sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==
|
||||
dependencies:
|
||||
lightningcss "^1.32.0"
|
||||
picomatch "^4.0.4"
|
||||
postcss "^8.5.15"
|
||||
rolldown "1.0.3"
|
||||
tinyglobby "^0.2.17"
|
||||
postcss "^8.5.14"
|
||||
rolldown "1.0.0-rc.18"
|
||||
tinyglobby "^0.2.16"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
|
||||
@@ -22,13 +22,11 @@ class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.cash = "Cash - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.creditors_usd = "_Test Payable USD - _TC"
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_usd_payable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-04-11 19:48:13.622253",
|
||||
"doctype": "DocType",
|
||||
@@ -8,8 +7,7 @@
|
||||
"field_order": [
|
||||
"bank_account",
|
||||
"date",
|
||||
"balance",
|
||||
"company"
|
||||
"balance"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -33,20 +31,12 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Balance",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "bank_account.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-16 22:17:48.007982",
|
||||
"modified": "2026-04-11 19:49:45.374695",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account Balance",
|
||||
|
||||
@@ -16,7 +16,6 @@ class BankAccountBalance(Document):
|
||||
|
||||
balance: DF.Currency
|
||||
bank_account: DF.Link
|
||||
company: DF.Link | None
|
||||
date: DF.Date
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -94,7 +94,6 @@ class BankClearance(Document):
|
||||
invalid_document = []
|
||||
invalid_cheque_date = []
|
||||
entries_to_update = []
|
||||
self.check_permission("write")
|
||||
|
||||
def validate_entry(d):
|
||||
is_valid = True
|
||||
|
||||
@@ -8,7 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Max, Sum
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, create_batch, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
@@ -518,7 +518,6 @@ def create_internal_transfer(
|
||||
"""
|
||||
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
bank_transaction.check_permission("write")
|
||||
|
||||
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_cached_value("Account", bank_account, "company")
|
||||
@@ -779,6 +778,7 @@ def create_bulk_payment_entry_and_reconcile(
|
||||
"""
|
||||
Create a payment entry and reconcile it with the bank transaction
|
||||
"""
|
||||
|
||||
output = []
|
||||
|
||||
for bank_transaction_name in bank_transaction_names:
|
||||
@@ -1410,14 +1410,12 @@ def get_je_matching_query(
|
||||
Sum(getattr(jea, amount_field)).as_("paid_amount"),
|
||||
ConstantColumn("Journal Entry").as_("doctype"),
|
||||
je.name,
|
||||
# non-grouped columns are constant per grouped JE name (party_type/currency come from the
|
||||
# single bank-account line) -> Max() keeps the GROUP BY valid on postgres with the same value
|
||||
Max(je.cheque_no).as_("reference_no"),
|
||||
Max(je.cheque_date).as_("reference_date"),
|
||||
Max(je.pay_to_recd_from).as_("party"),
|
||||
Max(jea.party_type).as_("party_type"),
|
||||
Max(je.posting_date).as_("posting_date"),
|
||||
Max(jea.account_currency).as_("currency"),
|
||||
je.cheque_no.as_("reference_no"),
|
||||
je.cheque_date.as_("reference_date"),
|
||||
je.pay_to_recd_from.as_("party"),
|
||||
jea.party_type,
|
||||
je.posting_date,
|
||||
jea.account_currency.as_("currency"),
|
||||
)
|
||||
.where(je.docstatus == 1)
|
||||
.where(je.voucher_type != "Opening Entry")
|
||||
@@ -1425,7 +1423,7 @@ def get_je_matching_query(
|
||||
.where(jea.account == common_filters.bank_account)
|
||||
.where(filter_by_date)
|
||||
.groupby(je.name)
|
||||
.orderby(Max(je.cheque_date) if cint(filter_by_reference_date) else Max(je.posting_date))
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
|
||||
@@ -17,10 +17,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -26,9 +26,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankStatementImportLog(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -5,8 +5,6 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Abs, Max, Sum
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
|
||||
@@ -376,7 +374,6 @@ def unreconcile_transaction(transaction_name: str | int):
|
||||
Else, cancel the individual entries
|
||||
"""
|
||||
transaction = frappe.get_doc("Bank Transaction", transaction_name)
|
||||
transaction.check_permission("write")
|
||||
|
||||
vouchers_to_cancel = []
|
||||
|
||||
@@ -404,7 +401,6 @@ def unreconcile_transaction_entry(bank_transaction_id: str | int, voucher_type:
|
||||
"""
|
||||
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_id)
|
||||
bank_transaction.check_permission("write")
|
||||
|
||||
# Find the voucher in the bank transaction and depending on the action, either remove it or cancel the voucher
|
||||
for entry in bank_transaction.payment_entries:
|
||||
@@ -480,28 +476,30 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
|
||||
|
||||
|
||||
def get_related_bank_gl_entries(docs):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
ac = frappe.qb.DocType("Account")
|
||||
result = (
|
||||
frappe.qb.from_(gle)
|
||||
.left_join(ac)
|
||||
.on(ac.name == gle.account)
|
||||
.select(
|
||||
gle.voucher_type.as_("doctype"),
|
||||
gle.voucher_no.as_("docname"),
|
||||
gle.account.as_("gl_account"),
|
||||
Sum(Abs(gle.credit_in_account_currency - gle.debit_in_account_currency)).as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(ac.account_type == "Bank")
|
||||
& Tuple(gle.voucher_type, gle.voucher_no).isin([Tuple(vt, vn) for vt, vn in docs])
|
||||
& (gle.is_cancelled == 0)
|
||||
)
|
||||
.groupby(gle.voucher_type, gle.voucher_no, gle.account)
|
||||
.run(as_dict=True)
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
gle.voucher_type AS doctype,
|
||||
gle.voucher_no AS docname,
|
||||
gle.account AS gl_account,
|
||||
SUM(ABS(gle.credit_in_account_currency - gle.debit_in_account_currency)) AS amount
|
||||
FROM
|
||||
`tabGL Entry` gle
|
||||
LEFT JOIN
|
||||
`tabAccount` ac ON ac.name = gle.account
|
||||
WHERE
|
||||
ac.account_type = 'Bank'
|
||||
AND (gle.voucher_type, gle.voucher_no) IN %(docs)s
|
||||
AND gle.is_cancelled = 0
|
||||
GROUP BY
|
||||
gle.voucher_type, gle.voucher_no, gle.account
|
||||
""",
|
||||
{"docs": docs},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
entries = {}
|
||||
@@ -523,32 +521,31 @@ def get_total_allocated_amount(docs):
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
# The original window query (ROW_NUMBER/FIRST_VALUE + rownum = 1) just collapses to one
|
||||
# row per (account, payment_document, payment_entry) with the partition's allocation total
|
||||
# and most recent transaction date — i.e. a plain GROUP BY with SUM and MAX.
|
||||
btp = frappe.qb.DocType("Bank Transaction Payments")
|
||||
bt = frappe.qb.DocType("Bank Transaction")
|
||||
ba = frappe.qb.DocType("Bank Account")
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(btp)
|
||||
.left_join(bt)
|
||||
.on(bt.name == btp.parent)
|
||||
.left_join(ba)
|
||||
.on(ba.name == bt.bank_account)
|
||||
.select(
|
||||
Sum(btp.allocated_amount).as_("total"),
|
||||
Max(bt.date).as_("latest_date"),
|
||||
ba.account.as_("gl_account"),
|
||||
btp.payment_document,
|
||||
btp.payment_entry,
|
||||
)
|
||||
.where(
|
||||
Tuple(btp.payment_document, btp.payment_entry).isin([Tuple(pd, pe) for pd, pe in docs])
|
||||
& (bt.docstatus == 1)
|
||||
)
|
||||
.groupby(ba.account, btp.payment_document, btp.payment_entry)
|
||||
.run(as_dict=True)
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT total, latest_date, gl_account, payment_document, payment_entry FROM (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER w AS rownum,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||
ba.account AS gl_account,
|
||||
btp.payment_document,
|
||||
btp.payment_entry
|
||||
FROM
|
||||
`tabBank Transaction Payments` btp
|
||||
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
|
||||
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
|
||||
WHERE
|
||||
(btp.payment_document, btp.payment_entry) IN %(docs)s
|
||||
AND bt.docstatus = 1
|
||||
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
|
||||
) temp
|
||||
WHERE
|
||||
rownum = 1
|
||||
""",
|
||||
dict(docs=docs),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
payment_allocation_details = {}
|
||||
|
||||
@@ -104,36 +104,6 @@ class TestBankTransaction(ERPNextTestSuite):
|
||||
self.assertEqual(bank_transaction.unallocated_amount, 1700)
|
||||
self.assertEqual(bank_transaction.payment_entries, [])
|
||||
|
||||
# Amending a reconciled payment entry must not carry over its clearance date
|
||||
def test_clearance_date_cleared_on_amend(self):
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
|
||||
)
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment.name,
|
||||
"amount": bank_transaction.unallocated_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
|
||||
self.assertTrue(frappe.db.get_value("Payment Entry", payment.name, "clearance_date"))
|
||||
|
||||
payment.reload()
|
||||
payment.cancel()
|
||||
|
||||
amended = frappe.copy_doc(payment)
|
||||
amended.amended_from = payment.name
|
||||
amended.docstatus = 0
|
||||
amended.insert()
|
||||
|
||||
self.assertFalse(amended.clearance_date)
|
||||
|
||||
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
|
||||
def test_debit_credit_output(self):
|
||||
bank_transaction = frappe.get_doc(
|
||||
|
||||
@@ -11,11 +11,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankTransactionRule(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -17,7 +17,6 @@ frappe.ui.form.on("Budget", {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
root_type: ["in", ["Income", "Expense"]],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
@@ -44,17 +43,13 @@ class CashierClosing(Document):
|
||||
self.make_calculations()
|
||||
|
||||
def get_outstanding(self):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
values = (
|
||||
frappe.qb.from_(si)
|
||||
.select(Sum(si.outstanding_amount))
|
||||
.where(
|
||||
(si.posting_date == self.date)
|
||||
& (si.posting_time >= self.from_time)
|
||||
& (si.posting_time <= self.time)
|
||||
& (si.owner == self.user)
|
||||
)
|
||||
.run()
|
||||
values = frappe.db.sql(
|
||||
"""
|
||||
select sum(outstanding_amount)
|
||||
from `tabSales Invoice`
|
||||
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
|
||||
""",
|
||||
(self.date, self.from_time, self.time, self.user),
|
||||
)
|
||||
self.outstanding_amount = flt(values[0][0] if values else 0)
|
||||
|
||||
|
||||
@@ -11,28 +11,22 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r && r.message) {
|
||||
let result = [],
|
||||
params = {};
|
||||
if (frm.doc.service_provider == "exchangerate.host") {
|
||||
result = ["result"];
|
||||
params = {
|
||||
let result = ["result"];
|
||||
let params = {
|
||||
date: "{transaction_date}",
|
||||
from: "{from_currency}",
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||
result = ["rates", "{to_currency}"];
|
||||
params = {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
symbols: "{to_currency}",
|
||||
};
|
||||
} else if (frm.doc.service_provider == "frankfurter.dev - v2") {
|
||||
result = ["rate"];
|
||||
params = {
|
||||
date: "{transaction_date}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
}
|
||||
add_param(frm, r.message, params, result);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -101,10 +101,11 @@
|
||||
"label": "Use HTTP Protocol"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 0,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-15 11:25:55.873110",
|
||||
"modified": "2026-03-16 13:28:21.075743",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -121,11 +122,24 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "frankfurter.dev - v2", "Custom"]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -70,14 +70,6 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "base", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
|
||||
|
||||
elif self.service_provider == "frankfurter.dev - v2":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
|
||||
self.append("result_key", {"key": "rate"})
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
|
||||
def validate_parameters(self):
|
||||
params = {}
|
||||
for row in self.req_params:
|
||||
@@ -113,20 +105,13 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in [
|
||||
"exchangerate.host",
|
||||
"frankfurter.dev",
|
||||
"frankfurter.app",
|
||||
"frankfurter.dev - v2",
|
||||
]:
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev - v2":
|
||||
api = "api.frankfurter.dev/v2/rate/{from_currency}/{to_currency}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Criterion, Order
|
||||
from frappe.query_builder.functions import Max, NullIf, Sum
|
||||
from frappe.query_builder.functions import NullIf, Sum
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
@@ -188,17 +188,11 @@ class ExchangeRateRevaluation(Document):
|
||||
accounts = [x[0] for x in res]
|
||||
|
||||
if accounts:
|
||||
gle = qb.DocType("GL Entry")
|
||||
having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
|
||||
(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
|
||||
)
|
||||
|
||||
# balance expressions reused in both SELECT and HAVING; postgres can't reference a
|
||||
# SELECT alias inside HAVING, so the aggregate expression must be repeated there.
|
||||
balance = Sum(gle.debit) - Sum(gle.credit)
|
||||
balance_in_account_currency = Sum(gle.debit_in_account_currency) - Sum(
|
||||
gle.credit_in_account_currency
|
||||
)
|
||||
having_clause = (balance != balance_in_account_currency) & (
|
||||
(balance_in_account_currency != 0) | (balance != 0)
|
||||
)
|
||||
gle = qb.DocType("GL Entry")
|
||||
|
||||
# conditions
|
||||
conditions = []
|
||||
@@ -215,15 +209,17 @@ class ExchangeRateRevaluation(Document):
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.account,
|
||||
# grouped by NullIf(party_type/party, ""); the bare columns + account_currency are
|
||||
# constant per group -> Max() keeps the GROUP BY valid on postgres with the same value.
|
||||
Max(gle.party_type).as_("party_type"),
|
||||
Max(gle.party).as_("party"),
|
||||
Max(gle.account_currency).as_("account_currency"),
|
||||
balance_in_account_currency.as_("balance_in_account_currency"),
|
||||
balance.as_("balance"),
|
||||
# zero_balance is recomputed in Python below (after rounding), so the SQL value is
|
||||
# unused -- dropped (it used MySQL's XOR operator, which postgres lacks).
|
||||
gle.party_type,
|
||||
gle.party,
|
||||
gle.account_currency,
|
||||
(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_(
|
||||
"balance_in_account_currency"
|
||||
),
|
||||
(Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
|
||||
(Sum(gle.debit) - Sum(gle.credit) == 0)
|
||||
^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_(
|
||||
"zero_balance"
|
||||
),
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, ""))
|
||||
|
||||
@@ -15,11 +15,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.item = "_Test Item"
|
||||
self.customer = "_Test Customer"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.set_system_and_company_settings()
|
||||
|
||||
def set_system_and_company_settings(self):
|
||||
|
||||
@@ -319,48 +319,56 @@ class InvoiceDiscounting(AccountsController):
|
||||
@frappe.whitelist()
|
||||
def get_invoices(filters: str):
|
||||
filters = frappe._dict(json.loads(filters))
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
di = frappe.qb.DocType("Discounted Invoice")
|
||||
|
||||
discounted = frappe.qb.from_(di).select(di.sales_invoice).where(di.docstatus == 1)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(si)
|
||||
.select(
|
||||
si.name.as_("sales_invoice"),
|
||||
si.customer,
|
||||
si.posting_date,
|
||||
si.outstanding_amount,
|
||||
si.debit_to,
|
||||
)
|
||||
.where((si.docstatus == 1) & (si.outstanding_amount > 0) & si.name.notin(discounted))
|
||||
)
|
||||
|
||||
cond = []
|
||||
if filters.customer:
|
||||
query = query.where(si.customer == filters.customer)
|
||||
cond.append("customer=%(customer)s")
|
||||
if filters.from_date:
|
||||
query = query.where(si.posting_date >= filters.from_date)
|
||||
cond.append("posting_date >= %(from_date)s")
|
||||
if filters.to_date:
|
||||
query = query.where(si.posting_date <= filters.to_date)
|
||||
cond.append("posting_date <= %(to_date)s")
|
||||
if filters.min_amount:
|
||||
query = query.where(si.base_grand_total >= filters.min_amount)
|
||||
cond.append("base_grand_total >= %(min_amount)s")
|
||||
if filters.max_amount:
|
||||
query = query.where(si.base_grand_total <= filters.max_amount)
|
||||
cond.append("base_grand_total <= %(max_amount)s")
|
||||
|
||||
return query.run(as_dict=1)
|
||||
where_condition = ""
|
||||
if cond:
|
||||
where_condition += " and " + " and ".join(cond)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name as sales_invoice,
|
||||
customer,
|
||||
posting_date,
|
||||
outstanding_amount,
|
||||
debit_to
|
||||
from `tabSales Invoice` si
|
||||
where
|
||||
docstatus = 1
|
||||
and outstanding_amount > 0
|
||||
%s
|
||||
and not exists(select di.name from `tabDiscounted Invoice` di
|
||||
where di.docstatus=1 and di.sales_invoice=si.name)
|
||||
"""
|
||||
% where_condition,
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_party_account_based_on_invoice_discounting(sales_invoice):
|
||||
party_account = None
|
||||
par = frappe.qb.DocType("Invoice Discounting")
|
||||
ch = frappe.qb.DocType("Discounted Invoice")
|
||||
invoice_discounting = (
|
||||
frappe.qb.from_(par)
|
||||
.inner_join(ch)
|
||||
.on(par.name == ch.parent)
|
||||
.select(par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status)
|
||||
.where((par.docstatus == 1) & (ch.sales_invoice == sales_invoice))
|
||||
.run(as_dict=1)
|
||||
invoice_discounting = frappe.db.sql(
|
||||
"""
|
||||
select par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status
|
||||
from `tabInvoice Discounting` par, `tabDiscounted Invoice` ch
|
||||
where par.name=ch.parent
|
||||
and par.docstatus=1
|
||||
and ch.sales_invoice = %s
|
||||
""",
|
||||
(sales_invoice),
|
||||
as_dict=1,
|
||||
)
|
||||
if invoice_discounting:
|
||||
if invoice_discounting[0].status == "Disbursed":
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -889,7 +889,7 @@ class JournalEntry(AccountsController):
|
||||
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
|
||||
return
|
||||
|
||||
self.set_total_debit_credit()
|
||||
self.total_debit, self.total_credit = 0, 0
|
||||
diff = flt(self.difference, self.precision("difference"))
|
||||
if diff:
|
||||
self._apply_difference_to_blank_row(diff, difference_account)
|
||||
|
||||
@@ -764,29 +764,6 @@ class TestJournalEntry(ERPNextTestSuite):
|
||||
self.assertEqual(blank_row.credit_in_account_currency, 100)
|
||||
self.assertEqual(jv.total_debit, jv.total_credit)
|
||||
|
||||
def test_get_balance_recomputes_difference_ignoring_client_value(self):
|
||||
"""get_balance computes its own difference instead of trusting a stale client-sent value."""
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.company = "_Test Company"
|
||||
jv.posting_date = nowdate()
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": "_Test Cash - _TC",
|
||||
"debit_in_account_currency": 100,
|
||||
"debit": 100,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
jv.append("accounts", {"account": "_Test Bank - _TC", "exchange_rate": 1})
|
||||
# a stale/incorrect value as the client might send; get_balance must not rely on it
|
||||
jv.difference = 0
|
||||
|
||||
jv.get_balance()
|
||||
self.assertEqual(jv.accounts[1].credit_in_account_currency, 100)
|
||||
self.assertEqual(jv.total_debit, jv.total_credit)
|
||||
self.assertEqual(jv.difference, 0)
|
||||
|
||||
def test_get_outstanding_invoices_builds_write_off_rows(self):
|
||||
"""Characterize: get_outstanding_invoices adds a party row for each outstanding invoice."""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 4,
|
||||
"fetch_from": "bank_account.account",
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
|
||||
@@ -12,11 +12,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.configure_monitoring_tool()
|
||||
self.clear_old_entries()
|
||||
|
||||
def configure_monitoring_tool(self):
|
||||
monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
|
||||
@@ -52,11 +52,12 @@ class ModeofPayment(Document):
|
||||
|
||||
def validate_pos_mode_of_payment(self):
|
||||
if not self.enabled:
|
||||
pos_profiles = frappe.get_all(
|
||||
"Sales Invoice Payment",
|
||||
filters={"parenttype": "POS Profile", "mode_of_payment": self.name},
|
||||
pluck="parent",
|
||||
pos_profiles = frappe.db.sql(
|
||||
"""SELECT sip.parent FROM `tabSales Invoice Payment` sip
|
||||
WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""",
|
||||
(self.name),
|
||||
)
|
||||
pos_profiles = list(map(lambda x: x[0], pos_profiles))
|
||||
|
||||
if pos_profiles:
|
||||
message = _(
|
||||
|
||||
@@ -270,13 +270,6 @@ def start_import(invoices):
|
||||
errors = 0
|
||||
names = []
|
||||
for idx, d in enumerate(invoices):
|
||||
# Scope each invoice to a savepoint so a failure only undoes that invoice.
|
||||
# A plain rollback() would discard the whole transaction — including invoices
|
||||
# imported earlier in this batch and the error logs of earlier failures (the
|
||||
# latter only survive on mariadb because the Error Log table is MyISAM; on
|
||||
# postgres they would be lost). Rolling back to a savepoint keeps both.
|
||||
savepoint = f"opening_invoice_{frappe.generate_hash(length=8)}"
|
||||
frappe.db.savepoint(savepoint)
|
||||
try:
|
||||
invoice_number = None
|
||||
if d.invoice_number:
|
||||
@@ -291,7 +284,7 @@ def start_import(invoices):
|
||||
names.append(doc.name)
|
||||
except Exception:
|
||||
errors += 1
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
frappe.db.rollback()
|
||||
doc.log_error("Opening invoice creation failed")
|
||||
if errors:
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -9,8 +9,8 @@ import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Case, Tuple
|
||||
from frappe.query_builder.functions import Abs, Count, Max
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
|
||||
from pypika.functions import Coalesce, Sum
|
||||
@@ -766,19 +766,13 @@ class PaymentEntry(AccountsController):
|
||||
def validate_journal_entry(self):
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype == "Journal Entry":
|
||||
je_accounts = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"account": self.party_account,
|
||||
"party": self.party,
|
||||
"docstatus": 1,
|
||||
"parent": d.reference_name,
|
||||
},
|
||||
or_filters=[
|
||||
["reference_type", "is", "not set"],
|
||||
["reference_type", "in", ["Sales Order", "Purchase Order"]],
|
||||
],
|
||||
fields=["debit", "credit"],
|
||||
je_accounts = frappe.db.sql(
|
||||
"""select debit, credit from `tabJournal Entry Account`
|
||||
where account = %s and party=%s and docstatus = 1 and parent = %s
|
||||
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
|
||||
""",
|
||||
(self.party_account, self.party, d.reference_name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not je_accounts:
|
||||
@@ -863,17 +857,27 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision)
|
||||
|
||||
ps = frappe.qb.DocType("Payment Schedule")
|
||||
if cancel:
|
||||
(
|
||||
frappe.qb.update(ps)
|
||||
.set(ps.paid_amount, ps.paid_amount - (allocated_amount - discounted_amt))
|
||||
.set(ps.base_paid_amount, ps.base_paid_amount - base_paid_amount)
|
||||
.set(ps.discounted_amount, ps.discounted_amount - discounted_amt)
|
||||
.set(ps.outstanding, ps.outstanding + allocated_amount)
|
||||
.set(ps.base_outstanding, ps.base_outstanding - base_outstanding)
|
||||
.where((ps.parent == key[1]) & (ps.payment_term == key[0]))
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabPayment Schedule`
|
||||
SET
|
||||
paid_amount = `paid_amount` - %s,
|
||||
base_paid_amount = `base_paid_amount` - %s,
|
||||
discounted_amount = `discounted_amount` - %s,
|
||||
outstanding = `outstanding` + %s,
|
||||
base_outstanding = `base_outstanding` - %s
|
||||
WHERE parent = %s and payment_term = %s""",
|
||||
(
|
||||
allocated_amount - discounted_amt,
|
||||
base_paid_amount,
|
||||
discounted_amt,
|
||||
allocated_amount,
|
||||
base_outstanding,
|
||||
key[1],
|
||||
key[0],
|
||||
),
|
||||
)
|
||||
else:
|
||||
if allocated_amount > outstanding:
|
||||
frappe.throw(
|
||||
@@ -883,15 +887,26 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
if allocated_amount and outstanding:
|
||||
(
|
||||
frappe.qb.update(ps)
|
||||
.set(ps.paid_amount, ps.paid_amount + (allocated_amount - discounted_amt))
|
||||
.set(ps.base_paid_amount, ps.base_paid_amount + base_paid_amount)
|
||||
.set(ps.discounted_amount, ps.discounted_amount + discounted_amt)
|
||||
.set(ps.outstanding, ps.outstanding - allocated_amount)
|
||||
.set(ps.base_outstanding, ps.base_outstanding - base_outstanding)
|
||||
.where((ps.parent == key[1]) & (ps.payment_term == key[0]))
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabPayment Schedule`
|
||||
SET
|
||||
paid_amount = `paid_amount` + %s,
|
||||
base_paid_amount = `base_paid_amount` + %s,
|
||||
discounted_amount = `discounted_amount` + %s,
|
||||
outstanding = `outstanding` - %s,
|
||||
base_outstanding = `base_outstanding` - %s
|
||||
WHERE parent = %s and payment_term = %s""",
|
||||
(
|
||||
allocated_amount - discounted_amt,
|
||||
base_paid_amount,
|
||||
discounted_amt,
|
||||
allocated_amount,
|
||||
base_outstanding,
|
||||
key[1],
|
||||
key[0],
|
||||
),
|
||||
)
|
||||
|
||||
def get_allocated_amount_in_transaction_currency(
|
||||
self, allocated_amount, reference_doctype, reference_docname
|
||||
@@ -1201,7 +1216,11 @@ class PaymentEntry(AccountsController):
|
||||
# Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast
|
||||
def clear_unallocated_reference_document_rows(self):
|
||||
self.set("references", self.get("references", {"allocated_amount": ["not in", [0, None, ""]]}))
|
||||
frappe.db.delete("Payment Entry Reference", {"parent": self.name, "allocated_amount": 0})
|
||||
frappe.db.sql(
|
||||
"""delete from `tabPayment Entry Reference`
|
||||
where parent = %s and allocated_amount = 0""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
def set_title(self):
|
||||
if frappe.flags.in_import and self.title:
|
||||
@@ -1857,7 +1876,7 @@ def get_matched_payment_request_of_references(references=None):
|
||||
PR.reference_doctype,
|
||||
PR.reference_name,
|
||||
PR.outstanding_amount.as_("allocated_amount"),
|
||||
Max(PR.name).as_("payment_request"), # count == 1 below ⇒ one row per group; postgres-safe
|
||||
PR.name.as_("payment_request"),
|
||||
Count("*").as_("count"),
|
||||
)
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
|
||||
@@ -2296,7 +2315,12 @@ def get_orders_to_be_billed(
|
||||
if not voucher_type:
|
||||
return []
|
||||
|
||||
# dynamic dimension filters
|
||||
condition = ""
|
||||
active_dimensions = get_dimensions(True)[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
@@ -2305,38 +2329,38 @@ def get_orders_to_be_billed(
|
||||
grand_total_field = "grand_total"
|
||||
rounded_total_field = "rounded_total"
|
||||
|
||||
voucher = frappe.qb.DocType(voucher_type)
|
||||
invoice_amount = (
|
||||
Case()
|
||||
.when(voucher[rounded_total_field] != 0, voucher[rounded_total_field])
|
||||
.else_(voucher[grand_total_field])
|
||||
orders = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name as voucher_no,
|
||||
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
|
||||
(if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount,
|
||||
transaction_date as posting_date
|
||||
from
|
||||
`tab{voucher_type}`
|
||||
where
|
||||
{party_type} = %s
|
||||
and docstatus = 1
|
||||
and company = %s
|
||||
and status != "Closed"
|
||||
and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid
|
||||
and abs(100 - per_billed) > 0.01
|
||||
{condition}
|
||||
order by
|
||||
transaction_date, name
|
||||
""".format(
|
||||
**{
|
||||
"rounded_total_field": rounded_total_field,
|
||||
"grand_total_field": grand_total_field,
|
||||
"voucher_type": voucher_type,
|
||||
"party_type": scrub(party_type),
|
||||
"condition": condition,
|
||||
}
|
||||
),
|
||||
(party, company),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(voucher)
|
||||
.select(
|
||||
voucher.name.as_("voucher_no"),
|
||||
invoice_amount.as_("invoice_amount"),
|
||||
(invoice_amount - voucher.advance_paid).as_("outstanding_amount"),
|
||||
voucher.transaction_date.as_("posting_date"),
|
||||
)
|
||||
.where(
|
||||
(voucher[scrub(party_type)] == party)
|
||||
& (voucher.docstatus == 1)
|
||||
& (voucher.company == company)
|
||||
& (voucher.status != "Closed")
|
||||
& (invoice_amount > voucher.advance_paid)
|
||||
& (Abs(100 - voucher.per_billed) > 0.01)
|
||||
)
|
||||
)
|
||||
|
||||
# dynamic dimension filters
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
query = query.where(voucher[dim.fieldname] == filters.get(dim.fieldname))
|
||||
|
||||
orders = query.orderby(voucher.transaction_date).orderby(voucher.name).run(as_dict=True)
|
||||
|
||||
order_list = []
|
||||
for d in orders:
|
||||
if (
|
||||
@@ -2385,8 +2409,8 @@ def get_negative_outstanding_invoices(
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
'{voucher_type}' as voucher_type, name as voucher_no, {account} as account,
|
||||
coalesce(nullif({rounded_total_field}, 0), {grand_total_field}) as invoice_amount,
|
||||
"{voucher_type}" as voucher_type, name as voucher_no, {account} as account,
|
||||
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
|
||||
outstanding_amount, posting_date,
|
||||
due_date, conversion_rate as exchange_rate
|
||||
from
|
||||
@@ -2756,7 +2780,7 @@ def get_payment_entry(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
|
||||
pe.set_exchange_rate()
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
@@ -3248,28 +3272,27 @@ def get_reference_as_per_payment_terms(
|
||||
|
||||
|
||||
def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
if party_type == "Customer":
|
||||
dr_or_cr = gle.credit_in_account_currency - gle.debit_in_account_currency
|
||||
dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
|
||||
else:
|
||||
dr_or_cr = gle.debit_in_account_currency - gle.credit_in_account_currency
|
||||
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
|
||||
|
||||
paid_amount = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(Sum(dr_or_cr))
|
||||
.where(
|
||||
(gle.against_voucher_type == dt)
|
||||
& (gle.against_voucher == dn)
|
||||
& (gle.party_type == party_type)
|
||||
& (gle.party == party)
|
||||
& (gle.account == account)
|
||||
& (gle.due_date == due_date)
|
||||
& (dr_or_cr > 0)
|
||||
)
|
||||
.run()
|
||||
paid_amount = frappe.db.sql(
|
||||
f"""
|
||||
select ifnull(sum({dr_or_cr}), 0) as paid_amount
|
||||
from `tabGL Entry`
|
||||
where against_voucher_type = %s
|
||||
and against_voucher = %s
|
||||
and party_type = %s
|
||||
and party = %s
|
||||
and account = %s
|
||||
and due_date = %s
|
||||
and {dr_or_cr} > 0
|
||||
""",
|
||||
(dt, dn, party_type, party, account, due_date),
|
||||
)
|
||||
|
||||
return (paid_amount[0][0] or 0) if paid_amount else 0
|
||||
return paid_amount[0][0] if paid_amount else 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -34,14 +34,8 @@ class PaymentEntryGLComposer(BaseGLComposer):
|
||||
self.add_deductions_gl_entries(gl_entries)
|
||||
self.add_tax_gl_entries(gl_entries)
|
||||
add_regional_gl_entries(gl_entries, doc)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries, doc)
|
||||
return gl_entries
|
||||
|
||||
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries, doc):
|
||||
for gle in gl_entries:
|
||||
gle.setdefault("transaction_currency", doc.transaction_currency)
|
||||
gle.setdefault("transaction_exchange_rate", doc.transaction_exchange_rate)
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
if not doc.party_account:
|
||||
|
||||
@@ -532,8 +532,6 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700)
|
||||
pe.source_exchange_rate = 50
|
||||
pe.set_amounts()
|
||||
pe.reference_no = si.name
|
||||
pe.reference_date = nowdate()
|
||||
|
||||
@@ -609,8 +607,6 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
pe = get_payment_entry(
|
||||
"Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900
|
||||
)
|
||||
pe.source_exchange_rate = 50
|
||||
pe.set_amounts()
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
@@ -1037,17 +1033,14 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
gle.credit_in_account_currency,
|
||||
gle.debit_in_transaction_currency,
|
||||
gle.credit_in_transaction_currency,
|
||||
gle.transaction_currency,
|
||||
gle.transaction_exchange_rate,
|
||||
)
|
||||
.orderby(gle.account)
|
||||
.where(gle.voucher_no == payment_entry.name)
|
||||
.run()
|
||||
)
|
||||
# transaction currency/rate come from the paid-from USD account (company currency is INR)
|
||||
expected_gl_entries = (
|
||||
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0, "USD", 84.4),
|
||||
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0, "USD", 84.4),
|
||||
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0),
|
||||
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0),
|
||||
)
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
|
||||
@@ -10,22 +10,76 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
self.company = "_Test Company"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.creditors = "Creditors - _TC"
|
||||
self.bank = "Cash - _TC"
|
||||
self.item = "_Test Item"
|
||||
self.customer = "_Test Customer"
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Payment Ledger"
|
||||
company = None
|
||||
if frappe.db.exists("Company", company_name):
|
||||
company = frappe.get_doc("Company", company_name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company_name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "All Warehouses - _PL"
|
||||
self.income_account = "Sales - _PL"
|
||||
self.expense_account = "Cost of Goods Sold - _PL"
|
||||
self.debit_to = "Debtors - _PL"
|
||||
self.creditors = "Creditors - _PL"
|
||||
|
||||
# create bank account
|
||||
if frappe.db.exists("Account", "HDFC - _PL"):
|
||||
self.bank = "HDFC - _PL"
|
||||
else:
|
||||
bank_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - _PL",
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
bank_acc.save()
|
||||
self.bank = bank_acc.name
|
||||
|
||||
def create_item(self):
|
||||
item_name = "_Test PL Item"
|
||||
item = create_item(
|
||||
item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_customer(self):
|
||||
name = "_Test PL Customer"
|
||||
if frappe.db.exists("Customer", name):
|
||||
self.customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
@@ -98,6 +152,18 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
)
|
||||
return so
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
|
||||
@@ -60,32 +60,23 @@ class PaymentOrder(Document):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_mop_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||
return frappe.get_all(
|
||||
"Payment Order Reference",
|
||||
filters={"parent": filters.get("parent"), "mode_of_payment": ["like", f"%{txt}%"]},
|
||||
fields=["mode_of_payment"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
as_list=True,
|
||||
return frappe.db.sql(
|
||||
""" select mode_of_payment from `tabPayment Order Reference`
|
||||
where parent = %(parent)s and mode_of_payment like %(txt)s
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||
return frappe.get_all(
|
||||
"Payment Order Reference",
|
||||
filters={
|
||||
"parent": filters.get("parent"),
|
||||
"supplier": ["like", f"%{txt}%"],
|
||||
"payment_reference": ["is", "not set"],
|
||||
},
|
||||
fields=["supplier"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
as_list=True,
|
||||
return frappe.db.sql(
|
||||
""" select supplier from `tabPayment Order Reference`
|
||||
where parent = %(parent)s and supplier like %(txt)s and
|
||||
(payment_reference is null or payment_reference='')
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,12 +11,11 @@ from erpnext import get_company_currency
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
get_payment_entry,
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_advance_payment_doctypes, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
@@ -629,9 +628,11 @@ class PaymentRequest(Document):
|
||||
|
||||
def check_if_payment_entry_exists(self):
|
||||
if self.status == "Paid":
|
||||
if frappe.db.exists(
|
||||
if frappe.get_all(
|
||||
"Payment Entry Reference",
|
||||
{"reference_name": self.reference_name, "docstatus": ["<", 2]},
|
||||
filters={"reference_name": self.reference_name, "docstatus": ["<", 2]},
|
||||
fields=["parent"],
|
||||
limit=1,
|
||||
):
|
||||
frappe.throw(_("Payment Entry already exists"), title=_("Error"))
|
||||
|
||||
@@ -1210,11 +1211,10 @@ def get_dummy_message(doc):
|
||||
@frappe.whitelist()
|
||||
def get_subscription_details(reference_doctype: str, reference_name: str):
|
||||
if reference_doctype == "Sales Invoice":
|
||||
subscriptions = frappe.get_all(
|
||||
"Subscription Invoice",
|
||||
filters={"invoice": reference_name},
|
||||
fields=["parent as sub_name"],
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
subscriptions = frappe.db.sql(
|
||||
"""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",
|
||||
reference_name,
|
||||
as_dict=1,
|
||||
)
|
||||
subscription_plans = []
|
||||
for subscription in subscriptions:
|
||||
|
||||
@@ -332,12 +332,7 @@ class TestPaymentRequest(ERPNextTestSuite):
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.source_exchange_rate = 50
|
||||
pe.target_exchange_rate = 50
|
||||
pe.set_amounts()
|
||||
pe.insert(ignore_permissions=True)
|
||||
pe.submit()
|
||||
pe = pr.set_as_paid()
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
@@ -423,12 +418,7 @@ class TestPaymentRequest(ERPNextTestSuite):
|
||||
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
|
||||
pr = frappe.get_doc(pr).save().submit()
|
||||
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.target_exchange_rate = 80
|
||||
pe.paid_amount = 800
|
||||
pe.set_amounts()
|
||||
pe.insert(ignore_permissions=True)
|
||||
pe.submit()
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(pe.base_paid_amount, 800)
|
||||
self.assertEqual(pe.paid_amount, 800)
|
||||
self.assertEqual(pe.base_received_amount, 800)
|
||||
|
||||
@@ -18,6 +18,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
|
||||
|
||||
def test_closing_entry(self):
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
@@ -26,10 +27,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = "Test PCV Company"
|
||||
jv1.company = company
|
||||
jv1.save()
|
||||
jv1.submit()
|
||||
|
||||
@@ -39,10 +40,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cost of Goods Sold - TPC",
|
||||
account2="Cash - TPC",
|
||||
cost_center=cost_center,
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv2.company = "Test PCV Company"
|
||||
jv2.company = company
|
||||
jv2.save()
|
||||
jv2.submit()
|
||||
|
||||
@@ -66,13 +67,14 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertEqual(pcv_gle, expected_gle)
|
||||
|
||||
def test_cost_center_wise_posting(self):
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
|
||||
cost_center1 = create_cost_center("Main")
|
||||
cost_center2 = create_cost_center("Western Branch")
|
||||
|
||||
create_sales_invoice(
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
cost_center=cost_center1,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
@@ -83,7 +85,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
create_sales_invoice(
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
cost_center=cost_center2,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
@@ -128,11 +130,12 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
def test_period_closing_with_finance_book_entries(self):
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
create_sales_invoice(
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
cost_center=cost_center,
|
||||
@@ -149,9 +152,9 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
amount=400,
|
||||
cost_center=cost_center,
|
||||
posting_date="2021-03-15",
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
)
|
||||
jv.company = "Test PCV Company"
|
||||
jv.company = company
|
||||
jv.finance_book = create_finance_book().name
|
||||
jv.save()
|
||||
jv.submit()
|
||||
@@ -178,6 +181,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertSequenceEqual(pcv_gle, expected_gle)
|
||||
|
||||
def test_gl_entries_restrictions(self):
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
@@ -188,15 +192,16 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = "Test PCV Company"
|
||||
jv1.company = company
|
||||
jv1.save()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jv1.submit)
|
||||
|
||||
def test_closing_balance_with_dimensions_and_test_reposting_entry(self):
|
||||
company = create_company()
|
||||
cost_center1 = create_cost_center("Test Cost Center 1")
|
||||
cost_center2 = create_cost_center("Test Cost Center 2")
|
||||
|
||||
@@ -206,10 +211,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center1,
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = "Test PCV Company"
|
||||
jv1.company = company
|
||||
jv1.save()
|
||||
jv1.submit()
|
||||
|
||||
@@ -219,10 +224,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv2.company = "Test PCV Company"
|
||||
jv2.company = company
|
||||
jv2.save()
|
||||
jv2.submit()
|
||||
|
||||
@@ -249,11 +254,11 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
|
||||
jv3.company = "Test PCV Company"
|
||||
jv3.company = company
|
||||
jv3.save()
|
||||
jv3.submit()
|
||||
|
||||
@@ -288,12 +293,12 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertEqual(cc2_closing_balance.credit, 500)
|
||||
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
|
||||
|
||||
warehouse = frappe.db.get_value("Warehouse", {"company": "Test PCV Company"}, "name")
|
||||
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
|
||||
|
||||
repost_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"company": "Test PCV Company",
|
||||
"company": company,
|
||||
"posting_date": "2020-03-15",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": "Test Item 1",
|
||||
@@ -334,6 +339,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
{"enable_immutable_ledger": 1},
|
||||
)
|
||||
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
jv = make_journal_entry(
|
||||
@@ -342,10 +348,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company="Test PCV Company",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv.company = "Test PCV Company"
|
||||
jv.company = company
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
@@ -371,6 +377,19 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
|
||||
|
||||
|
||||
def create_company():
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": "Test PCV Company",
|
||||
"country": "United States",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
)
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
return company.name
|
||||
|
||||
|
||||
def create_account():
|
||||
account = frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -553,8 +553,7 @@ def process_individual_date(docname: str, date, report_type, parentfield):
|
||||
Sum(gle.credit).as_("credit"),
|
||||
Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"),
|
||||
Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"),
|
||||
# account_currency is constant per grouped account -> Max() keeps the GROUP BY postgres-valid
|
||||
Max(gle.account_currency).as_("account_currency"),
|
||||
gle.account_currency,
|
||||
).where(
|
||||
(gle.company.eq(company))
|
||||
& (gle.is_cancelled.eq(0))
|
||||
|
||||
@@ -25,8 +25,10 @@ class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
self.company = "_Test Company"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_customer(customer_name="Other Customer")
|
||||
self.clear_old_entries()
|
||||
self.si = create_sales_invoice()
|
||||
create_sales_invoice(customer="Other Customer")
|
||||
|
||||
|
||||
@@ -167,6 +167,14 @@
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"commission_section",
|
||||
"purchase_partner",
|
||||
"amount_eligible_for_commission",
|
||||
"column_break_commission",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"purchase_team_section",
|
||||
"purchase_team",
|
||||
"more_info_tab",
|
||||
"status_section",
|
||||
"status",
|
||||
@@ -614,12 +622,10 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.items.every((item) => !item.pr_detail)",
|
||||
"description": "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Purchase Receipt is created separately.",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Stock",
|
||||
"print_hide": 1,
|
||||
"show_description_on_click": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scan_barcode",
|
||||
@@ -1685,6 +1691,66 @@
|
||||
"fieldname": "automation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Automation"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "purchase_partner",
|
||||
"fieldname": "commission_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Commission",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_partner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Partner",
|
||||
"options": "Purchase Partner",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_commission",
|
||||
"fieldtype": "Column Break",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "purchase_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Commission Rate (%)",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "purchase_team",
|
||||
"fieldname": "purchase_team_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Purchase Team",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "purchase_team",
|
||||
"fieldtype": "Table",
|
||||
"label": "Purchase Contributions and Incentives",
|
||||
"options": "Purchase Team",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1692,7 +1758,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-13 18:36:46.704623",
|
||||
"modified": "2026-05-28 12:36:55.215363",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -51,6 +51,16 @@ class ExpenseAccountService:
|
||||
if doc.update_stock and item.warehouse and (not item.from_warehouse):
|
||||
_inv_dict = doc.get_inventory_account_dict(item, inventory_account_map)
|
||||
|
||||
if for_validate and item.expense_account and item.expense_account != _inv_dict["account"]:
|
||||
msg = _(
|
||||
"Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account"
|
||||
).format(
|
||||
item.idx,
|
||||
frappe.bold(_inv_dict["account"]),
|
||||
frappe.bold(item.expense_account),
|
||||
frappe.bold(item.warehouse),
|
||||
)
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
item.expense_account = _inv_dict["account"]
|
||||
else:
|
||||
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"section_break_82",
|
||||
"grant_commission",
|
||||
"page_break"
|
||||
],
|
||||
"fields": [
|
||||
@@ -1004,6 +1005,15 @@
|
||||
"label": "Delivered by Supplier",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1021,4 +1031,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,6 @@ def start_repost(account_repost_doc: str | None = None) -> None:
|
||||
frappe.flags.through_repost_accounting_ledger = True
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
repost_doc.check_permission("write")
|
||||
|
||||
if repost_doc.docstatus == 1:
|
||||
# Prevent repost on invoices with deferred accounting
|
||||
|
||||
@@ -715,7 +715,6 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
|
||||
"description": "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Delivery Note is created separately.",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
@@ -723,8 +722,7 @@
|
||||
"label": "Update Stock",
|
||||
"oldfieldname": "update_stock",
|
||||
"oldfieldtype": "Check",
|
||||
"print_hide": 1,
|
||||
"show_description_on_click": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scan_barcode",
|
||||
|
||||
@@ -412,8 +412,8 @@ class SalesInvoice(SellingController):
|
||||
validate_account_head(item.idx, item.income_account, self.company, _("Income"))
|
||||
|
||||
def before_save(self):
|
||||
POSService(self).update_paid_amount()
|
||||
POSService(self).set_account_for_mode_of_payment()
|
||||
POSService(self).set_paid_amount()
|
||||
|
||||
def before_submit(self):
|
||||
self.add_remarks()
|
||||
|
||||
@@ -114,17 +114,10 @@ class POSService:
|
||||
|
||||
return pos
|
||||
|
||||
def update_paid_amount(self) -> None:
|
||||
def set_paid_amount(self) -> None:
|
||||
doc = self.doc
|
||||
paid_amount = 0.0
|
||||
base_paid_amount = 0.0
|
||||
|
||||
if not cint(doc.is_pos) and doc.is_return:
|
||||
doc.set("payments", [])
|
||||
doc.paid_amount = paid_amount
|
||||
doc.base_paid_amount = base_paid_amount
|
||||
return
|
||||
|
||||
for data in doc.payments:
|
||||
data.base_amount = flt(data.amount * doc.conversion_rate, doc.precision("base_paid_amount"))
|
||||
paid_amount += data.amount
|
||||
|
||||
@@ -16,14 +16,12 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_supplier()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
@@ -374,6 +372,7 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
so1 = self.create_sales_order()
|
||||
so2 = self.create_sales_order()
|
||||
|
||||
@@ -424,11 +423,7 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
def test_07_adv_from_so_to_invoice(self):
|
||||
frappe.db.set_value("Company", self.company, "book_advance_payments_in_separate_party_account", True)
|
||||
frappe.db.set_value(
|
||||
"Company", self.company, "default_advance_received_account", "Advance Received - _TC"
|
||||
)
|
||||
|
||||
self.enable_advance_as_liability()
|
||||
so = self.create_sales_order()
|
||||
pe = self.create_payment_entry()
|
||||
pe.paid_amount = 1000
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs, Max, Sum
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
@@ -72,7 +72,7 @@ class UnreconcilePayment(Document):
|
||||
alloc.party,
|
||||
)
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", 1)
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -120,20 +120,18 @@ def get_linked_payments_for_doc(
|
||||
res = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Max(ple.account).as_("account"),
|
||||
Max(ple.party_type).as_("party_type"),
|
||||
Max(ple.party).as_("party"),
|
||||
Max(ple.company).as_("company"),
|
||||
Max(ple.voucher_type).as_("reference_doctype"),
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.company,
|
||||
ple.voucher_type.as_("reference_doctype"),
|
||||
ple.voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
Max(ple.account_currency).as_("account_currency"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.voucher_no, ple.against_voucher_no)
|
||||
.having(Abs(Sum(ple.amount_in_account_currency)) > 0)
|
||||
# deterministic order across backends (postgres GROUP BY does not imply ordering)
|
||||
.orderby(ple.voucher_no)
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return res
|
||||
@@ -148,19 +146,17 @@ def get_linked_payments_for_doc(
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Max(ple.company).as_("company"),
|
||||
Max(ple.account).as_("account"),
|
||||
Max(ple.party_type).as_("party_type"),
|
||||
Max(ple.party).as_("party"),
|
||||
Max(ple.against_voucher_type).as_("reference_doctype"),
|
||||
ple.company,
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
ple.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
Max(ple.account_currency).as_("account_currency"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.against_voucher_no)
|
||||
# deterministic order across backends (postgres GROUP BY does not imply ordering)
|
||||
.orderby(ple.against_voucher_no)
|
||||
)
|
||||
|
||||
res = query.run(as_dict=True)
|
||||
@@ -184,18 +180,15 @@ def get_linked_advances(company, docname):
|
||||
return (
|
||||
qb.from_(adv)
|
||||
.select(
|
||||
# non-grouped columns are constant per against_voucher_no -> Max() is unchanged and postgres-valid
|
||||
Max(adv.company).as_("company"),
|
||||
Max(adv.against_voucher_type).as_("reference_doctype"),
|
||||
adv.company,
|
||||
adv.against_voucher_type.as_("reference_doctype"),
|
||||
adv.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(adv.amount)).as_("allocated_amount"),
|
||||
Max(adv.currency).as_("currency"),
|
||||
adv.currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.having(Abs(Sum(adv.amount)) > 0)
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.groupby(adv.against_voucher_no)
|
||||
# deterministic order across backends (postgres GROUP BY does not imply ordering)
|
||||
.orderby(adv.against_voucher_no)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -509,6 +509,11 @@ def get_party_advance_account(party_type, party, company):
|
||||
return account
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_party_bank_account(party_type: str, party: str):
|
||||
return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1})
|
||||
|
||||
|
||||
def get_party_account_currency(party_type, party, company):
|
||||
def generator():
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
@@ -543,19 +548,11 @@ def get_party_gle_currency(party_type, party, company):
|
||||
|
||||
def get_party_gle_account(party_type, party, company):
|
||||
def generator():
|
||||
gl = qb.DocType("GL Entry")
|
||||
existing_gle_account = (
|
||||
qb.from_(gl)
|
||||
.select(gl.account)
|
||||
.where(
|
||||
(gl.docstatus == 1)
|
||||
& (gl.company == company)
|
||||
& (gl.party_type == party_type)
|
||||
& (gl.party == party)
|
||||
& (gl.is_cancelled == 0)
|
||||
)
|
||||
.limit(1)
|
||||
.run()
|
||||
existing_gle_account = frappe.db.sql(
|
||||
"""select account from `tabGL Entry`
|
||||
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
|
||||
limit 1""",
|
||||
{"company": company, "party_type": party_type, "party": party},
|
||||
)
|
||||
|
||||
return existing_gle_account[0][0] if existing_gle_account else None
|
||||
|
||||
@@ -9,10 +9,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.item = "_Test Item"
|
||||
self.supplier = "_Test Supplier 2"
|
||||
self.creditors_usd = "_Test Payable USD - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_supplier(currency="USD", supplier_name="Test Supplier2")
|
||||
self.create_usd_payable_account()
|
||||
|
||||
def test_accounts_payable_for_foreign_currency_supplier(self):
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Max, Substring, Sum
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -427,21 +427,32 @@ class ReceivablePayableReport:
|
||||
self.delivery_notes = frappe._dict()
|
||||
|
||||
# delivery note link inside sales invoice
|
||||
si_against_dn = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"docstatus": 1, "parent": ["in", list(self.invoices)]},
|
||||
fields=["parent", "delivery_note"],
|
||||
# nosemgrep
|
||||
si_against_dn = frappe.db.sql(
|
||||
"""
|
||||
select parent, delivery_note
|
||||
from `tabSales Invoice Item`
|
||||
where docstatus=1 and parent in (%s)
|
||||
"""
|
||||
% (",".join(["%s"] * len(self.invoices))),
|
||||
tuple(self.invoices),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in si_against_dn:
|
||||
if d.delivery_note:
|
||||
self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note)
|
||||
|
||||
dn_against_si = frappe.get_all(
|
||||
"Delivery Note Item",
|
||||
filters={"against_sales_invoice": ["in", list(self.invoices)]},
|
||||
fields=["parent", "against_sales_invoice"],
|
||||
distinct=True,
|
||||
# nosemgrep
|
||||
dn_against_si = frappe.db.sql(
|
||||
"""
|
||||
select distinct parent, against_sales_invoice
|
||||
from `tabDelivery Note Item`
|
||||
where against_sales_invoice in (%s)
|
||||
"""
|
||||
% (",".join(["%s"] * len(self.invoices))),
|
||||
tuple(self.invoices),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in dn_against_si:
|
||||
@@ -465,10 +476,14 @@ class ReceivablePayableReport:
|
||||
|
||||
# Get Sales Team
|
||||
if self.filters.show_sales_person:
|
||||
sales_team = frappe.get_all(
|
||||
"Sales Team",
|
||||
filters={"parenttype": "Sales Invoice"},
|
||||
fields=["parent", "sales_person"],
|
||||
# nosemgrep
|
||||
sales_team = frappe.db.sql(
|
||||
"""
|
||||
select parent, sales_person
|
||||
from `tabSales Team`
|
||||
where parenttype = 'Sales Invoice'
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
for d in sales_team:
|
||||
self.invoice_details.setdefault(d.parent, {}).setdefault("sales_team", []).append(
|
||||
@@ -533,31 +548,22 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_payment_terms(self, row):
|
||||
# build payment_terms for row
|
||||
si = frappe.qb.DocType(row.voucher_type)
|
||||
ps = frappe.qb.DocType("Payment Schedule")
|
||||
payment_terms_details = (
|
||||
frappe.qb.from_(si)
|
||||
.inner_join(ps)
|
||||
.on(si.name == ps.parent)
|
||||
.select(
|
||||
si.name,
|
||||
si.party_account_currency,
|
||||
si.currency,
|
||||
si.conversion_rate,
|
||||
si.total_advance,
|
||||
ps.due_date,
|
||||
ps.payment_term,
|
||||
ps.payment_amount,
|
||||
ps.base_payment_amount,
|
||||
ps.description,
|
||||
ps.paid_amount,
|
||||
ps.base_paid_amount,
|
||||
ps.discounted_amount,
|
||||
)
|
||||
.where((ps.parenttype == row.voucher_type) & (si.name == row.voucher_no) & (si.is_return == 0))
|
||||
.orderby(ps.paid_amount, order=frappe.qb.desc)
|
||||
.orderby(ps.due_date)
|
||||
.run(as_dict=1)
|
||||
# nosemgrep
|
||||
payment_terms_details = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
si.name, si.party_account_currency, si.currency, si.conversion_rate,
|
||||
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
|
||||
ps.description, ps.paid_amount, ps.base_paid_amount, ps.discounted_amount
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
|
||||
si.name = %s and
|
||||
si.is_return = 0
|
||||
order by ps.paid_amount desc, due_date
|
||||
""",
|
||||
row.voucher_no,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
original_row = frappe._dict(row)
|
||||
@@ -655,6 +661,7 @@ class ReceivablePayableReport:
|
||||
def get_future_payments_from_payment_entry(self):
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
return (
|
||||
frappe.qb.from_(pe)
|
||||
@@ -667,14 +674,11 @@ class ReceivablePayableReport:
|
||||
(pe.posting_date).as_("future_date"),
|
||||
(pe_ref.allocated_amount).as_("future_amount"),
|
||||
(pe.reference_no).as_("future_ref"),
|
||||
# CASE is portable; MySQL's IF() does not exist on postgres
|
||||
query_builder.Case()
|
||||
.when(
|
||||
ifelse(
|
||||
pe.payment_type == "Receive",
|
||||
pe.source_exchange_rate * pe_ref.allocated_amount,
|
||||
)
|
||||
.else_(pe.target_exchange_rate * pe_ref.allocated_amount)
|
||||
.as_("future_amount_in_base_currency"),
|
||||
pe.target_exchange_rate * pe_ref.allocated_amount,
|
||||
).as_("future_amount_in_base_currency"),
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus < 2)
|
||||
@@ -691,13 +695,11 @@ class ReceivablePayableReport:
|
||||
.inner_join(jea)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
# Sum() below makes this an implicit aggregate (no GROUP BY); the non-aggregated columns
|
||||
# are arbitrary per the single group on MySQL -> Max() keeps it valid on postgres.
|
||||
Max(jea.reference_name).as_("invoice_no"),
|
||||
Max(jea.party).as_("party"),
|
||||
Max(jea.party_type).as_("party_type"),
|
||||
Max(je.posting_date).as_("future_date"),
|
||||
Max(je.cheque_no).as_("future_ref"),
|
||||
jea.reference_name.as_("invoice_no"),
|
||||
jea.party,
|
||||
jea.party_type,
|
||||
je.posting_date.as_("future_date"),
|
||||
je.cheque_no.as_("future_ref"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus < 2)
|
||||
@@ -710,25 +712,30 @@ class ReceivablePayableReport:
|
||||
|
||||
if self.filters.get("party"):
|
||||
if self.account_type == "Payable":
|
||||
future_amount = Sum(jea.debit_in_account_currency - jea.credit_in_account_currency)
|
||||
future_amount_in_base_currency = Sum(jea.debit - jea.credit)
|
||||
query = query.select(
|
||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency"))
|
||||
else:
|
||||
future_amount = Sum(jea.credit_in_account_currency - jea.debit_in_account_currency)
|
||||
future_amount_in_base_currency = Sum(jea.credit - jea.debit)
|
||||
query = query.select(
|
||||
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency"))
|
||||
else:
|
||||
future_amount_in_base_currency = Sum(jea.debit if self.account_type == "Payable" else jea.credit)
|
||||
future_amount = Sum(
|
||||
jea.debit_in_account_currency
|
||||
if self.account_type == "Payable"
|
||||
else jea.credit_in_account_currency
|
||||
query = query.select(
|
||||
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_(
|
||||
"future_amount_in_base_currency"
|
||||
)
|
||||
)
|
||||
query = query.select(
|
||||
Sum(
|
||||
jea.debit_in_account_currency
|
||||
if self.account_type == "Payable"
|
||||
else jea.credit_in_account_currency
|
||||
).as_("future_amount")
|
||||
)
|
||||
|
||||
query = query.select(
|
||||
future_amount.as_("future_amount"),
|
||||
future_amount_in_base_currency.as_("future_amount_in_base_currency"),
|
||||
)
|
||||
# use the aggregate expression in HAVING; postgres can't reference a SELECT alias there
|
||||
query = query.having(future_amount > 0)
|
||||
query = query.having(qb.Field("future_amount") > 0)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def allocate_future_payments(self, row):
|
||||
@@ -884,19 +891,16 @@ class ReceivablePayableReport:
|
||||
if self.filters.get("sales_person"):
|
||||
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
|
||||
|
||||
steam = frappe.qb.DocType("Sales Team")
|
||||
sp = frappe.qb.DocType("Sales Person")
|
||||
records = (
|
||||
frappe.qb.from_(steam)
|
||||
.select(steam.parent, steam.parenttype)
|
||||
.distinct()
|
||||
.where(
|
||||
steam.parenttype.isin(["Customer", "Sales Invoice"])
|
||||
& steam.sales_person.isin(
|
||||
frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt))
|
||||
)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
# nosemgrep
|
||||
records = frappe.db.sql(
|
||||
"""
|
||||
select distinct parent, parenttype
|
||||
from `tabSales Team` steam
|
||||
where parenttype in ('Customer', 'Sales Invoice')
|
||||
and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
|
||||
""",
|
||||
(lft, rgt),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.sales_person_records = frappe._dict()
|
||||
|
||||
@@ -12,17 +12,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.company_abbr = "_TC"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_usd_receivable_account()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -11,11 +11,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def test_01_receivable_summary_output(self):
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,10 @@ def execute(filters=None):
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
depreciation_accounts = frappe.get_all("Account", filters={"account_type": "Depreciation"}, pluck="name")
|
||||
depreciation_accounts = frappe.db.sql_list(
|
||||
""" select name from tabAccount
|
||||
where ifnull(account_type, '') = 'Depreciation' """
|
||||
)
|
||||
|
||||
filters_data = [
|
||||
["company", "=", filters.get("company")],
|
||||
@@ -30,8 +33,10 @@ def get_data(filters):
|
||||
filters_data.append(["against_voucher", "=", filters.get("asset")])
|
||||
|
||||
if filters.get("asset_category"):
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"asset_category": filters.get("asset_category"), "docstatus": 1}, pluck="name"
|
||||
assets = frappe.db.sql_list(
|
||||
"""select name from tabAsset
|
||||
where asset_category = %s and docstatus=1""",
|
||||
filters.get("asset_category"),
|
||||
)
|
||||
|
||||
filters_data.append(["against_voucher", "in", assets])
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.report.asset_depreciation_ledger.asset_depreciation_ledger import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAssetDepreciationLedger(ERPNextTestSuite):
|
||||
def test_report_executes(self):
|
||||
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
|
||||
# both MariaDB and postgres.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
columns, *_rest = execute(
|
||||
frappe._dict({"company": company, "from_date": "2020-01-01", "to_date": "2030-12-31"})
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.custom import MonthName
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.utils import add_months, flt, formatdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -113,6 +113,7 @@ def build_budget_map(budget_records, filters):
|
||||
|
||||
def get_actual_transactions(dimension_name, filters):
|
||||
budget_against = frappe.scrub(filters.get("budget_against"))
|
||||
monthname = CustomFunction("MONTHNAME", ["date"])
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
budget = frappe.qb.DocType("Budget")
|
||||
@@ -125,7 +126,7 @@ def get_actual_transactions(dimension_name, filters):
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.fiscal_year,
|
||||
MonthName(gle.posting_date).as_("month_name"),
|
||||
monthname(gle.posting_date).as_("month_name"),
|
||||
budget[budget_against].as_("budget_against"),
|
||||
)
|
||||
.where(
|
||||
@@ -136,10 +137,7 @@ def get_actual_transactions(dimension_name, filters):
|
||||
& (gle.is_cancelled == 0)
|
||||
& (budget[budget_against] == dimension_name)
|
||||
)
|
||||
# budget[budget_against] is selected from the Budget table, which is not functionally
|
||||
# dependent on the grouped GL Entry PK, so postgres requires it in the GROUP BY. The WHERE
|
||||
# pins it to dimension_name (a constant), so grouping by it does not change the result.
|
||||
.groupby(gle.name, budget[budget_against])
|
||||
.groupby(gle.name)
|
||||
.orderby(gle.fiscal_year)
|
||||
)
|
||||
|
||||
@@ -159,11 +157,15 @@ def get_actual_transactions(dimension_name, filters):
|
||||
|
||||
|
||||
def get_budget_distributions(budget):
|
||||
return frappe.get_all(
|
||||
"Budget Distribution",
|
||||
filters={"parent": budget.name},
|
||||
fields=["start_date", "end_date", "amount", "percent"],
|
||||
order_by="start_date asc",
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT start_date, end_date, amount, percent
|
||||
FROM `tabBudget Distribution`
|
||||
WHERE parent = %s
|
||||
ORDER BY start_date ASC
|
||||
""",
|
||||
(budget.name,),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -349,16 +351,20 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_fiscal_years(filters):
|
||||
return frappe.get_all(
|
||||
"Fiscal Year",
|
||||
filters={"name": ["between", [filters["from_fiscal_year"], filters["to_fiscal_year"]]]},
|
||||
fields=["name"],
|
||||
# the raw query had no ORDER BY (de-facto oldest-first); get_all would otherwise apply the
|
||||
# Fiscal Year doctype default (name DESC) and reverse column order / cumulative-mode values.
|
||||
order_by="name asc",
|
||||
as_list=True,
|
||||
fiscal_year = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name
|
||||
from
|
||||
`tabFiscal Year`
|
||||
where
|
||||
name between %(from_fiscal_year)s and %(to_fiscal_year)s
|
||||
""",
|
||||
{"from_fiscal_year": filters["from_fiscal_year"], "to_fiscal_year": filters["to_fiscal_year"]},
|
||||
)
|
||||
|
||||
return fiscal_year
|
||||
|
||||
|
||||
def get_cost_center_with_children(cost_centers):
|
||||
"""Expand each cost center to include itself and all its descendants."""
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.report.budget_variance_report.budget_variance_report import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestBudgetVarianceReport(ERPNextTestSuite):
|
||||
def test_report_executes(self):
|
||||
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
|
||||
# both MariaDB and postgres.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc")
|
||||
columns, *_rest = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_fiscal_year": fy,
|
||||
"to_fiscal_year": fy,
|
||||
"period": "Yearly",
|
||||
"budget_against": "Cost Center",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
@@ -7,7 +7,6 @@ from datetime import timedelta
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cstr, flt
|
||||
from pypika import Order
|
||||
|
||||
@@ -214,43 +213,37 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
|
||||
|
||||
|
||||
def get_account_type_based_gl_data(company, filters=None):
|
||||
cond = ""
|
||||
filters = frappe._dict(filters or {})
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
account = frappe.qb.DocType("Account")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(Sum(gle.credit) - Sum(gle.debit))
|
||||
.where(
|
||||
(gle.company == company)
|
||||
& (gle.posting_date >= filters.start_date)
|
||||
& (gle.posting_date <= filters.end_date)
|
||||
& (gle.voucher_type != "Period Closing Voucher")
|
||||
& gle.account.isin(
|
||||
frappe.qb.from_(account)
|
||||
.select(account.name)
|
||||
.where(account.account_type == filters.account_type)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if filters.include_default_book_entries:
|
||||
company_fb = frappe.get_cached_value("Company", company, "default_finance_book")
|
||||
query = query.where(
|
||||
gle.finance_book.isin([filters.finance_book, company_fb, ""]) | gle.finance_book.isnull()
|
||||
cond = """ AND (finance_book in ({}, {}, '') OR finance_book IS NULL)
|
||||
""".format(
|
||||
frappe.db.escape(filters.finance_book),
|
||||
frappe.db.escape(company_fb),
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
gle.finance_book.isin([cstr(filters.finance_book), ""]) | gle.finance_book.isnull()
|
||||
cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" % (
|
||||
frappe.db.escape(cstr(filters.finance_book))
|
||||
)
|
||||
|
||||
if filters.get("cost_center"):
|
||||
cost_centers = get_cost_centers_with_children(filters.cost_center)
|
||||
query = query.where(gle.cost_center.isin(cost_centers))
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
cond += " and cost_center in %(cost_center)s"
|
||||
|
||||
gl_sum = query.run()
|
||||
return gl_sum[0][0] if gl_sum and gl_sum[0][0] else 0
|
||||
gl_sum = frappe.db.sql_list(
|
||||
f"""
|
||||
select sum(credit) - sum(debit)
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
|
||||
and voucher_type != 'Period Closing Voucher'
|
||||
and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
|
||||
""",
|
||||
filters,
|
||||
)
|
||||
|
||||
return gl_sum[0] if gl_sum and gl_sum[0] else 0
|
||||
|
||||
|
||||
def get_start_date(period, accumulated_values, company):
|
||||
@@ -374,10 +367,11 @@ def get_net_income(company, period_list, filters):
|
||||
from_date, to_date = get_opening_range_using_fiscal_year(company, period_list)
|
||||
|
||||
for root_type in ["Income", "Expense"]:
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
for root in frappe.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
company,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.report.cash_flow.cash_flow import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCashFlow(ERPNextTestSuite):
|
||||
def test_report_executes(self):
|
||||
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
|
||||
# both MariaDB and postgres.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc")
|
||||
columns, *_rest = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_fiscal_year": fy,
|
||||
"to_fiscal_year": fy,
|
||||
"filter_based_on": "Fiscal Year",
|
||||
"periodicity": "Yearly",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
pe = qb.DocType("Payment Entry")
|
||||
doctype_name = ConstantColumn("Payment Entry")
|
||||
payments = (
|
||||
@@ -100,10 +101,7 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
Case()
|
||||
.when(pe.paid_from.eq(filters.account), pe.paid_amount)
|
||||
.else_(pe.received_amount)
|
||||
.as_("amount"),
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.report.cheques_and_deposits_incorrectly_cleared.cheques_and_deposits_incorrectly_cleared import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestChequesAndDepositsIncorrectlyCleared(ERPNextTestSuite):
|
||||
def test_report_executes_with_case_amount(self):
|
||||
# Exercises the Payment Entry branch whose amount column uses a db-aware CASE expression
|
||||
# (previously a MySQL-only IF()). IF() does not compile on postgres, so running the report
|
||||
# query guards the portability fix on both databases.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
account = frappe.db.get_value(
|
||||
"Account", {"account_type": "Bank", "company": company, "is_group": 0}, "name"
|
||||
)
|
||||
columns, data = execute(frappe._dict({"account": account, "report_date": nowdate()}))
|
||||
self.assertTrue(columns)
|
||||
self.assertIsInstance(data, list)
|
||||
@@ -347,10 +347,11 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
|
||||
filters.end_date = end_date
|
||||
|
||||
gl_entries_by_account = {}
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
for root in frappe.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
start_date,
|
||||
@@ -511,11 +512,9 @@ def get_companies(filters):
|
||||
def get_subsidiary_companies(company):
|
||||
lft, rgt = frappe.get_cached_value("Company", company, ["lft", "rgt"])
|
||||
|
||||
return frappe.get_all(
|
||||
"Company",
|
||||
filters={"lft": [">=", lft], "rgt": ["<=", rgt]},
|
||||
pluck="name",
|
||||
order_by="lft, rgt",
|
||||
return frappe.db.sql_list(
|
||||
f"""select name from `tabCompany`
|
||||
where lft >= {lft} and rgt <= {rgt} order by lft, rgt"""
|
||||
)
|
||||
|
||||
|
||||
@@ -605,10 +604,14 @@ def set_gl_entries_by_account(
|
||||
|
||||
company_lft, company_rgt = frappe.get_cached_value("Company", filters.get("company"), ["lft", "rgt"])
|
||||
|
||||
companies = frappe.get_all(
|
||||
"Company",
|
||||
filters={"lft": [">=", company_lft], "rgt": ["<=", company_rgt]},
|
||||
fields=["name", "default_currency"],
|
||||
companies = frappe.db.sql(
|
||||
""" select name, default_currency from `tabCompany`
|
||||
where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
|
||||
{
|
||||
"company_lft": company_lft,
|
||||
"company_rgt": company_rgt,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
currency_info = frappe._dict(
|
||||
|
||||
@@ -126,22 +126,12 @@ def get_data(filters) -> list[list]:
|
||||
|
||||
|
||||
def get_company_wise_tb_data(filters, reporting_currency, ignore_reporting_currency):
|
||||
accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[
|
||||
"name",
|
||||
"account_number",
|
||||
"parent_account",
|
||||
"account_name",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_type",
|
||||
"is_group",
|
||||
"lft",
|
||||
"rgt",
|
||||
],
|
||||
order_by="lft",
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, account_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
@@ -11,12 +11,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False, **args):
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import functions
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder import Column, functions
|
||||
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, getdate, rounded
|
||||
|
||||
from erpnext.accounts.report.financial_statements import get_period_list
|
||||
@@ -301,10 +300,8 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
Get all sales and purchase invoices which has deferred revenue/expense items
|
||||
"""
|
||||
gle = qb.DocType("GL Entry")
|
||||
# a literal marker: real GL rows are "posted" (dummy/simulated future entries use "not").
|
||||
# ConstantColumn renders a single-quoted string literal, valid on both backends -- a plain
|
||||
# Column rendered as "posted", which MySQL reads as the string but postgres as an identifier.
|
||||
posted = ConstantColumn("posted").as_("posted")
|
||||
# column doesn't have an alias option
|
||||
posted = Column("posted")
|
||||
|
||||
if self.filters.type == "Revenue":
|
||||
inv = qb.DocType("Sales Invoice")
|
||||
@@ -330,15 +327,13 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
)
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
# non-grouped columns are constant per grouped invoice / invoice item -> Max() keeps the
|
||||
# GROUP BY valid on postgres while returning the same value MySQL picked.
|
||||
functions.Max(inv.posting_date).as_("posting_date"),
|
||||
inv.posting_date,
|
||||
inv_item.name.as_("item"),
|
||||
functions.Max(inv_item.item_name).as_("item_name"),
|
||||
functions.Max(inv_item.service_start_date).as_("service_start_date"),
|
||||
functions.Max(inv_item.service_end_date).as_("service_end_date"),
|
||||
functions.Max(inv_item.base_net_amount).as_("base_net_amount"),
|
||||
functions.Max(deferred_account_field).as_(deferred_account_field.name),
|
||||
inv_item.item_name,
|
||||
inv_item.service_start_date,
|
||||
inv_item.service_end_date,
|
||||
inv_item.base_net_amount,
|
||||
deferred_account_field,
|
||||
gle.posting_date.as_("gle_posting_date"),
|
||||
functions.Sum(gle.debit).as_("debit"),
|
||||
functions.Sum(gle.credit).as_("credit"),
|
||||
|
||||
@@ -61,16 +61,11 @@ class TestDeferredRevenueAndExpense(ERPNextTestSuite, AccountsTestMixin):
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.company_abbr = "_TC"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.create_company()
|
||||
self.create_customer("_Test Customer")
|
||||
self.create_supplier("_Test Furniture Supplier")
|
||||
self.setup_deferred_accounts_and_items()
|
||||
self.clear_old_entries()
|
||||
|
||||
@ERPNextTestSuite.change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||
def test_deferred_revenue(self):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
@@ -31,23 +31,18 @@ def execute(filters=None):
|
||||
def get_data(filters, dimension_list):
|
||||
company_currency = erpnext.get_company_currency(filters.company)
|
||||
|
||||
acc = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[
|
||||
"name",
|
||||
"account_number",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"include_in_gross",
|
||||
"account_type",
|
||||
"is_group",
|
||||
],
|
||||
order_by="lft",
|
||||
acc = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, account_number, parent_account, lft, rgt, root_type,
|
||||
report_type, account_name, include_in_gross, account_type, is_group
|
||||
from
|
||||
`tabAccount`
|
||||
where
|
||||
company=%s
|
||||
order by lft""",
|
||||
(filters.company),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not acc:
|
||||
@@ -55,17 +50,16 @@ def get_data(filters, dimension_list):
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
|
||||
|
||||
lft_rgt = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[{"MIN": "lft", "as": "min_lft"}, {"MAX": "rgt", "as": "max_rgt"}],
|
||||
min_lft, max_rgt = frappe.db.sql(
|
||||
"""select min(lft), max(rgt) from `tabAccount`
|
||||
where company=%s""",
|
||||
(filters.company),
|
||||
)[0]
|
||||
min_lft, max_rgt = lft_rgt.min_lft, lft_rgt.max_rgt
|
||||
|
||||
account = frappe.get_all(
|
||||
"Account",
|
||||
filters={"lft": [">=", min_lft], "rgt": ["<=", max_rgt], "company": filters.company},
|
||||
pluck="name",
|
||||
account = frappe.db.sql_list(
|
||||
"""select name from `tabAccount`
|
||||
where lft >= %s and rgt <= %s and company = %s""",
|
||||
(min_lft, max_rgt, filters.company),
|
||||
)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
@@ -81,34 +75,42 @@ def get_data(filters, dimension_list):
|
||||
|
||||
|
||||
def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account):
|
||||
dimension_field = frappe.scrub(filters.get("dimension"))
|
||||
condition = get_condition(filters.get("dimension"))
|
||||
|
||||
if account:
|
||||
condition += " and account in ({})".format(", ".join([frappe.db.escape(d) for d in account]))
|
||||
|
||||
gl_filters = {
|
||||
"company": filters.get("company"),
|
||||
dimension_field: ["in", list(set(dimension_list))],
|
||||
"posting_date": ["between", [filters.get("from_date"), filters.get("to_date")]],
|
||||
"is_cancelled": 0,
|
||||
"from_date": filters.get("from_date"),
|
||||
"to_date": filters.get("to_date"),
|
||||
"finance_book": cstr(filters.get("finance_book")),
|
||||
}
|
||||
if account:
|
||||
gl_filters["account"] = ["in", account]
|
||||
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters=gl_filters,
|
||||
fields=[
|
||||
"posting_date",
|
||||
"account",
|
||||
dimension_field,
|
||||
"debit",
|
||||
"credit",
|
||||
"is_opening",
|
||||
"fiscal_year",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
"account_currency",
|
||||
],
|
||||
order_by="account, posting_date",
|
||||
)
|
||||
gl_filters["dimensions"] = tuple(set(dimension_list))
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year,
|
||||
debit_in_account_currency, credit_in_account_currency, account_currency
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
company=%(company)s
|
||||
{condition}
|
||||
and posting_date >= %(from_date)s
|
||||
and posting_date <= %(to_date)s
|
||||
and is_cancelled = 0
|
||||
order by account, posting_date""".format(
|
||||
dimension=frappe.scrub(filters.get("dimension")), condition=condition
|
||||
),
|
||||
gl_filters,
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
|
||||
for entry in gl_entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
@@ -176,6 +178,14 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
|
||||
].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0)
|
||||
|
||||
|
||||
def get_condition(dimension):
|
||||
conditions = []
|
||||
|
||||
conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s")
|
||||
|
||||
return " and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
|
||||
def get_dimensions(filters):
|
||||
meta = frappe.get_meta(filters.get("dimension"), cached=False)
|
||||
query_filters = {}
|
||||
|
||||
@@ -71,7 +71,6 @@ def get_ratios_data(filters, period_list, years):
|
||||
assets, liabilities, income, expense = get_gl_data(filters, period_list, years)
|
||||
|
||||
current_asset, total_asset = {}, {}
|
||||
fixed_asset = {}
|
||||
current_liability, total_liability = {}, {}
|
||||
net_sales, total_income = {}, {}
|
||||
cogs, total_expense = {}, {}
|
||||
@@ -94,7 +93,6 @@ def get_ratios_data(filters, period_list, years):
|
||||
quick_asset,
|
||||
total_quick_asset,
|
||||
],
|
||||
[fixed_asset, total_asset, "Fixed Asset", year, assets, "Asset", {}, 0],
|
||||
[
|
||||
current_liability,
|
||||
total_liability,
|
||||
@@ -114,7 +112,7 @@ def get_ratios_data(filters, period_list, years):
|
||||
add_solvency_ratios(
|
||||
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||||
)
|
||||
add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense)
|
||||
add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense)
|
||||
|
||||
return data
|
||||
|
||||
@@ -195,7 +193,7 @@ def add_solvency_ratios(
|
||||
data.append(return_on_equity_ratio)
|
||||
|
||||
|
||||
def add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense):
|
||||
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": _("Turnover Ratios")})
|
||||
|
||||
@@ -210,7 +208,7 @@ def add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sale
|
||||
)
|
||||
|
||||
ratio_data = [
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, fixed_asset],
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
|
||||
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
|
||||
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
|
||||
[_("Inventory Turnover Ratio"), cogs, avg_stock],
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.report.financial_ratios.financial_ratios import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestFinancialRatios(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.abbr = "_TC"
|
||||
# The report matches the group accounts by their account_type, which the
|
||||
# standard chart of accounts does not set on group accounts by default.
|
||||
self.set_account_type("Fixed Assets", "Fixed Asset")
|
||||
self.set_account_type("Direct Income", "Direct Income")
|
||||
|
||||
def set_account_type(self, account_name, account_type):
|
||||
frappe.db.set_value("Account", f"{account_name} - {self.abbr}", "account_type", account_type)
|
||||
|
||||
def test_fixed_asset_turnover_uses_net_fixed_assets(self):
|
||||
# Acquire a fixed asset worth 10,000 funded by equity.
|
||||
self.make_journal_entry("Buildings", "Capital Stock", 10000)
|
||||
# Book sales of 20,000 collected in cash. Total assets now = 30,000
|
||||
# (Buildings 10,000 + Cash 20,000), while net fixed assets stay at 10,000.
|
||||
self.make_journal_entry("Cash", "Sales", 20000)
|
||||
|
||||
columns, data = execute(self.get_report_filters())
|
||||
year_key = columns[1]["fieldname"]
|
||||
ratio_row = next((row for row in data if row.get("ratio") == "Fixed Asset Turnover Ratio"), None)
|
||||
self.assertIsNotNone(ratio_row, "Fixed Asset Turnover Ratio row not found in report output")
|
||||
|
||||
# Net Sales / Net Fixed Assets = 20,000 / 10,000 = 2.0
|
||||
# (the old behaviour divided by total assets, giving 20,000 / 30,000 = 0.667)
|
||||
self.assertEqual(ratio_row[year_key], 2.0)
|
||||
|
||||
def get_report_filters(self):
|
||||
active_fy = frappe.db.get_value(
|
||||
"Fiscal Year",
|
||||
{"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())},
|
||||
["name", "year_start_date", "year_end_date"],
|
||||
as_dict=True,
|
||||
)
|
||||
return frappe._dict(
|
||||
company=self.company,
|
||||
from_fiscal_year=active_fy.name,
|
||||
to_fiscal_year=active_fy.name,
|
||||
period_start_date=active_fy.year_start_date,
|
||||
period_end_date=active_fy.year_end_date,
|
||||
filter_based_on="Fiscal Year",
|
||||
periodicity="Yearly",
|
||||
)
|
||||
|
||||
def make_journal_entry(self, debit_account, credit_account, amount):
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.posting_date = today()
|
||||
journal_entry.company = self.company
|
||||
for account, debit, credit in (
|
||||
(debit_account, amount, 0),
|
||||
(credit_account, 0, amount),
|
||||
):
|
||||
journal_entry.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": f"{account} - {self.abbr}",
|
||||
"debit_in_account_currency": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
},
|
||||
)
|
||||
journal_entry.insert()
|
||||
journal_entry.submit()
|
||||
@@ -179,10 +179,11 @@ def get_data(
|
||||
company_currency = get_appropriate_currency(company, filters)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
for root in frappe.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
company,
|
||||
@@ -372,23 +373,13 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
|
||||
|
||||
|
||||
def get_accounts(company, root_type):
|
||||
return frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": company, "root_type": root_type},
|
||||
fields=[
|
||||
"name",
|
||||
"account_number",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"include_in_gross",
|
||||
"account_type",
|
||||
"is_group",
|
||||
],
|
||||
order_by="lft",
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group, lft, rgt
|
||||
from `tabAccount`
|
||||
where company=%s and root_type=%s order by lft""",
|
||||
(company, root_type),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -538,11 +529,7 @@ def get_accounting_entries(
|
||||
gl_entry.credit_in_account_currency
|
||||
if not group_by_account
|
||||
else Sum(gl_entry.credit_in_account_currency).as_("credit_in_account_currency"),
|
||||
# when grouping by account the non-aggregated columns must be aggregated for postgres;
|
||||
# account_currency is constant per account so Max() returns the same value.
|
||||
gl_entry.account_currency
|
||||
if not group_by_account
|
||||
else Max(gl_entry.account_currency).as_("account_currency"),
|
||||
gl_entry.account_currency,
|
||||
)
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
@@ -560,29 +547,15 @@ def get_accounting_entries(
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
if doctype == "GL Entry":
|
||||
# aggregate the non-grouped columns when grouping by account (postgres requirement)
|
||||
if group_by_account:
|
||||
query = query.select(
|
||||
Max(gl_entry.posting_date).as_("posting_date"),
|
||||
Max(gl_entry.is_opening).as_("is_opening"),
|
||||
Max(gl_entry.fiscal_year).as_("fiscal_year"),
|
||||
)
|
||||
else:
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
query = query.where(gl_entry.is_cancelled == 0)
|
||||
query = query.where(gl_entry.posting_date <= to_date)
|
||||
# FORCE INDEX is MySQL-only; postgres has no index hints (its planner uses the index anyway)
|
||||
if frappe.db.db_type != "postgres":
|
||||
query = query.force_index("posting_date_company_index")
|
||||
query = query.force_index("posting_date_company_index")
|
||||
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
query = query.where(gl_entry.is_opening == "No")
|
||||
else:
|
||||
query = query.select(
|
||||
Max(gl_entry.closing_date).as_("posting_date")
|
||||
if group_by_account
|
||||
else gl_entry.closing_date.as_("posting_date")
|
||||
)
|
||||
query = query.select(gl_entry.closing_date.as_("posting_date"))
|
||||
query = query.where(gl_entry.period_closing_voucher == period_closing_voucher)
|
||||
|
||||
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
|
||||
|
||||
@@ -12,13 +12,7 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestGeneralAndPaymentLedger(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.creditors = "Creditors - _TC"
|
||||
self.create_company()
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
|
||||
@@ -35,7 +35,7 @@ def execute(filters=None):
|
||||
if filters and filters.get("print_in_account_currency") and not filters.get("account"):
|
||||
frappe.throw(_("Select an account to print in account currency"))
|
||||
|
||||
for acc in frappe.get_all("Account", fields=["name", "is_group"]):
|
||||
for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1):
|
||||
account_details.setdefault(acc.name, acc)
|
||||
|
||||
if filters.get("party"):
|
||||
@@ -650,8 +650,10 @@ def get_result_as_list(data, filters):
|
||||
|
||||
def get_supplier_invoice_details():
|
||||
inv_details = {}
|
||||
for d in frappe.get_all(
|
||||
"Purchase Invoice", filters={"docstatus": 1, "bill_no": ["is", "set"]}, fields=["name", "bill_no"]
|
||||
for d in frappe.db.sql(
|
||||
""" select name, bill_no from `tabPurchase Invoice`
|
||||
where docstatus = 1 and bill_no is not null and bill_no != '' """,
|
||||
as_dict=1,
|
||||
):
|
||||
inv_details[d.name] = d.bill_no
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class TestGeneralLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.clear_old_entries()
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
|
||||
@@ -713,25 +713,20 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
def get_returned_invoice_items(self):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
si_item = frappe.qb.DocType("Sales Invoice Item")
|
||||
returned_invoices = (
|
||||
frappe.qb.from_(si)
|
||||
.inner_join(si_item)
|
||||
.on(si.name == si_item.parent)
|
||||
.select(
|
||||
si.name,
|
||||
si_item.item_code,
|
||||
si_item.stock_qty.as_("qty"),
|
||||
si_item.base_net_amount.as_("base_amount"),
|
||||
si.return_against,
|
||||
)
|
||||
.where(
|
||||
(si.docstatus == 1)
|
||||
& (si.is_return == 1)
|
||||
& si.posting_date.between(self.filters.from_date, self.filters.to_date)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
returned_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against
|
||||
from
|
||||
`tabSales Invoice` si, `tabSales Invoice Item` si_item
|
||||
where
|
||||
si.name = si_item.parent
|
||||
and si.docstatus = 1
|
||||
and si.is_return = 1
|
||||
and si.posting_date between %(from_date)s and %(to_date)s
|
||||
""",
|
||||
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.returned_invoices = frappe._dict()
|
||||
@@ -1246,4 +1241,7 @@ class GrossProfitGenerator:
|
||||
).setdefault(d.parent_item, []).append(d)
|
||||
|
||||
def load_non_stock_items(self):
|
||||
self.non_stock_items = frappe.get_all("Item", filters={"is_stock_item": 0}, pluck="name")
|
||||
self.non_stock_items = frappe.db.sql_list(
|
||||
"""select name from tabItem
|
||||
where is_stock_item=0"""
|
||||
)
|
||||
|
||||
@@ -14,17 +14,73 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestGrossProfit(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.finished_warehouse = "Finished Goods - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.item = "_Test Item"
|
||||
self.item2 = "_Test Item Home Desktop 100"
|
||||
self.bundle = "_Test Product Bundle Item"
|
||||
self.customer = "_Test Customer"
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_bundle()
|
||||
self.create_customer()
|
||||
self.create_sales_invoice()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Gross Profit"
|
||||
abbr = "_GP"
|
||||
if frappe.db.exists("Company", company_name):
|
||||
company = frappe.get_doc("Company", company_name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company_name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "Stores - " + abbr
|
||||
self.finished_warehouse = "Finished Goods - " + abbr
|
||||
self.income_account = "Sales - " + abbr
|
||||
self.expense_account = "Cost of Goods Sold - " + abbr
|
||||
self.debit_to = "Debtors - " + abbr
|
||||
self.creditors = "Creditors - " + abbr
|
||||
|
||||
def create_item(self):
|
||||
item = create_item(
|
||||
item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_bundle(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
|
||||
item2 = create_item(
|
||||
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item2 = item2 if isinstance(item2, str) else item2.item_code
|
||||
|
||||
# This will be parent item
|
||||
bundle = create_item(
|
||||
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
|
||||
|
||||
# Create Product Bundle
|
||||
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
|
||||
|
||||
def create_customer(self):
|
||||
name = "_Test GP Customer"
|
||||
if frappe.db.exists("Customer", name):
|
||||
self.customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
@@ -158,7 +214,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 150.0,
|
||||
@@ -187,7 +243,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 100.0,
|
||||
@@ -219,7 +275,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"item_code": self.item2,
|
||||
"s_warehouse": "",
|
||||
"t_warehouse": self.finished_warehouse,
|
||||
"qty": 2,
|
||||
"qty": 1,
|
||||
"basic_rate": 100,
|
||||
"conversion_factor": item.conversion_factor or 1.0,
|
||||
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
||||
@@ -319,7 +375,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 4.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 125.0,
|
||||
@@ -360,10 +416,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 100.0,
|
||||
"avg._selling_rate": 100,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": 0.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": 0.0,
|
||||
@@ -383,7 +439,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"""
|
||||
# Make Cr Note
|
||||
sinv = self.create_sales_invoice(
|
||||
qty=-1, rate=200, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
sinv.is_return = 1
|
||||
sinv.items[0].allow_zero_valuation_rate = 1
|
||||
@@ -406,14 +462,14 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": -1.0,
|
||||
"avg._selling_rate": 200.0,
|
||||
"valuation_rate": 100.0,
|
||||
"selling_amount": -200.0,
|
||||
"buying_amount": -100.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": -50.0,
|
||||
"gross_profit_%": -100.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry}
|
||||
@@ -499,7 +555,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 4.0,
|
||||
"avg._selling_rate": 800.0,
|
||||
"valuation_rate": 700.0,
|
||||
@@ -562,7 +618,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
def test_gross_profit_groupby_invoices(self):
|
||||
create_sales_invoice(
|
||||
qty=1,
|
||||
rate=200,
|
||||
rate=100,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
@@ -584,10 +640,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 200.0)
|
||||
self.assertEqual(total.buying_amount, 100.0)
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 50.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
@@ -596,7 +652,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
return_inv_date = add_days(month_end_date, 1)
|
||||
|
||||
# create sales invoice on month start date
|
||||
sinv = self.create_sales_invoice(qty=1, rate=200, do_not_save=True, do_not_submit=True)
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = sales_inv_date
|
||||
sinv.save().submit()
|
||||
@@ -615,10 +671,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 200.0)
|
||||
self.assertEqual(total.buying_amount, 100.0)
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 50.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update({"to_date": return_inv_date})
|
||||
@@ -636,10 +692,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, -200.0)
|
||||
self.assertEqual(total.buying_amount, -100.0)
|
||||
self.assertEqual(total.selling_amount, -100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, -100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), -50.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), -100.0)
|
||||
|
||||
def test_sales_person_wise_gross_profit(self):
|
||||
sales_person = make_sales_person("_Test Sales Person")
|
||||
@@ -670,10 +726,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total[5], 1000.0) # selling amount
|
||||
self.assertEqual(total[6], 1000.0) # buying amount
|
||||
self.assertEqual(total[7], 0.0) # gross profit
|
||||
self.assertEqual(total[8], 0.0) # gross profit %
|
||||
self.assertEqual(total[5], 1000.0)
|
||||
self.assertEqual(total[6], 0.0)
|
||||
self.assertEqual(total[7], 1000.0)
|
||||
self.assertEqual(total[8], 100.0)
|
||||
|
||||
def test_drop_ship(self):
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import CurDate, DateDiff
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@@ -102,11 +102,11 @@ def get_sales_details(filters):
|
||||
child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item"
|
||||
child = frappe.qb.DocType(child_doctype)
|
||||
|
||||
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
|
||||
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
|
||||
current_date = CustomFunction("CURRENT_DATE", [])
|
||||
|
||||
# DateDiff is cross-database (DATEDIFF on MariaDB, date subtraction on postgres); CurDate()
|
||||
# renders the bare CURRENT_DATE keyword. Yields the integer number of days.
|
||||
days_since_last_order = DateDiff(CurDate(), date_col)
|
||||
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
|
||||
days_since_last_order = date_diff(current_date(), date_col)
|
||||
|
||||
sales_data = (
|
||||
frappe.qb.from_(parent)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.report.inactive_sales_items.inactive_sales_items import execute
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestInactiveSalesItems(ERPNextTestSuite):
|
||||
def test_days_since_last_order_is_computed(self):
|
||||
# Exercises the date-arithmetic path (DATEDIFF/CURRENT_DATE on mariadb, date subtraction on
|
||||
# postgres) which must produce the same integer day count on both databases.
|
||||
item = make_item("_Test Inactive Sales Item").name
|
||||
old_date = add_days(today(), -120)
|
||||
so = make_sales_order(item=item, qty=3, rate=150, transaction_date=old_date)
|
||||
so.items[0].delivery_date = add_days(old_date, 7)
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
columns, data = execute(frappe._dict({"based_on": "Sales Order", "days": 30}))
|
||||
self.assertTrue(columns)
|
||||
row = next((r for r in data if r.get("item") == item and r.get("days_since_last_order")), None)
|
||||
self.assertIsNotNone(row, "Inactive item should appear in the report")
|
||||
self.assertGreaterEqual(row["days_since_last_order"], 30)
|
||||
|
||||
def test_report_runs_for_sales_invoice(self):
|
||||
columns, _data = execute(frappe._dict({"based_on": "Sales Invoice", "days": 30}))
|
||||
self.assertTrue(columns)
|
||||
@@ -376,7 +376,7 @@ def get_items(filters, additional_table_columns):
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
return dict(frappe.get_all("Company", fields=["name", "stock_received_but_not_billed"], as_list=True))
|
||||
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
|
||||
|
||||
|
||||
def get_purchase_receipts_against_purchase_order(item_list):
|
||||
@@ -384,11 +384,16 @@ def get_purchase_receipts_against_purchase_order(item_list):
|
||||
po_item_rows = list(set(d.po_detail for d in item_list))
|
||||
|
||||
if po_item_rows:
|
||||
purchase_receipts = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"docstatus": 1, "purchase_order_item": ["in", po_item_rows]},
|
||||
fields=["parent", "purchase_order_item"],
|
||||
group_by="purchase_order_item, parent",
|
||||
purchase_receipts = frappe.db.sql(
|
||||
"""
|
||||
select parent, purchase_order_item
|
||||
from `tabPurchase Receipt Item`
|
||||
where docstatus=1 and purchase_order_item in (%s)
|
||||
group by purchase_order_item, parent
|
||||
"""
|
||||
% (", ".join(["%s"] * len(po_item_rows))),
|
||||
tuple(po_item_rows),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for pr in purchase_receipts:
|
||||
|
||||
@@ -9,9 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
pi = make_purchase_invoice(
|
||||
|
||||
@@ -9,11 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
|
||||
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -9,12 +9,42 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestPaymentLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.create_company()
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
doctypes = []
|
||||
doctypes.append(qb.DocType("GL Entry"))
|
||||
doctypes.append(qb.DocType("Payment Ledger Entry"))
|
||||
doctypes.append(qb.DocType("Sales Invoice"))
|
||||
doctypes.append(qb.DocType("Payment Entry"))
|
||||
|
||||
for doctype in doctypes:
|
||||
qb.from_(doctype).delete().where(doctype.company == self.company).run()
|
||||
|
||||
def create_company(self):
|
||||
name = "Test Payment Ledger"
|
||||
company = None
|
||||
if frappe.db.exists("Company", name):
|
||||
company = frappe.get_doc("Company", name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "All Warehouses" + " - " + company.abbr
|
||||
self.income_account = company.default_income_account
|
||||
self.expense_account = company.default_expense_account
|
||||
self.debit_to = company.default_receivable_account
|
||||
|
||||
def test_unpaid_invoice_outstanding(self):
|
||||
sinv = create_sales_invoice(
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
|
||||
@@ -48,46 +46,39 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_pos_entries(filters, group_by_field):
|
||||
p = frappe.qb.DocType("POS Invoice")
|
||||
query = (
|
||||
frappe.qb.from_(p)
|
||||
.select(
|
||||
p.posting_date,
|
||||
p.name.as_("pos_invoice"),
|
||||
p.pos_profile,
|
||||
p.company,
|
||||
p.owner,
|
||||
p.customer,
|
||||
p.is_return,
|
||||
p.base_grand_total.as_("grand_total"),
|
||||
)
|
||||
.where(p.docstatus == 1)
|
||||
)
|
||||
|
||||
for condition in get_conditions(filters, p):
|
||||
query = query.where(condition)
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
order_by = "p.posting_date"
|
||||
select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", ""
|
||||
if group_by_field == "mode_of_payment":
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
paid_amount = sip.base_amount - Case().when(sip.type == "Cash", p.change_amount).else_(0)
|
||||
query = (
|
||||
query.inner_join(sip)
|
||||
.on(sip.parent == p.name)
|
||||
.select(sip.mode_of_payment, paid_amount.as_("paid_amount"))
|
||||
.where(IfNull(paid_amount, 0) != 0)
|
||||
.orderby(p.posting_date)
|
||||
.orderby(sip.mode_of_payment)
|
||||
select_mop_field = (
|
||||
", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount"
|
||||
)
|
||||
elif group_by_field:
|
||||
query = (
|
||||
query.select((p.base_paid_amount - p.change_amount).as_("paid_amount"))
|
||||
.orderby(p.posting_date)
|
||||
.orderby(p[group_by_field])
|
||||
)
|
||||
else:
|
||||
query = query.orderby(p.posting_date)
|
||||
from_sales_invoice_payment = ", `tabSales Invoice Payment` sip"
|
||||
group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount - IF(sip.type='Cash', p.change_amount, 0), 0) != 0 AND"
|
||||
order_by += ", sip.mode_of_payment"
|
||||
|
||||
return query.run(as_dict=1)
|
||||
elif group_by_field:
|
||||
order_by += f", p.{group_by_field}"
|
||||
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
p.posting_date, p.name as pos_invoice, p.pos_profile, p.company,
|
||||
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
|
||||
FROM
|
||||
`tabPOS Invoice` p {from_sales_invoice_payment}
|
||||
WHERE
|
||||
p.docstatus = 1 and
|
||||
{group_by_mop_condition}
|
||||
{conditions}
|
||||
ORDER BY
|
||||
{order_by}
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def concat_mode_of_payments(pos_entries):
|
||||
@@ -136,34 +127,27 @@ def validate_filters(filters):
|
||||
frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method"))
|
||||
|
||||
|
||||
def get_conditions(filters, p):
|
||||
conditions = [
|
||||
p.company == filters.get("company"),
|
||||
p.posting_date >= filters.get("from_date"),
|
||||
p.posting_date <= filters.get("to_date"),
|
||||
]
|
||||
def get_conditions(filters):
|
||||
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s"
|
||||
|
||||
if filters.get("pos_profile"):
|
||||
conditions.append(p.pos_profile == filters.get("pos_profile"))
|
||||
conditions += " AND pos_profile = %(pos_profile)s"
|
||||
|
||||
if filters.get("owner"):
|
||||
conditions.append(p.owner == filters.get("owner"))
|
||||
conditions += " AND owner = %(owner)s"
|
||||
|
||||
if filters.get("customer"):
|
||||
conditions.append(p.customer == filters.get("customer"))
|
||||
conditions += " AND customer = %(customer)s"
|
||||
|
||||
if filters.get("is_return"):
|
||||
conditions.append(p.is_return == filters.get("is_return"))
|
||||
conditions += " AND is_return = %(is_return)s"
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
conditions.append(
|
||||
p.name.isin(
|
||||
frappe.qb.from_(sip)
|
||||
.select(sip.parent)
|
||||
.where(IfNull(sip.mode_of_payment, "") == filters.get("mode_of_payment"))
|
||||
)
|
||||
)
|
||||
conditions += """
|
||||
AND EXISTS(
|
||||
SELECT name FROM `tabSales Invoice Payment` sip
|
||||
WHERE parent=p.name AND ifnull(sip.mode_of_payment, '') = %(mode_of_payment)s
|
||||
)"""
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.report.pos_register.pos_register import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPOSRegister(ERPNextTestSuite):
|
||||
def test_report_executes(self):
|
||||
# Smoke-guards the raw-SQL -> query-builder port: the report's POS Invoice query must
|
||||
# compile and run on both MariaDB and postgres (it returns columns + a row list either way).
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
columns, data = execute(
|
||||
frappe._dict({"company": company, "from_date": add_days(today(), -365), "to_date": today()})
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
self.assertIsInstance(data, list)
|
||||
@@ -14,11 +14,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
|
||||
def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2026-06-15 00:00:00.000000",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Partners Commission",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "SELECT\n purchase_partner as \"Purchase Partner:Link/Purchase Partner:220\",\n sum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n sum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n sum(total_commission) as \"Total Commission:Currency:170\",\n sum(total_commission)*100 / NULLIF(sum(amount_eligible_for_commission), 0) as \"Average Commission Rate:Percent:220\"\nFROM\n `tabPurchase Invoice`\nWHERE\n docstatus = 1\n AND IFNULL(base_net_total, 0) > 0\n AND IFNULL(total_commission, 0) > 0\nGROUP BY\n purchase_partner\nORDER BY\n sum(total_commission) DESC",
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Purchase Partners Commission",
|
||||
"report_type": "Query Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika.terms import Bracket, LiteralValue, Order
|
||||
|
||||
@@ -309,17 +307,14 @@ def get_account_columns(invoice_list, include_payments):
|
||||
unrealized_profit_loss_account_columns = []
|
||||
|
||||
if invoice_list:
|
||||
expense_accounts = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"expense_account": ["is", "set"],
|
||||
"parenttype": "Purchase Invoice",
|
||||
"parent": ["in", [inv.name for inv in invoice_list]],
|
||||
},
|
||||
pluck="expense_account",
|
||||
distinct=True,
|
||||
order_by="expense_account",
|
||||
expense_accounts = frappe.db.sql_list(
|
||||
"""select distinct expense_account
|
||||
from `tabPurchase Invoice Item` where docstatus = 1
|
||||
and (expense_account is not null and expense_account != '')
|
||||
and parenttype='Purchase Invoice'
|
||||
and parent in (%s) order by expense_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple([inv.name for inv in invoice_list]),
|
||||
)
|
||||
|
||||
purchase_taxes_query = get_taxes_query(invoice_list, "Purchase Taxes and Charges", "Purchase Invoice")
|
||||
@@ -331,16 +326,13 @@ def get_account_columns(invoice_list, include_payments):
|
||||
advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
|
||||
tax_accounts = set(tax_accounts + advance_tax_accounts)
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.get_all(
|
||||
"Purchase Invoice",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"name": ["in", [inv.name for inv in invoice_list]],
|
||||
"unrealized_profit_loss_account": ["is", "set"],
|
||||
},
|
||||
pluck="unrealized_profit_loss_account",
|
||||
distinct=True,
|
||||
order_by="unrealized_profit_loss_account",
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list(
|
||||
"""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
)
|
||||
|
||||
for account in expense_accounts:
|
||||
@@ -462,11 +454,16 @@ def get_payments(filters):
|
||||
|
||||
|
||||
def get_invoice_expense_map(invoice_list):
|
||||
expense_details = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Purchase Invoice"},
|
||||
fields=["parent", "expense_account", {"SUM": "base_net_amount", "as": "amount"}],
|
||||
group_by="parent, expense_account",
|
||||
expense_details = frappe.db.sql(
|
||||
"""
|
||||
select parent, expense_account, sum(base_net_amount) as amount
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
group by parent, expense_account
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
invoice_expense_map = {}
|
||||
@@ -478,16 +475,13 @@ def get_invoice_expense_map(invoice_list):
|
||||
|
||||
|
||||
def get_internal_invoice_map(invoice_list):
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
unrealized_amount_details = (
|
||||
frappe.qb.from_(pi)
|
||||
.select(pi.name, pi.unrealized_profit_loss_account, pi.base_net_total.as_("amount"))
|
||||
.where(
|
||||
pi.name.isin([inv.name for inv in invoice_list])
|
||||
& (pi.is_internal_supplier == 1)
|
||||
& (pi.company == pi.represents_company)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
unrealized_amount_details = frappe.db.sql(
|
||||
"""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
|
||||
and is_internal_supplier = 1 and company = represents_company"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
internal_invoice_map = {}
|
||||
@@ -499,23 +493,18 @@ def get_internal_invoice_map(invoice_list):
|
||||
|
||||
|
||||
def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, include_payments=False):
|
||||
ptc = frappe.qb.DocType("Purchase Taxes and Charges")
|
||||
tax_amount = (
|
||||
Case()
|
||||
.when(ptc.add_deduct_tax == "Add", Sum(ptc.base_tax_amount_after_discount_amount))
|
||||
.else_(Sum(ptc.base_tax_amount_after_discount_amount) * -1)
|
||||
)
|
||||
tax_details = (
|
||||
frappe.qb.from_(ptc)
|
||||
.select(ptc.parent, ptc.account_head, tax_amount.as_("tax_amount"))
|
||||
.where(
|
||||
ptc.parent.isin([inv.name for inv in invoice_list])
|
||||
& ptc.category.isin(["Total", "Valuation and Total"])
|
||||
& (ptc.base_tax_amount_after_discount_amount != 0)
|
||||
& (ptc.parenttype == "Purchase Invoice")
|
||||
)
|
||||
.groupby(ptc.parent, ptc.account_head, ptc.add_deduct_tax)
|
||||
.run(as_dict=1)
|
||||
tax_details = frappe.db.sql(
|
||||
"""
|
||||
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)
|
||||
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
|
||||
from `tabPurchase Taxes and Charges`
|
||||
where parent in (%s) and category in ('Total', 'Valuation and Total')
|
||||
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
|
||||
group by parent, account_head, add_deduct_tax
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if include_payments:
|
||||
@@ -536,10 +525,15 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
|
||||
|
||||
|
||||
def get_invoice_po_pr_map(invoice_list):
|
||||
pi_items = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Purchase Invoice"},
|
||||
fields=["parent", "purchase_order", "purchase_receipt", "po_detail", "project"],
|
||||
pi_items = frappe.db.sql(
|
||||
"""
|
||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
invoice_po_pr_map = {}
|
||||
@@ -553,11 +547,10 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
if d.purchase_receipt:
|
||||
pr_list = [d.purchase_receipt]
|
||||
elif d.po_detail:
|
||||
pr_list = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"docstatus": 1, "purchase_order_item": d.po_detail},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
pr_list = frappe.db.sql_list(
|
||||
"""select distinct parent from `tabPurchase Receipt Item`
|
||||
where docstatus=1 and purchase_order_item=%s""",
|
||||
d.po_detail,
|
||||
)
|
||||
|
||||
if pr_list:
|
||||
@@ -572,8 +565,12 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
def get_account_details(invoice_list):
|
||||
account_map = {}
|
||||
accounts = list(set([inv.credit_to for inv in invoice_list]))
|
||||
for acc in frappe.get_all(
|
||||
"Account", filters={"name": ["in", accounts]}, fields=["name", "parent_account"]
|
||||
for acc in frappe.db.sql(
|
||||
"""select name, parent_account from tabAccount
|
||||
where name in (%s)"""
|
||||
% ", ".join(["%s"] * len(accounts)),
|
||||
tuple(accounts),
|
||||
as_dict=1,
|
||||
):
|
||||
account_map[acc.name] = acc.parent_account
|
||||
|
||||
|
||||
@@ -346,15 +346,12 @@ def get_account_columns(invoice_list, include_payments):
|
||||
unrealized_profit_loss_account_columns = []
|
||||
|
||||
if invoice_list:
|
||||
# frappe drops ORDER BY for distinct queries on postgres (db_query), so sort in python to keep
|
||||
# the generated account-column order deterministic and identical on both backends.
|
||||
income_accounts = sorted(
|
||||
frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"docstatus": 1, "parent": ["in", [inv.name for inv in invoice_list]]},
|
||||
pluck="income_account",
|
||||
distinct=True,
|
||||
)
|
||||
income_accounts = frappe.db.sql_list(
|
||||
"""select distinct income_account
|
||||
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
|
||||
order by income_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
)
|
||||
|
||||
sales_taxes_query = get_taxes_query(invoice_list, "Sales Taxes and Charges", "Sales Invoice")
|
||||
@@ -366,18 +363,14 @@ def get_account_columns(invoice_list, include_payments):
|
||||
advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
|
||||
tax_accounts = set(tax_accounts + advance_tax_accounts)
|
||||
|
||||
unrealized_profit_loss_accounts = sorted(
|
||||
frappe.get_all(
|
||||
"Sales Invoice",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"name": ["in", [inv.name for inv in invoice_list]],
|
||||
"is_internal_customer": 1,
|
||||
"unrealized_profit_loss_account": ["is", "set"],
|
||||
},
|
||||
pluck="unrealized_profit_loss_account",
|
||||
distinct=True,
|
||||
)
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list(
|
||||
"""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabSales Invoice` where docstatus = 1 and name in (%s)
|
||||
and is_internal_customer = 1
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
)
|
||||
|
||||
for account in income_accounts:
|
||||
@@ -501,11 +494,12 @@ def get_payments(filters):
|
||||
|
||||
|
||||
def get_invoice_income_map(invoice_list):
|
||||
income_details = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
|
||||
fields=["parent", "income_account", {"SUM": "base_net_amount", "as": "amount"}],
|
||||
group_by="parent, income_account",
|
||||
income_details = frappe.db.sql(
|
||||
"""select parent, income_account, sum(base_net_amount) as amount
|
||||
from `tabSales Invoice Item` where parent in (%s) group by parent, income_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
invoice_income_map = {}
|
||||
@@ -517,16 +511,13 @@ def get_invoice_income_map(invoice_list):
|
||||
|
||||
|
||||
def get_internal_invoice_map(invoice_list):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
unrealized_amount_details = (
|
||||
frappe.qb.from_(si)
|
||||
.select(si.name, si.unrealized_profit_loss_account, si.base_net_total.as_("amount"))
|
||||
.where(
|
||||
si.name.isin([inv.name for inv in invoice_list])
|
||||
& (si.is_internal_customer == 1)
|
||||
& (si.company == si.represents_company)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
unrealized_amount_details = frappe.db.sql(
|
||||
"""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabSales Invoice` where name in (%s)
|
||||
and is_internal_customer = 1 and company = represents_company"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
internal_invoice_map = {}
|
||||
@@ -538,15 +529,14 @@ def get_internal_invoice_map(invoice_list):
|
||||
|
||||
|
||||
def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, include_payments=False):
|
||||
tax_details = frappe.get_all(
|
||||
"Sales Taxes and Charges",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Sales Invoice"},
|
||||
fields=[
|
||||
"parent",
|
||||
"account_head",
|
||||
{"SUM": "base_tax_amount_after_discount_amount", "as": "tax_amount"},
|
||||
],
|
||||
group_by="parent, account_head",
|
||||
tax_details = frappe.db.sql(
|
||||
"""select parent, account_head,
|
||||
sum(base_tax_amount_after_discount_amount) as tax_amount
|
||||
from `tabSales Taxes and Charges` where parent in (%s) and parenttype = 'Sales Invoice'
|
||||
group by parent, account_head"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if include_payments:
|
||||
@@ -567,11 +557,13 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, inclu
|
||||
|
||||
|
||||
def get_invoice_so_dn_map(invoice_list):
|
||||
si_items = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
|
||||
or_filters=[["sales_order", "!=", ""], ["delivery_note", "!=", ""]],
|
||||
fields=["parent", "sales_order", "delivery_note", "so_detail"],
|
||||
si_items = frappe.db.sql(
|
||||
"""select parent, sales_order, delivery_note, so_detail
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (sales_order != '' or delivery_note != '')"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
invoice_so_dn_map = {}
|
||||
@@ -585,11 +577,10 @@ def get_invoice_so_dn_map(invoice_list):
|
||||
if d.delivery_note:
|
||||
delivery_note_list = [d.delivery_note]
|
||||
elif d.sales_order:
|
||||
delivery_note_list = frappe.get_all(
|
||||
"Delivery Note Item",
|
||||
filters={"docstatus": 1, "so_detail": d.so_detail},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
delivery_note_list = frappe.db.sql_list(
|
||||
"""select distinct parent from `tabDelivery Note Item`
|
||||
where docstatus=1 and so_detail=%s""",
|
||||
d.so_detail,
|
||||
)
|
||||
|
||||
if delivery_note_list:
|
||||
@@ -601,11 +592,13 @@ def get_invoice_so_dn_map(invoice_list):
|
||||
|
||||
|
||||
def get_invoice_cc_wh_map(invoice_list):
|
||||
si_items = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
|
||||
or_filters=[["cost_center", "!=", ""], ["warehouse", "!=", ""]],
|
||||
fields=["parent", "cost_center", "warehouse"],
|
||||
si_items = frappe.db.sql(
|
||||
"""select parent, cost_center, warehouse
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (cost_center != '' or warehouse != '')"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
invoice_cc_wh_map = {}
|
||||
@@ -626,11 +619,12 @@ def get_invoice_cc_wh_map(invoice_list):
|
||||
def get_mode_of_payments(invoice_list):
|
||||
mode_of_payments = {}
|
||||
if invoice_list:
|
||||
inv_mop = frappe.get_all(
|
||||
"Sales Invoice Payment",
|
||||
filters={"parent": ["in", list(invoice_list)]},
|
||||
fields=["parent", "mode_of_payment"],
|
||||
group_by="parent, mode_of_payment",
|
||||
inv_mop = frappe.db.sql(
|
||||
"""select parent, mode_of_payment
|
||||
from `tabSales Invoice Payment` where parent in (%s) group by parent, mode_of_payment"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(invoice_list),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in inv_mop:
|
||||
|
||||
@@ -10,13 +10,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_child_cost_center()
|
||||
|
||||
def create_child_cost_center(self):
|
||||
|
||||
@@ -62,10 +62,15 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_all_transfers(date, shareholder):
|
||||
return frappe.get_all(
|
||||
"Share Transfer",
|
||||
filters={"date": ["<=", date], "docstatus": 1},
|
||||
or_filters=[["from_shareholder", "=", shareholder], ["to_shareholder", "=", shareholder]],
|
||||
fields=["*"],
|
||||
order_by="date",
|
||||
condition = " "
|
||||
# if company:
|
||||
# condition = 'AND company = %(company)s '
|
||||
return frappe.db.sql(
|
||||
f"""SELECT * FROM `tabShare Transfer`
|
||||
WHERE ((DATE(date) <= %(date)s AND from_shareholder = %(shareholder)s {condition})
|
||||
OR (DATE(date) <= %(date)s AND to_shareholder = %(shareholder)s {condition}))
|
||||
AND docstatus = 1
|
||||
ORDER BY date""",
|
||||
{"date": date, "shareholder": shareholder},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -9,9 +9,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -20,7 +20,8 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.create_company()
|
||||
self.clear_old_entries()
|
||||
create_records()
|
||||
|
||||
def test_tax_withholding_for_customers(self):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Max, Sum
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, cstr, flt, formatdate, getdate
|
||||
|
||||
import erpnext
|
||||
@@ -82,21 +82,12 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[
|
||||
"name",
|
||||
"account_number",
|
||||
"parent_account",
|
||||
"account_name",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"is_group",
|
||||
"lft",
|
||||
"rgt",
|
||||
],
|
||||
order_by="lft",
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
as_dict=True,
|
||||
)
|
||||
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
|
||||
|
||||
@@ -249,8 +240,7 @@ def get_opening_balance(
|
||||
frappe.qb.from_(closing_balance)
|
||||
.select(
|
||||
closing_balance.account,
|
||||
# account_currency is constant per grouped account -> Max() keeps the GROUP BY postgres-valid
|
||||
Max(closing_balance.account_currency).as_("account_currency"),
|
||||
closing_balance.account_currency,
|
||||
Sum(closing_balance.debit).as_("debit"),
|
||||
Sum(closing_balance.credit).as_("credit"),
|
||||
Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Max, Sum
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -43,13 +43,7 @@ def get_data(filters):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
# voucher_type is constant per grouped voucher_no -> Max() keeps the GROUP BY valid on postgres
|
||||
.select(
|
||||
Max(gle.voucher_type).as_("voucher_type"),
|
||||
gle.voucher_no,
|
||||
Sum(gle.debit).as_("debit"),
|
||||
Sum(gle.credit).as_("credit"),
|
||||
)
|
||||
.select(gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit"))
|
||||
.where(gle.is_cancelled == 0)
|
||||
.groupby(gle.voucher_no)
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Abs, Max, Sum
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
@@ -150,7 +150,7 @@ def calculate_total_advance_from_ledger(doc) -> list:
|
||||
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
||||
return (
|
||||
frappe.qb.from_(adv)
|
||||
.select(Abs(Sum(adv.amount)).as_("amount"), Max(adv.currency).as_("account_currency"))
|
||||
.select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency"))
|
||||
.where(adv.company == doc.company)
|
||||
.where(adv.delinked == 0)
|
||||
.where(adv.against_voucher_type == doc.doctype)
|
||||
|
||||
@@ -141,7 +141,7 @@ def make_exchange_gain_loss_journal(
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (je.name.isin(parents))
|
||||
& (je.voucher_type == "Exchange Gain Or Loss")
|
||||
& (je.voucher_type == "Exchange Gain or Loss")
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
@@ -80,8 +80,6 @@ class TestUtils(ERPNextTestSuite):
|
||||
purchase_invoice.submit()
|
||||
|
||||
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
|
||||
payment_entry.target_exchange_rate = 82.32
|
||||
payment_entry.set_amounts()
|
||||
payment_entry.paid_amount = 15725
|
||||
payment_entry.deductions = []
|
||||
payment_entry.save()
|
||||
|
||||
@@ -200,6 +200,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_on(
|
||||
account: str | None = None,
|
||||
date: DateTimeLikeObject | None = None,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user