Compare commits

..

1 Commits

Author SHA1 Message Date
Mohd Haris
a3f262b415 fix: restrict Party Type/Party to Receivable/Payable accounts in Journal Entry
Journal Entry validation only checked party details when an account was
of type Receivable/Payable, but never restricted setting a Party Type or
Party against accounts of other types. This regressed v14 behavior where
party info could only be captured for Receivable/Payable accounts.

Add a branch to validate_party() that throws when a Party Type or Party
is set on an account that is not Receivable/Payable, with a clear message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:21:51 +05:30
335 changed files with 4894 additions and 11710 deletions

View File

@@ -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",

View File

@@ -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"

View File

@@ -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(':')

View File

@@ -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"

View File

@@ -1,7 +1,6 @@
import frappe
from frappe import _
from frappe.email import sendmail_to_system_managers
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import (
add_days,
add_months,
@@ -54,24 +53,20 @@ def validate_service_stop_date(doc):
def build_conditions(process_type, account, company):
if process_type == "Income":
item = frappe.qb.DocType("Sales Invoice Item")
parent = frappe.qb.DocType("Sales Invoice")
deferred_account = item.deferred_revenue_account
else:
item = frappe.qb.DocType("Purchase Invoice Item")
parent = frappe.qb.DocType("Purchase Invoice")
deferred_account = item.deferred_expense_account
conditions = ""
deferred_account = (
"item.deferred_revenue_account" if process_type == "Income" else "item.deferred_expense_account"
)
if account:
return deferred_account == account
conditions += f"AND {deferred_account}={frappe.db.escape(account)}"
elif company:
return parent.company == company
conditions += f"AND p.company = {frappe.db.escape(company)}"
return None
return conditions
def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=None):
def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=""):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date:
@@ -80,25 +75,17 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
end_date = add_days(today(), -1)
# check for the purchase invoice for which GL entries has to be done
item = frappe.qb.DocType("Purchase Invoice Item")
parent = frappe.qb.DocType("Purchase Invoice")
query = (
frappe.qb.from_(item)
.inner_join(parent)
.on(item.parent == parent.name)
.select(item.parent)
.distinct()
.where(
(item.service_start_date <= end_date)
& (item.service_end_date >= start_date)
& (item.enable_deferred_expense == 1)
& (item.docstatus == 1)
& (IfNull(item.amount, 0) > 0)
)
)
if conditions is not None:
query = query.where(conditions)
invoices = query.run(pluck=True)
invoices = frappe.db.sql_list(
f"""
select distinct item.parent
from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s
and item.enable_deferred_expense = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{conditions}
""",
(end_date, start_date),
) # nosec
# For each invoice, book deferred expense
for invoice in invoices:
@@ -109,7 +96,7 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
send_mail(deferred_process)
def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=None):
def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=""):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date:
@@ -118,25 +105,17 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
end_date = add_days(today(), -1)
# check for the sales invoice for which GL entries has to be done
item = frappe.qb.DocType("Sales Invoice Item")
parent = frappe.qb.DocType("Sales Invoice")
query = (
frappe.qb.from_(item)
.inner_join(parent)
.on(item.parent == parent.name)
.select(item.parent)
.distinct()
.where(
(item.service_start_date <= end_date)
& (item.service_end_date >= start_date)
& (item.enable_deferred_revenue == 1)
& (item.docstatus == 1)
& (IfNull(item.amount, 0) > 0)
)
)
if conditions is not None:
query = query.where(conditions)
invoices = query.run(pluck=True)
invoices = frappe.db.sql_list(
f"""
select distinct item.parent
from `tabSales Invoice Item` item, `tabSales Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s
and item.enable_deferred_revenue = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{conditions}
""",
(end_date, start_date),
) # nosec
for invoice in invoices:
doc = frappe.get_doc("Sales Invoice", invoice)
@@ -157,39 +136,26 @@ def get_booking_dates(doc, item, posting_date=None, prev_posting_date=None):
)
if not prev_posting_date:
prev_gl_entry = frappe.get_all(
"GL Entry",
filters={
"company": doc.company,
"account": item.get(deferred_account),
"voucher_type": doc.doctype,
"voucher_no": doc.name,
"voucher_detail_no": item.name,
"is_cancelled": 0,
},
fields=["name", "posting_date"],
order_by="posting_date desc",
limit=1,
prev_gl_entry = frappe.db.sql(
"""
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
order by posting_date desc limit 1
""",
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
prev_gl_via_je = (
frappe.qb.from_(je)
.inner_join(jea)
.on(je.name == jea.parent)
.select(je.name, je.posting_date)
.where(
(je.company == doc.company)
& (jea.account == item.get(deferred_account))
& (jea.reference_type == doc.doctype)
& (jea.reference_name == doc.name)
& (jea.reference_detail_no == item.name)
& (jea.docstatus < 2)
)
.orderby(je.posting_date, order=frappe.qb.desc)
.limit(1)
.run(as_dict=True)
prev_gl_via_je = frappe.db.sql(
"""
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
WHERE p.name = c.parent and p.company=%s and c.account=%s
and c.reference_type=%s and c.reference_name=%s
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
""",
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
if prev_gl_via_je:
@@ -311,47 +277,26 @@ def get_already_booked_amount(doc, item):
total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency"
deferred_account = "deferred_expense_account"
gle = frappe.qb.DocType("GL Entry")
gl_entries_details = (
frappe.qb.from_(gle)
.select(
Sum(gle[total_credit_debit]).as_("total_credit"),
Sum(gle[total_credit_debit_currency]).as_("total_credit_in_account_currency"),
gle.voucher_detail_no,
)
.where(
(gle.company == doc.company)
& (gle.account == item.get(deferred_account))
& (gle.voucher_type == doc.doctype)
& (gle.voucher_no == doc.name)
& (gle.voucher_detail_no == item.name)
& (gle.is_cancelled == 0)
)
.groupby(gle.voucher_detail_no)
.run(as_dict=True)
gl_entries_details = frappe.db.sql(
"""
select sum({}) as total_credit, sum({}) as total_credit_in_account_currency, voucher_detail_no
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
group by voucher_detail_no
""".format(total_credit_debit, total_credit_debit_currency),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
journal_entry_details = (
frappe.qb.from_(je)
.inner_join(jea)
.on(je.name == jea.parent)
.select(
Sum(jea[total_credit_debit]).as_("total_credit"),
Sum(jea[total_credit_debit_currency]).as_("total_credit_in_account_currency"),
jea.reference_detail_no,
)
.where(
(je.company == doc.company)
& (jea.account == item.get(deferred_account))
& (jea.reference_type == doc.doctype)
& (jea.reference_name == doc.name)
& (jea.reference_detail_no == item.name)
& (je.docstatus < 2)
)
.groupby(jea.reference_detail_no)
.run(as_dict=True)
journal_entry_details = frappe.db.sql(
"""
SELECT sum(c.{}) as total_credit, sum(c.{}) as total_credit_in_account_currency, reference_detail_no
FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and
p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s
and p.docstatus < 2 group by reference_detail_no
""".format(total_credit_debit, total_credit_debit_currency),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0

View File

@@ -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):
"""

View File

@@ -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",

View File

@@ -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

View File

@@ -7,7 +7,7 @@ from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Coalesce, Max, Sum
from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import cint, flt, fmt_money, getdate
from pypika import Order
@@ -195,17 +195,14 @@ def get_payment_entries_for_bank_clearance(
.select(
ConstantColumn("Journal Entry").as_("payment_document"),
journal_entry.name.as_("payment_entry"),
# non-grouped columns are constant per grouped JE name / account (against_account is
# arbitrary per group on MySQL) -> Max() keeps the GROUP BY valid on postgres with the
# same value MySQL picked.
Max(journal_entry.cheque_no).as_("cheque_number"),
Max(journal_entry.cheque_date).as_("cheque_date"),
journal_entry.cheque_no.as_("cheque_number"),
journal_entry.cheque_date,
Sum(journal_entry_account.debit_in_account_currency).as_("debit"),
Sum(journal_entry_account.credit_in_account_currency).as_("credit"),
Max(journal_entry.posting_date).as_("posting_date"),
Max(journal_entry_account.against_account).as_("against_account"),
Max(journal_entry.clearance_date).as_("clearance_date"),
Max(journal_entry_account.account_currency).as_("account_currency"),
journal_entry.posting_date,
journal_entry_account.against_account,
journal_entry.clearance_date,
journal_entry_account.account_currency,
)
.where(
(journal_entry_account.account == account)
@@ -218,13 +215,12 @@ def get_payment_entries_for_bank_clearance(
if not include_reconciled_entries:
journal_entry_query = journal_entry_query.where(
(journal_entry.clearance_date.isnull())
| (journal_entry.clearance_date == ("0000-00-00" if frappe.db.db_type != "postgres" else None))
(journal_entry.clearance_date.isnull()) | (journal_entry.clearance_date == "0000-00-00")
)
journal_entries = (
journal_entry_query.groupby(journal_entry_account.account, journal_entry.name)
.orderby(Max(journal_entry.posting_date))
.orderby(journal_entry.posting_date)
.orderby(journal_entry.name, order=Order.desc)
).run(as_dict=True)
@@ -294,8 +290,7 @@ def get_payment_entries_for_bank_clearance(
if not include_reconciled_entries:
payment_entry_query = payment_entry_query.where(
(pe.clearance_date.isnull())
| (pe.clearance_date == ("0000-00-00" if frappe.db.db_type != "postgres" else None))
(pe.clearance_date.isnull()) | (pe.clearance_date == "0000-00-00")
)
payment_entries = (payment_entry_query.orderby(pe.posting_date).orderby(pe.name, order=Order.desc)).run(
@@ -332,8 +327,7 @@ def get_payment_entries_for_bank_clearance(
if not include_reconciled_entries:
paid_purchase_invoices_query = paid_purchase_invoices_query.where(
(pi.clearance_date.isnull())
| (pi.clearance_date == ("0000-00-00" if frappe.db.db_type != "postgres" else None))
(pi.clearance_date.isnull()) | (pi.clearance_date == "0000-00-00")
)
paid_purchase_invoices = (
@@ -373,8 +367,7 @@ def get_payment_entries_for_bank_clearance(
if not include_reconciled_entries:
pos_sales_invoices_query = pos_sales_invoices_query.where(
(si_payment.clearance_date.isnull())
| (si_payment.clearance_date == ("0000-00-00" if frappe.db.db_type != "postgres" else None))
(si_payment.clearance_date.isnull()) | (si_payment.clearance_date == "0000-00-00")
)
pos_sales_invoices = (

View File

@@ -9,13 +9,6 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
frappe.ui.form.on("Bank Guarantee", {
setup: function (frm) {
frm.set_query("reference_doctype", function () {
return {
filters: {
name: ["in", ["Sales Order", "Purchase Order"]],
},
};
});
frm.set_query("bank_account", function () {
return {
filters: {

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "ACC-BG-.YYYY.-.#####",
"creation": "2016-12-17 10:43:35.731631",
"doctype": "DocType",
@@ -51,7 +50,8 @@
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
"options": "DocType",
"read_only": 1
},
{
"fieldname": "reference_docname",
@@ -60,14 +60,14 @@
"options": "reference_doctype"
},
{
"depends_on": "eval: doc.reference_doctype == \"Sales Order\"",
"depends_on": "eval: doc.bg_type == \"Receiving\"",
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer"
},
{
"depends_on": "eval: doc.reference_doctype == \"Purchase Order\"",
"depends_on": "eval: doc.bg_type == \"Providing\"",
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
@@ -218,11 +218,10 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2026-05-25 18:12:10.768835",
"modified": "2025-08-29 11:52:33.550847",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Guarantee",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{

View File

@@ -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
@@ -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:

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
@@ -480,28 +478,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 +523,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 = {}

View File

@@ -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(

View File

@@ -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()

View File

@@ -136,9 +136,6 @@ function set_total_budget_amount(frm) {
function toggle_distribution_fields(frm) {
const grid = frm.fields_dict.budget_distribution.grid;
frm.set_df_property("budget_distribution", "cannot_add_rows", true);
frm.set_df_property("budget_distribution", "cannot_delete_rows", true);
["amount", "percent"].forEach((field) => {
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
});

View File

@@ -5,11 +5,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Coalesce, Sum
from frappe.query_builder.functions import Sum
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
from frappe.utils.data import get_first_day
from pypika.terms import ExistsCriterion
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -117,26 +115,23 @@ class Budget(Document):
if not account:
return
budget = frappe.qb.DocType("Budget")
fy_from = frappe.qb.DocType("Fiscal Year").as_("fy_from")
fy_to = frappe.qb.DocType("Fiscal Year").as_("fy_to")
existing_budget = (
frappe.qb.from_(budget)
.inner_join(fy_from)
.on(fy_from.name == budget.from_fiscal_year)
.inner_join(fy_to)
.on(fy_to.name == budget.to_fiscal_year)
.select(budget.name, budget.account)
.where(
(budget.docstatus < 2)
& (budget.company == self.company)
& (budget[budget_against_field] == budget_against)
& (budget.account == account)
& (budget.name != self.name)
& (fy_from.year_start_date <= self.budget_end_date)
& (fy_to.year_end_date >= self.budget_start_date)
)
.run(as_dict=True)
existing_budget = frappe.db.sql(
f"""
SELECT name, account
FROM `tabBudget`
WHERE
docstatus < 2
AND company = %s
AND {budget_against_field} = %s
AND account = %s
AND name != %s
AND (
(SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
AND (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
)
""",
(self.company, budget_against, account, self.name, self.budget_end_date, self.budget_start_date),
as_dict=True,
)
if existing_budget:
@@ -358,8 +353,8 @@ class Budget(Document):
if self.should_regenerate_budget_distribution():
return
total_amount = sum(flt(d.amount) for d in self.budget_distribution)
total_percent = sum(flt(d.percent) for d in self.budget_distribution)
total_amount = sum(d.amount for d in self.budget_distribution)
total_percent = sum(d.percent for d in self.budget_distribution)
if flt(abs(total_amount - self.budget_amount), 2) > 0.10:
frappe.throw(
@@ -386,24 +381,17 @@ def validate_expense_against_budget(params, expense_amount=0):
posting_fiscal_year = get_fiscal_year(posting_date, company=params.get("company"))[0]
year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year)
budget = frappe.qb.DocType("Budget")
fy_from = frappe.qb.DocType("Fiscal Year").as_("fy_from")
fy_to = frappe.qb.DocType("Fiscal Year").as_("fy_to")
budget_exists = (
frappe.qb.from_(budget)
.inner_join(fy_from)
.on(fy_from.name == budget.from_fiscal_year)
.inner_join(fy_to)
.on(fy_to.name == budget.to_fiscal_year)
.select(budget.name)
.where(
(budget.company == params.company)
& (budget.docstatus == 1)
& (fy_from.year_start_date <= year_end_date)
& (fy_to.year_end_date >= year_start_date)
)
.limit(1)
.run()
budget_exists = frappe.db.sql(
"""
select name
from `tabBudget`
where company = %s
and docstatus = 1
and (SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
and (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
limit 1
""",
(params.company, year_end_date, year_start_date),
)
if not budget_exists:
@@ -446,52 +434,50 @@ def validate_expense_against_budget(params, expense_amount=0):
and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense")
):
doctype = dimension.get("document_type")
params.is_tree = bool(frappe.get_cached_value("DocType", doctype, "is_tree"))
if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.get_cached_value(doctype, params.get(budget_against), ["lft", "rgt"])
condition = f"""and exists(select name from `tab{doctype}`
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
params.is_tree = True
else:
condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}"
params.is_tree = False
params.budget_against_field = budget_against
params.budget_against_doctype = doctype
b = frappe.qb.DocType("Budget")
query = (
frappe.qb.from_(b)
.select(
budget_records = frappe.db.sql(
f"""
SELECT
b.name,
getattr(b, budget_against).as_("budget_against"),
b.{budget_against} AS budget_against,
b.budget_amount,
b.from_fiscal_year,
b.to_fiscal_year,
b.budget_start_date,
b.budget_end_date,
Coalesce(b.applicable_on_material_request, 0).as_("for_material_request"),
Coalesce(b.applicable_on_purchase_order, 0).as_("for_purchase_order"),
Coalesce(b.applicable_on_booking_actual_expenses, 0).as_("for_actual_expenses"),
IFNULL(b.applicable_on_material_request, 0) AS for_material_request,
IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order,
IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses,
b.action_if_annual_budget_exceeded,
b.action_if_accumulated_monthly_budget_exceeded,
b.action_if_annual_budget_exceeded_on_mr,
b.action_if_accumulated_monthly_budget_exceeded_on_mr,
b.action_if_annual_budget_exceeded_on_po,
b.action_if_accumulated_monthly_budget_exceeded_on_po,
)
.where(b.company == params.company)
.where(b.docstatus == 1)
.where(b.budget_start_date <= params.posting_date)
.where(b.budget_end_date >= params.posting_date)
.where(b.account == params.account)
)
if params.is_tree:
lft, rgt = frappe.get_cached_value(doctype, params.get(budget_against), ["lft", "rgt"])
dim = frappe.qb.DocType(doctype)
query = query.where(
ExistsCriterion(
frappe.qb.from_(dim)
.select(dim.name)
.where((dim.lft <= lft) & (dim.rgt >= rgt) & (dim.name == getattr(b, budget_against)))
)
)
else:
query = query.where(getattr(b, budget_against) == params.get(budget_against))
budget_records = query.run(as_dict=True)
b.action_if_accumulated_monthly_budget_exceeded_on_po
FROM
`tabBudget` b
WHERE
b.company = %s
AND b.docstatus = 1
AND %s BETWEEN b.budget_start_date AND b.budget_end_date
AND b.account = %s
{condition}
""",
(params.company, params.posting_date, params.account),
as_dict=True,
) # nosec
if budget_records:
validate_budget_records(params, budget_records, expense_amount)
@@ -688,27 +674,15 @@ def get_actions(params, budget):
def get_requested_amount(params):
item_code = params.get("item_code")
condition = get_other_condition(params, "Material Request")
child = frappe.qb.DocType("Material Request Item")
parent = frappe.qb.DocType("Material Request")
data = (
frappe.qb.from_(child)
.join(parent)
.on(parent.name == child.parent)
.select(
# rate inside the aggregate: Sum(qty * rate) is the correct requested amount and is PG-valid
Coalesce(Sum((child.stock_qty - child.ordered_qty) * child.rate), 0).as_("amount")
)
.where(
(child.item_code == item_code)
& (parent.docstatus == 1)
& (child.stock_qty > child.ordered_qty)
& Criterion.all(get_other_condition(params, child, parent, "Material Request"))
& (parent.material_request_type == "Purchase")
& (parent.status != "Stopped")
)
.run(as_list=1)
data = frappe.db.sql(
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {} and
parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition),
item_code,
as_list=1,
)
return data[0][0] if data else 0
@@ -716,43 +690,37 @@ def get_requested_amount(params):
def get_ordered_amount(params):
item_code = params.get("item_code")
condition = get_other_condition(params, "Purchase Order")
child = frappe.qb.DocType("Purchase Order Item")
parent = frappe.qb.DocType("Purchase Order")
data = (
frappe.qb.from_(child)
.join(parent)
.on(parent.name == child.parent)
.select(Coalesce(Sum(child.amount - child.billed_amt), 0).as_("amount"))
.where(
(child.item_code == item_code)
& (parent.docstatus == 1)
& (child.amount > child.billed_amt)
& (parent.status != "Closed")
& Criterion.all(get_other_condition(params, child, parent, "Purchase Order"))
)
.run(as_list=1)
data = frappe.db.sql(
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
from `tabPurchase Order Item` child, `tabPurchase Order` parent where
parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt
and parent.status != 'Closed' and {condition}""",
item_code,
as_list=1,
)
return data[0][0] if data else 0
def get_other_condition(params, child, parent, for_doc):
conditions = [child.expense_account == params.expense_account]
def get_other_condition(params, for_doc):
condition = f"expense_account = {frappe.db.escape(params.expense_account)}"
budget_against_field = params.get("budget_against_field")
if budget_against_field and params.get(budget_against_field):
conditions.append(child[budget_against_field] == params.get(budget_against_field))
condition += (
f" and child.{budget_against_field} = {frappe.db.escape(params.get(budget_against_field))}"
)
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
conditions.append(parent[date_field][str(start_date) : str(end_date)])
condition += f" and parent.{date_field} between {frappe.db.escape(str(start_date))} and {frappe.db.escape(str(end_date))}"
return conditions
return condition
def get_actual_expense(params):
@@ -760,19 +728,11 @@ def get_actual_expense(params):
params.budget_against_doctype = frappe.unscrub(params.budget_against_field)
budget_against_field = params.get("budget_against_field")
condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else ""
gle = frappe.qb.DocType("GL Entry")
conditions = [
gle.is_cancelled == 0,
gle.account == params.get("account"),
gle.posting_date[str(params.budget_start_date) : str(params.budget_end_date)],
gle.company == params.get("company"),
gle.docstatus == 1,
]
if params.get("month_end_date"):
conditions.append(gle.posting_date <= params.get("month_end_date"))
date_condition = (
f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
)
if params.is_tree:
lft_rgt = frappe.db.get_value(
@@ -780,27 +740,35 @@ def get_actual_expense(params):
)
params.update(lft_rgt)
tree = frappe.qb.DocType(params.budget_against_doctype)
conditions.append(
ExistsCriterion(
frappe.qb.from_(tree)
.select(tree.name)
.where(
(tree.lft >= params.get("lft"))
& (tree.rgt <= params.get("rgt"))
& (tree.name == gle[budget_against_field])
)
condition2 = f"""
and exists(
select name from `tab{params.budget_against_doctype}`
where lft >= %(lft)s and rgt <= %(rgt)s
and name = gle.{budget_against_field}
)
)
"""
else:
conditions.append(gle[budget_against_field] == params.get(budget_against_field))
condition2 = f"""
and gle.{budget_against_field} = %({budget_against_field})s
"""
amount = flt(
frappe.qb.from_(gle)
.select(Sum(gle.debit) - Sum(gle.credit))
.where(Criterion.all(conditions))
.run()[0][0]
)
frappe.db.sql(
f"""
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where
is_cancelled = 0
and gle.account = %(account)s
{condition1}
{date_condition}
and gle.company = %(company)s
and gle.docstatus = 1
{condition2}
""",
params,
)[0][0]
) # nosec
return amount

View File

@@ -18,7 +18,6 @@
"in_list_view": 1,
"label": "Start Date",
"read_only": 1,
"reqd": 1,
"search_index": 1
},
{
@@ -26,29 +25,26 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date",
"read_only": 1,
"reqd": 1
"read_only": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"reqd": 1
"label": "Amount"
},
{
"fieldname": "percent",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Percent",
"reqd": 1
"label": "Percent"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-06-18 11:23:17.669733",
"modified": "2025-11-03 13:18:28.398198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget Distribution",

View File

@@ -15,12 +15,12 @@ class BudgetDistribution(Document):
from frappe.types import DF
amount: DF.Currency
end_date: DF.Date
end_date: DF.Date | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
percent: DF.Percent
start_date: DF.Date
start_date: DF.Date | None
# end: auto-generated types
pass

View File

@@ -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)

View File

@@ -75,10 +75,7 @@ def validate_company(company: str):
@frappe.whitelist()
def import_coa(file_name: str, company: str):
frappe.only_for("Accounts Manager")
# delete existing data for accounts
frappe.has_permission("Company", "write", company, throw=True)
unset_existing_data(company)
# create accounts
@@ -456,7 +453,6 @@ def unset_existing_data(company):
fieldnames = get_linked_fields("Account").get("Company", {}).get("fieldname", [])
linked = [{"fieldname": name} for name in fieldnames]
update_values = {d.get("fieldname"): "" for d in linked}
frappe.db.set_value("Company", company, update_values, update_values)
# remove accounts data from various doctypes
@@ -468,7 +464,8 @@ def unset_existing_data(company):
"Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template",
]:
frappe.get_query(doctype, delete=True, filters={"company": company}, ignore_permissions=False).run()
dt = frappe.qb.DocType(doctype)
frappe.qb.from_(dt).where(dt.company == company).delete().run()
def set_default_accounts(company):

View File

@@ -84,10 +84,10 @@ class CostCenter(NestedSet):
return frappe.db.get_value("GL Entry", {"cost_center": self.name})
def check_if_child_exists(self):
return frappe.get_all(
"Cost Center",
filters={"parent_cost_center": self.name, "docstatus": ["!=", 2]},
pluck="name",
return frappe.db.sql(
"select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2",
self.name,
)
def if_allocation_exists_against_cost_center(self):

View File

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

View File

@@ -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):

View File

@@ -72,8 +72,10 @@ class FiscalYear(Document):
if existing_fiscal_years:
for existing in existing_fiscal_years:
company_for_existing = frappe.get_all(
"Fiscal Year Company", filters={"parent": existing.name}, pluck="company"
company_for_existing = frappe.db.sql_list(
"""select company from `tabFiscal Year Company`
where parent=%s""",
existing.name,
)
overlap = False

View File

@@ -7,7 +7,6 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.model.naming import set_name_from_naming_options
from frappe.query_builder.functions import Sum
from frappe.utils import create_batch, flt, fmt_money, now
import erpnext
@@ -332,12 +331,10 @@ def validate_balance_type(account, adv_adj=False):
if not adv_adj and account:
balance_must_be = frappe.get_cached_value("Account", account, "balance_must_be")
if balance_must_be:
gle = frappe.qb.DocType("GL Entry")
balance = (
frappe.qb.from_(gle)
.select(Sum(gle.debit) - Sum(gle.credit))
.where((gle.is_cancelled == 0) & (gle.account == account))
.run()
balance = frappe.db.sql(
"""select sum(debit) - sum(credit)
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
account,
)[0][0]
if (balance_must_be == "Debit" and flt(balance) < 0) or (
@@ -351,48 +348,44 @@ def validate_balance_type(account, adv_adj=False):
def update_outstanding_amt(
account, party_type, party, against_voucher_type, against_voucher, on_cancel=False
):
gle = frappe.qb.DocType("GL Entry")
conditions = (
(gle.against_voucher_type == against_voucher_type)
& (gle.against_voucher == against_voucher)
& (gle.voucher_type != "Invoice Discounting")
)
if party_type and party:
conditions &= (gle.party_type == party_type) & (gle.party == party)
party_condition = " and party_type={} and party={}".format(
frappe.db.escape(party_type), frappe.db.escape(party)
)
else:
party_condition = ""
if against_voucher_type == "Sales Invoice":
party_account = frappe.get_cached_value(against_voucher_type, against_voucher, "debit_to")
conditions &= gle.account.isin([account, party_account])
account_condition = f"and account in ({frappe.db.escape(account)}, {frappe.db.escape(party_account)})"
else:
conditions &= gle.account == account
account_condition = f" and account = {frappe.db.escape(account)}"
# get final outstanding amt
bal = flt(
frappe.qb.from_(gle)
.select(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency))
.where(conditions)
.run()[0][0]
frappe.db.sql(
f"""
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where against_voucher_type=%s and against_voucher=%s
and voucher_type != 'Invoice Discounting'
{party_condition} {account_condition}""",
(against_voucher_type, against_voucher),
)[0][0]
or 0.0
)
if against_voucher_type == "Purchase Invoice":
bal = -bal
elif against_voucher_type == "Journal Entry":
je_conditions = (
(gle.voucher_type == "Journal Entry")
& (gle.voucher_no == against_voucher)
& (gle.account == account)
& (gle.against_voucher.isnull() | (gle.against_voucher == ""))
)
if party_type and party:
je_conditions &= (gle.party_type == party_type) & (gle.party == party)
against_voucher_amount = flt(
frappe.qb.from_(gle)
.select(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency))
.where(je_conditions)
.run()[0][0]
frappe.db.sql(
f"""
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
and account = %s and (against_voucher is null or against_voucher='') {party_condition}""",
(against_voucher, account),
)[0][0]
)
if not against_voucher_amount:
@@ -487,14 +480,10 @@ def rename_temporarily_named_docs(doctype):
oldname = doc.name
set_name_from_naming_options(autoname, doc)
newname = doc.name
dt = frappe.qb.DocType(doctype)
(
frappe.qb.update(dt)
.set(dt.name, newname)
.set(dt.to_rename, 0)
.set(dt.modified, now())
.where(dt.name == oldname)
).run()
frappe.db.sql(
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
(newname, now(), oldname),
)
for hook_type in ("on_gle_rename", "on_sle_rename"):
for hook in frappe.get_hooks(hook_type):

View File

@@ -26,17 +26,12 @@ class TestGLEntry(ERPNextTestSuite):
jv.flags.ignore_validate = True
jv.submit()
round_off_entry = frappe.get_all(
"GL Entry",
filters={
"voucher_type": "Journal Entry",
"voucher_no": jv.name,
"account": "_Test Write Off - _TC",
"cost_center": "_Test Cost Center - _TC",
"debit": 0,
"credit": 0.01,
},
pluck="name",
round_off_entry = frappe.db.sql(
"""select name from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
and account='_Test Write Off - _TC' and cost_center='_Test Cost Center - _TC'
and debit = 0 and credit = '.01'""",
jv.name,
)
self.assertTrue(round_off_entry)
@@ -60,9 +55,8 @@ class TestGLEntry(ERPNextTestSuite):
)
self.assertTrue(all(entry.to_rename == 1 for entry in gl_entries))
series = frappe.qb.DocType("Series")
old_naming_series_current_value = (
frappe.qb.from_(series).select(series["current"]).where(series.name == naming_series).run()
old_naming_series_current_value = frappe.db.sql(
"SELECT current from tabSeries where name = %s", naming_series
)[0][0]
rename_gle_sle_docs()
@@ -79,8 +73,8 @@ class TestGLEntry(ERPNextTestSuite):
all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries, strict=False))
)
new_naming_series_current_value = (
frappe.qb.from_(series).select(series["current"]).where(series.name == naming_series).run()
new_naming_series_current_value = frappe.db.sql(
"SELECT current from tabSeries where name = %s", naming_series
)[0][0]
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)

View File

@@ -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":

View File

@@ -28,7 +28,6 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
from erpnext.accounts.party import get_party_account
from erpnext.accounts.services.gl_validator import validate_opening_entry_against_pcv
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
@@ -150,9 +149,6 @@ class JournalEntry(AccountsController):
if not self.is_opening:
self.is_opening = "No"
if self.is_opening == "Yes":
validate_opening_entry_against_pcv(self.company)
self.clearance_date = None
self.validate_party()
@@ -488,6 +484,12 @@ class JournalEntry(AccountsController):
d.idx, d.account, d.party_type
)
)
elif d.party_type or d.party:
frappe.throw(
_(
"Row {0}: Party Type or Party can only be set for Receivable / Payable account, but account {1} is of type {2}"
).format(d.idx, d.account, account_type or _("None"))
)
def check_credit_limit(self):
customers = list(

View File

@@ -662,6 +662,13 @@ class TestJournalEntry(ERPNextTestSuite):
jv.save()
self.assertRaises(frappe.ValidationError, jv.submit)
def test_party_not_allowed_for_non_receivable_payable_account(self):
customer = make_customer("_Test New Customer")
jv = make_journal_entry(account1="_Test Cash - _TC", account2="_Test Bank - _TC", amount=100, save=False)
jv.accounts[0].party_type = "Customer"
jv.accounts[0].party = customer
self.assertRaises(frappe.ValidationError, jv.save)
def test_validate_reference_doc_debit_against_sales_order_throws(self):
"""Characterize: a debit entry linked to a Sales Order is rejected."""
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order

View File

@@ -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")

View File

@@ -39,32 +39,28 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No
if not expiry_date:
expiry_date = today()
return frappe.get_all(
"Loyalty Point Entry",
filters={
"customer": customer,
"loyalty_program": loyalty_program,
"expiry_date": [">=", expiry_date],
"loyalty_points": [">", 0],
"company": company,
},
fields=["name", "loyalty_points", "expiry_date", "loyalty_program_tier", "invoice_type", "invoice"],
order_by="expiry_date",
return frappe.db.sql(
"""
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s
and expiry_date>=%s and loyalty_points>0 and company=%s
order by expiry_date
""",
(customer, loyalty_program, expiry_date, company),
as_dict=1,
)
def get_redemption_details(customer, loyalty_program, company):
return frappe._dict(
frappe.get_all(
"Loyalty Point Entry",
filters={
"customer": customer,
"loyalty_program": loyalty_program,
"loyalty_points": ["<", 0],
"company": company,
},
fields=["redeem_against", {"SUM": "loyalty_points", "as": "loyalty_points"}],
group_by="redeem_against",
as_list=True,
frappe.db.sql(
"""
select redeem_against, sum(loyalty_points)
from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and loyalty_points<0 and company=%s
group by redeem_against
""",
(customer, loyalty_program, company),
)
)

View File

@@ -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 = _(

View File

@@ -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(

View File

@@ -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
@@ -1191,9 +1206,9 @@ class PaymentEntry(AccountsController):
continue
if tax.add_deduct_tax == "Add":
included_taxes += flt(tax.base_tax_amount)
included_taxes += tax.base_tax_amount
else:
included_taxes -= flt(tax.base_tax_amount)
included_taxes -= tax.base_tax_amount
return included_taxes
@@ -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
@@ -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()

View File

@@ -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:

View File

@@ -1037,17 +1037,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)
@@ -1113,27 +1110,6 @@ class TestPaymentEntry(ERPNextTestSuite):
self.assertEqual(gl_entries, expected_gl_entries)
def test_payment_entry_with_inclusive_tax(self):
# inclusive tax built server-side: base_tax_amount is None until apply_taxes()
payment_entry = create_payment_entry(paid_amount=1180)
payment_entry.append(
"taxes",
{
"account_head": "_Test Account Service Tax - _TC",
"charge_type": "On Paid Amount",
"rate": 18,
"included_in_paid_amount": 1,
"add_deduct_tax": "Add",
"description": "Service Tax",
},
)
payment_entry.save()
payment_entry.submit()
# 1180 incl 18% => 1000 base + 180 tax
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), 180.0)
self.assertEqual(flt(payment_entry.unallocated_amount, 2), 1000.0)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()

View File

@@ -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()

View File

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

View File

@@ -629,9 +629,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 +1212,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:

View File

@@ -73,10 +73,7 @@ class PeriodClosingVoucher(AccountsController):
if not previous_fiscal_year:
return
# get_fiscal_year() returns a single (name, start_date, end_date) tuple, so the start date
# is [1]; the old [0][1] read the 2nd char of the name ('T'), which MariaDB silently
# coerced to NULL but postgres rejects as an invalid date.
previous_fiscal_year_start_date = previous_fiscal_year[1]
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
previous_fiscal_year_closed = frappe.db.exists(
"Period Closing Voucher",
{
@@ -290,43 +287,40 @@ class PeriodClosingVoucher(AccountsController):
self.accounting_dimension_fields = default_dimensions + get_accounting_dimensions()
def get_gl_entries_for_current_period(self, report_type, only_opening_entries=False, as_iterator=False):
gle = frappe.qb.DocType("GL Entry")
account = frappe.qb.DocType("Account")
fields = [
gle.name,
gle.posting_date,
gle.account,
gle.account_currency,
gle.debit_in_account_currency,
gle.credit_in_account_currency,
gle.debit,
gle.credit,
]
fields += [gle[dimension] for dimension in self.accounting_dimension_fields]
query = (
frappe.qb.from_(gle)
.select(*fields)
.where(
(gle.company == self.company)
& (gle.voucher_type != "Period Closing Voucher")
& (gle.is_cancelled == 0)
& gle.account.isin(
frappe.qb.from_(account).select(account.name).where(account.report_type == report_type)
)
)
)
date_condition = ""
if only_opening_entries:
query = query.where(gle.is_opening == "Yes")
date_condition = "is_opening = 'Yes'"
else:
query = query.where(
gle.posting_date.between(self.period_start_date, self.period_end_date)
& (gle.is_opening == "No")
)
date_condition = f"posting_date BETWEEN '{self.period_start_date}' AND '{self.period_end_date}' and is_opening = 'No'"
return query.run(as_dict=1, as_iterator=as_iterator)
# nosemgrep
return frappe.db.sql(
"""
SELECT
name,
posting_date,
account,
account_currency,
debit_in_account_currency,
credit_in_account_currency,
debit,
credit,
{}
FROM `tabGL Entry`
WHERE
{}
AND company = %s
AND voucher_type != 'Period Closing Voucher'
AND EXISTS(SELECT name FROM `tabAccount` WHERE name = account AND report_type = %s)
AND is_cancelled = 0
""".format(
", ".join(self.accounting_dimension_fields),
date_condition,
),
(self.company, report_type),
as_dict=1,
as_iterator=as_iterator,
)
def set_account_balance_dict(self, gle, acc_bal_dict):
key = self.get_key(gle)

View File

@@ -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()
@@ -55,28 +56,25 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
("Sales - TPC", 400.0, 0.0),
)
pcv_gle = [
tuple(row)
for row in frappe.get_all(
"GL Entry",
filters={"voucher_no": pcv.name},
fields=["account", "debit", "credit"],
order_by="account",
as_list=True,
)
]
pcv_gle = frappe.db.sql(
"""
select account, debit, credit from `tabGL Entry` where voucher_no=%s order by account
""",
(pcv.name),
)
pcv.reload()
self.assertEqual(pcv.gle_processing_status, "Completed")
self.assertEqual(tuple(pcv_gle), expected_gle)
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",
@@ -87,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",
@@ -110,16 +108,14 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
("Sales - TPC", 200.0, 0.0, cost_center2),
)
pcv_gle = [
tuple(row)
for row in frappe.get_all(
"GL Entry",
filters={"voucher_no": pcv.name},
fields=["account", "debit", "credit", "cost_center"],
order_by="account, cost_center",
as_list=True,
)
]
pcv_gle = frappe.db.sql(
"""
select account, debit, credit, cost_center
from `tabGL Entry` where voucher_no=%s
order by account, cost_center
""",
(pcv.name),
)
self.assertSequenceEqual(pcv_gle, expected_gle)
@@ -134,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,
@@ -155,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()
@@ -172,21 +169,19 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
("Sales - TPC", 400.0, 0.0, jv.finance_book),
)
pcv_gle = [
tuple(row)
for row in frappe.get_all(
"GL Entry",
filters={"voucher_no": pcv.name},
fields=["account", "debit", "credit", "finance_book"],
order_by="account, finance_book",
as_list=True,
)
]
pcv_gle = frappe.db.sql(
"""
select account, debit, credit, finance_book
from `tabGL Entry` where voucher_no=%s
order by account, finance_book
""",
(pcv.name),
)
# compare order-independently: postgres and MariaDB order NULL finance_book differently
self.assertSequenceEqual(sorted(pcv_gle, key=str), sorted(expected_gle, key=str))
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")
@@ -197,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")
@@ -215,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()
@@ -228,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()
@@ -258,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()
@@ -297,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",
@@ -343,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(
@@ -351,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()
@@ -367,15 +364,32 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
posting_date="2022-01-01",
)
totals_after_cancel = frappe.get_all(
"GL Entry",
filters={"voucher_type": "Journal Entry", "voucher_no": jv.name, "is_cancelled": 0},
fields=[{"SUM": "debit", "as": "total_debit"}, {"SUM": "credit", "as": "total_credit"}],
totals_after_cancel = frappe.db.sql(
"""
select sum(debit) as total_debit, sum(credit) as total_credit
from `tabGL Entry`
where voucher_type=%s and voucher_no=%s and is_cancelled=0
""",
("Journal Entry", jv.name),
as_dict=True,
)[0]
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(
{

View File

@@ -295,7 +295,7 @@ def get_payments(invoices):
.groupby(SalesInvoicePayment.mode_of_payment)
.select(
SalesInvoicePayment.mode_of_payment,
fn.Max(SalesInvoicePayment.account).as_("account"),
SalesInvoicePayment.account,
fn.Sum(SalesInvoicePayment.amount).as_("amount"),
)
)
@@ -419,7 +419,7 @@ def build_invoice_query(invoice_doctype, user, pos_profile, start, end):
InvoiceDocType.account_for_change_amount,
InvoiceDocType.is_return,
InvoiceDocType.return_against,
fn.CombineDatetime(InvoiceDocType.posting_date, InvoiceDocType.posting_time).as_("timestamp"),
fn.Timestamp(InvoiceDocType.posting_date, InvoiceDocType.posting_time).as_("timestamp"),
ConstantColumn(invoice_doctype).as_("doctype"),
)
.where(
@@ -428,8 +428,8 @@ def build_invoice_query(invoice_doctype, user, pos_profile, start, end):
& (InvoiceDocType.is_pos == 1)
& (InvoiceDocType.pos_profile == pos_profile)
& (
(fn.CombineDatetime(InvoiceDocType.posting_date, InvoiceDocType.posting_time) >= start)
& (fn.CombineDatetime(InvoiceDocType.posting_date, InvoiceDocType.posting_time) <= end)
(fn.Timestamp(InvoiceDocType.posting_date, InvoiceDocType.posting_time) >= start)
& (fn.Timestamp(InvoiceDocType.posting_date, InvoiceDocType.posting_time) <= end)
)
)
)

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder.functions import IfNull, Lower, Sum
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
from frappe.utils.nestedset import get_descendants_of
@@ -505,20 +505,19 @@ class POSInvoice(SalesInvoice):
if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos:
POI = frappe.qb.DocType("POS Invoice Item")
s = sr.lower()
serial_no_exists = (
frappe.qb.from_(POI)
.select(POI.name)
.where(POI.parent == self.return_against)
.where(
(Lower(POI.serial_no) == s)
| Lower(POI.serial_no).like(f"{s}\n%")
| Lower(POI.serial_no).like(f"%\n{s}")
| Lower(POI.serial_no).like(f"%\n{s}\n%")
)
.limit(1)
.run()
serial_no_exists = frappe.db.sql(
"""
SELECT name
FROM `tabPOS Invoice Item`
WHERE
parent = %s
and (serial_no = %s
or serial_no like %s
or serial_no like %s
or serial_no like %s
)
""",
(self.return_against, sr, sr + "\n%", "%\n" + sr, "%\n" + sr + "\n%"),
)
if not serial_no_exists:
@@ -964,9 +963,15 @@ def get_bundle_availability(bundle_item_code, warehouse):
def get_bin_qty(item_code, warehouse):
actual_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty")
bin_qty = frappe.db.sql(
"""select actual_qty from `tabBin`
where item_code = %s and warehouse = %s
limit 1""",
(item_code, warehouse),
as_dict=1,
)
return actual_qty or 0
return bin_qty[0].actual_qty or 0 if bin_qty else 0
def get_pos_reserved_qty(item_code, warehouse):

View File

@@ -118,21 +118,14 @@ class POSProfile(Document):
def validate_default_profile(self):
for row in self.applicable_for_users:
pfu = frappe.qb.DocType("POS Profile User")
pf = frappe.qb.DocType("POS Profile")
res = (
frappe.qb.from_(pfu)
.inner_join(pf)
.on(pf.name == pfu.parent)
.select(pf.name)
.where(
(pfu.user == row.user)
& (pf.name != self.name)
& (pf.company == self.company)
& (pfu.default == 1)
& (pf.disabled == 0)
)
.run()
res = frappe.db.sql(
"""select pf.name
from
`tabPOS Profile User` pfu, `tabPOS Profile` pf
where
pf.name = pfu.parent and pfu.user = %s and pf.name != %s and pf.company = %s
and pfu.default=1 and pf.disabled = 0""",
(row.user, self.name, self.company),
)
if row.default and res:
@@ -272,11 +265,10 @@ def get_permitted_nodes(group_type):
def get_child_nodes(group_type, root):
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
return frappe.get_all(
group_type,
filters={"lft": [">=", lft], "rgt": ["<=", rgt]},
fields=["name", "lft", "rgt"],
order_by="lft",
return frappe.db.sql(
f""" Select name, lft, rgt from `tab{group_type}` where
lft >= {lft} and rgt <= {rgt} order by lft""",
as_dict=1,
)
@@ -286,33 +278,40 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
user = frappe.session["user"]
company = filters.get("company") or frappe.defaults.get_user_default("company")
pf = frappe.qb.DocType("POS Profile")
pfu = frappe.qb.DocType("POS Profile User")
args = {
"user": user,
"start": start,
"company": company,
"page_len": page_len,
"txt": "%%%s%%" % txt,
}
pos_profile = (
frappe.qb.from_(pf)
.inner_join(pfu)
.on(pfu.parent == pf.name)
.select(pf.name)
.where((pfu.user == user) & (pf.company == company) & pf.name.like(f"%{txt}%") & (pf.disabled == 0))
.limit(page_len)
.offset(start)
.run()
pos_profile = frappe.db.sql(
"""select pf.name
from
`tabPOS Profile` pf, `tabPOS Profile User` pfu
where
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
and (pf.name like %(txt)s)
and pf.disabled = 0 limit %(page_len)s offset %(start)s""",
args,
)
if not pos_profile:
pos_profile = (
frappe.qb.from_(pf)
.left_join(pfu)
.on(pf.name == pfu.parent)
.select(pf.name)
.where(
(pfu.user.isnull() | (pfu.user == ""))
& (pf.company == company)
& pf.name.like(f"%{txt}%")
& (pf.disabled == 0)
)
.run()
del args["user"]
pos_profile = frappe.db.sql(
"""select pf.name
from
`tabPOS Profile` pf left join `tabPOS Profile User` pfu
on
pf.name = pfu.parent
where
ifnull(pfu.user, '') = ''
and pf.company = %(company)s
and pf.name like %(txt)s
and pf.disabled = 0""",
args,
)
return pos_profile

View File

@@ -114,7 +114,7 @@ def _get_pricing_rules(apply_on, args, values):
if apply_on_field == "item_code":
if args.get("uom", None):
item_conditions += (
" and ({child_doc}.uom={item_uom} or COALESCE({child_doc}.uom, '')='')".format(
" and ({child_doc}.uom={item_uom} or IFNULL({child_doc}.uom, '')='')".format(
child_doc=child_doc, item_uom=frappe.db.escape(args.get("uom"))
)
)
@@ -127,7 +127,7 @@ def _get_pricing_rules(apply_on, args, values):
elif apply_on_field == "item_group":
item_conditions = _get_tree_conditions(args, "Item Group", child_doc, False)
if args.get("uom", None):
item_conditions += " and ({child_doc}.uom={item_uom} or COALESCE({child_doc}.uom, '')='')".format(
item_conditions += " and ({child_doc}.uom={item_uom} or IFNULL({child_doc}.uom, '')='')".format(
child_doc=child_doc, item_uom=frappe.db.escape(args.get("uom"))
)
@@ -139,7 +139,7 @@ def _get_pricing_rules(apply_on, args, values):
if not args.price_list:
args.price_list = None
conditions += " and coalesce(`tabPricing Rule`.for_price_list, '') in (%(price_list)s, '')"
conditions += " and ifnull(`tabPricing Rule`.for_price_list, '') in (%(price_list)s, '')"
values["price_list"] = args.get("price_list")
pricing_rules = (
@@ -195,8 +195,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
except TypeError:
frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.get_all(
parenttype, filters={"lft": ["<=", lft], "rgt": [">=", rgt]}, pluck="name"
parent_groups = frappe.db.sql_list(
"""select name from `tab{}`
where lft<={} and rgt>={}""".format(parenttype, "%s", "%s"),
(lft, rgt),
)
if parenttype in ["Customer Group", "Item Group", "Territory"]:
@@ -215,14 +217,14 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
if parent_groups:
if allow_blank:
parent_groups.append("")
condition = "coalesce({table}.{field}, '') in ({parent_groups})".format(
condition = "ifnull({table}.{field}, '') in ({parent_groups})".format(
table=table, field=field, parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups)
)
frappe.flags.tree_conditions[key] = condition
elif allow_blank:
condition = f"coalesce({table}.{field}, '') = ''"
condition = f"ifnull({table}.{field}, '') = ''"
return condition
@@ -230,10 +232,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
def get_other_conditions(conditions, values, args):
for field in ["company", "customer", "supplier", "campaign", "sales_partner"]:
if args.get(field):
conditions += f" and coalesce(`tabPricing Rule`.{field}, '') in (%({field})s, '')"
conditions += f" and ifnull(`tabPricing Rule`.{field}, '') in (%({field})s, '')"
values[field] = args.get(field)
else:
conditions += f" and coalesce(`tabPricing Rule`.{field}, '') = ''"
conditions += f" and ifnull(`tabPricing Rule`.{field}, '') = ''"
for parenttype in ["Customer Group", "Territory", "Supplier Group"]:
group_condition = _get_tree_conditions(args, parenttype, "`tabPricing Rule`")
@@ -246,8 +248,8 @@ def get_other_conditions(conditions, values, args):
or frappe.get_value(args.get("doctype"), args.get("name"), "posting_date", ignore=True)
)
if date:
conditions += """ and %(transaction_date)s between coalesce(`tabPricing Rule`.valid_from, '2000-01-01')
and coalesce(`tabPricing Rule`.valid_upto, '2500-12-31')"""
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = date
if args.get("doctype") in [
@@ -262,9 +264,9 @@ def get_other_conditions(conditions, values, args):
"POS Invoice",
"POS Invoice Item",
]:
conditions += """ and coalesce(`tabPricing Rule`.selling, 0) = 1"""
conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1"""
else:
conditions += """ and coalesce(`tabPricing Rule`.buying, 0) = 1"""
conditions += """ and ifnull(`tabPricing Rule`.buying, 0) = 1"""
return conditions

View File

@@ -431,9 +431,7 @@ def reconcile(doc: None | str = None) -> None:
# Update reconciled flag
allocation_names = [x.name for x in allocations]
ppa = qb.DocType("Process Payment Reconciliation Log Allocations")
qb.update(ppa).set(ppa.reconciled, 1).where(
ppa.name.isin(allocation_names)
).run() # smallint, not bool
qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run()
# Update reconciled count
reconciled_count = frappe.db.count(

View File

@@ -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))

View File

@@ -16,7 +16,6 @@
"categorize_by",
"cost_center",
"territory",
"show_opening_entries",
"ignore_exchange_rate_revaluation_journals",
"ignore_cr_dr_notes",
"column_break_14",
@@ -415,17 +414,10 @@
"fieldtype": "Link",
"label": "Print Format",
"options": "Print Format"
},
{
"default": "0",
"depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "show_opening_entries",
"fieldtype": "Check",
"label": "Show Opening Entries"
}
],
"links": [],
"modified": "2026-06-01 15:37:07.660442",
"modified": "2025-10-07 12:19:20.719898",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -6,6 +6,7 @@ import copy
import frappe
from frappe import _
from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document
from frappe.utils import add_days, add_months, format_date, getdate, today
from frappe.utils.jinja import validate_template
@@ -19,7 +20,6 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
execute as get_ageing,
)
from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa
from erpnext.utilities.query import get_match_conditions_qb
class ProcessStatementOfAccounts(Document):
@@ -75,7 +75,6 @@ class ProcessStatementOfAccounts(Document):
sender: DF.Link | None
show_future_payments: DF.Check
show_net_values_in_party_account: DF.Check
show_opening_entries: DF.Check
show_remarks: DF.Check
start_date: DF.Date | None
subject: DF.Data | None
@@ -271,7 +270,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
"categorize_by": doc.categorize_by,
"currency": doc.currency,
"project": [p.project_name for p in doc.project],
"show_opening_entries": doc.show_opening_entries,
"show_opening_entries": 0,
"include_default_book_entries": 0,
"tax_id": tax_id if tax_id else None,
"show_net_values_in_party_account": doc.show_net_values_in_party_account,
@@ -366,19 +365,15 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
def get_customers_based_on_sales_person(sales_person):
lft, rgt = frappe.db.get_value("Sales Person", 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 == "Customer")
& steam.sales_person.isin(
frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt))
)
)
.run(as_dict=1)
records = frappe.db.sql(
"""
select distinct parent, parenttype
from `tabSales Team` steam
where parenttype = 'Customer'
and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
""",
(lft, rgt),
as_dict=1,
)
sales_person_records = frappe._dict()
for d in records:
@@ -473,30 +468,31 @@ def get_customer_emails(customer_name: str, primary_mandatory: str | int, billin
frappe.has_permission("Customer", "read", customer_name, throw=True)
email = frappe.qb.DocType("Contact Email")
link = frappe.qb.DocType("Dynamic Link")
contact = frappe.qb.DocType("Contact")
query = (
frappe.qb.from_(email)
.join(link)
.on(email.parent == link.parent)
.join(contact)
.on(contact.name == link.parent)
.select(email.email_id)
.where(
(link.link_doctype == "Customer")
& (link.link_name == customer_name)
& (contact.is_billing_contact == 1)
)
.orderby(contact.creation, order=frappe.qb.desc)
billing_email = frappe.db.sql(
"""
SELECT
email.email_id
FROM
`tabContact Email` AS email
JOIN
`tabDynamic Link` AS link
ON
email.parent=link.parent
JOIN
`tabContact` AS contact
ON
contact.name=link.parent
WHERE
link.link_doctype='Customer'
and link.link_name=%s
and contact.is_billing_contact=1
{mcond}
ORDER BY
contact.creation desc
""".format(mcond=get_match_cond("Contact")),
customer_name,
)
for condition in get_match_conditions_qb("Contact", table=contact):
query = query.where(condition)
billing_email = query.run()
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:
frappe.throw(_("No billing email found for customer: {0}").format(customer_name))

View File

@@ -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")

View File

@@ -524,11 +524,16 @@ class PurchaseInvoice(BuyingController):
def check_prev_docstatus(self):
for d in self.get("items"):
if d.purchase_order:
submitted = frappe.db.exists("Purchase Order", {"docstatus": 1, "name": d.purchase_order})
submitted = frappe.db.sql(
"select name from `tabPurchase Order` where docstatus = 1 and name = %s", d.purchase_order
)
if not submitted:
frappe.throw(_("Purchase Order {0} is not submitted").format(d.purchase_order))
if d.purchase_receipt:
submitted = frappe.db.exists("Purchase Receipt", {"docstatus": 1, "name": d.purchase_receipt})
submitted = frappe.db.sql(
"select name from `tabPurchase Receipt` where docstatus = 1 and name = %s",
d.purchase_receipt,
)
if not submitted:
frappe.throw(_("Purchase Receipt {0} is not submitted").format(d.purchase_receipt))
@@ -796,20 +801,25 @@ class PurchaseInvoice(BuyingController):
if cint(frappe.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
pi = frappe.get_all(
"Purchase Invoice",
filters={
pi = frappe.db.sql(
"""select name from `tabPurchase Invoice`
where
bill_no = %(bill_no)s
and supplier = %(supplier)s
and name != %(name)s
and docstatus < 2
and posting_date between %(year_start_date)s and %(year_end_date)s""",
{
"bill_no": self.bill_no,
"supplier": self.supplier,
"name": ["!=", self.name],
"docstatus": ["<", 2],
"posting_date": ["between", [fiscal_year.year_start_date, fiscal_year.year_end_date]],
"name": self.name,
"year_start_date": fiscal_year.year_start_date,
"year_end_date": fiscal_year.year_end_date,
},
pluck="name",
)
if pi:
pi = pi[0]
pi = pi[0][0]
frappe.throw(
_("Supplier Invoice No exists in Purchase Invoice {0}").format(

View File

@@ -55,13 +55,10 @@ class ExpenseAccountService:
else:
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
if item.purchase_receipt:
negative_expense_booked_in_pr = frappe.db.exists(
"GL Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": item.purchase_receipt,
"account": stock_not_billed_account,
},
negative_expense_booked_in_pr = frappe.db.sql(
"""select name from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no=%s and account = %s""",
(item.purchase_receipt, stock_not_billed_account),
)
if negative_expense_booked_in_pr:

View File

@@ -395,14 +395,10 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
):
# Post reverse entry for Stock-Received-But-Not-Billed if booked in Purchase Receipt
if item.purchase_receipt and valuation_tax_accounts:
negative_expense_booked_in_pr = frappe.get_all(
"GL Entry",
filters={
"voucher_type": "Purchase Receipt",
"voucher_no": item.purchase_receipt,
"account": ["in", valuation_tax_accounts],
},
pluck="name",
negative_expense_booked_in_pr = frappe.db.sql(
"""select name from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""",
(item.purchase_receipt, valuation_tax_accounts),
)
(

View File

@@ -11,7 +11,6 @@
"add_deduct_tax",
"charge_type",
"row_id",
"allocate_full_amount_to_stock_items",
"included_in_print_rate",
"included_in_paid_amount",
"col_break1",
@@ -79,14 +78,6 @@
"oldfieldname": "row_id",
"oldfieldtype": "Data"
},
{
"default": "1",
"depends_on": "eval:doc.charge_type=='Actual' && ['Valuation', 'Valuation and Total'].includes(doc.category)",
"description": "If checked, the entire amount (e.g. Freight) is allocated to the valuation of stock & asset items only. If unchecked, the amount is distributed across all items and the portion belonging to non-stock items is not added to valuation.",
"fieldname": "allocate_full_amount_to_stock_items",
"fieldtype": "Check",
"label": "Allocate Full Amount to Stock Items"
},
{
"default": "0",
"description": "If checked, the tax amount will be considered as already included in the Print Rate / Print Amount",

View File

@@ -200,11 +200,106 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
set_purchase_references(target)
def update_details(source_doc, target_doc, source_parent):
def _validate_address_link(address, link_doctype, link_name):
return frappe.db.get_value(
"Dynamic Link",
{
"parent": address,
"parenttype": "Address",
"link_doctype": link_doctype,
"link_name": link_name,
},
"parent",
)
target_doc.inter_company_invoice_reference = source_doc.name
if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]:
_apply_purchase_party_details(target_doc, source_doc, details)
currency = frappe.db.get_value("Supplier", details.get("party"), "default_currency")
target_doc.company = details.get("company")
target_doc.supplier = details.get("party")
target_doc.is_internal_supplier = 1
target_doc.ignore_pricing_rule = 1
target_doc.buying_price_list = source_doc.selling_price_list
# Invert Addresses
if source_doc.company_address and _validate_address_link(
source_doc.company_address, "Supplier", details.get("party")
):
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
if source_doc.dispatch_address_name and _validate_address_link(
source_doc.dispatch_address_name, "Company", details.get("company")
):
update_address(
target_doc,
"dispatch_address",
"dispatch_address_display",
source_doc.dispatch_address_name,
)
if source_doc.shipping_address_name and _validate_address_link(
source_doc.shipping_address_name, "Company", details.get("company")
):
update_address(
target_doc,
"shipping_address",
"shipping_address_display",
source_doc.shipping_address_name,
)
if source_doc.customer_address and _validate_address_link(
source_doc.customer_address, "Company", details.get("company")
):
update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
)
if currency:
target_doc.currency = currency
update_taxes(
target_doc,
party=target_doc.supplier,
party_type="Supplier",
company=target_doc.company,
doctype=target_doc.doctype,
party_address=target_doc.supplier_address,
company_address=target_doc.shipping_address,
)
else:
_apply_sales_party_details(target_doc, source_doc, details)
currency = frappe.db.get_value("Customer", details.get("party"), "default_currency")
target_doc.company = details.get("company")
target_doc.customer = details.get("party")
target_doc.selling_price_list = source_doc.buying_price_list
if source_doc.supplier_address and _validate_address_link(
source_doc.supplier_address, "Company", details.get("company")
):
update_address(
target_doc, "company_address", "company_address_display", source_doc.supplier_address
)
if source_doc.shipping_address and _validate_address_link(
source_doc.shipping_address, "Customer", details.get("party")
):
update_address(
target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address
)
if source_doc.shipping_address and _validate_address_link(
source_doc.shipping_address, "Customer", details.get("party")
):
update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address)
if currency:
target_doc.currency = currency
update_taxes(
target_doc,
party=target_doc.customer,
party_type="Customer",
company=target_doc.company,
doctype=target_doc.doctype,
party_address=target_doc.customer_address,
company_address=target_doc.company_address,
shipping_address_name=target_doc.shipping_address_name,
)
def update_item(source, target, source_parent):
target.qty = flt(source.qty) - received_items.get(source.name, 0.0)
@@ -283,97 +378,6 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
return doclist
def _get_linked_address(address, link_doctype, link_name):
return frappe.db.get_value(
"Dynamic Link",
{
"parent": address,
"parenttype": "Address",
"link_doctype": link_doctype,
"link_name": link_name,
},
"parent",
)
def _apply_purchase_party_details(target_doc, source_doc, details):
currency = frappe.db.get_value("Supplier", details.get("party"), "default_currency")
target_doc.company = details.get("company")
target_doc.supplier = details.get("party")
target_doc.is_internal_supplier = 1
target_doc.ignore_pricing_rule = 1
target_doc.buying_price_list = source_doc.selling_price_list
# Invert Addresses
if source_doc.company_address and _get_linked_address(
source_doc.company_address, "Supplier", details.get("party")
):
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
if source_doc.dispatch_address_name and _get_linked_address(
source_doc.dispatch_address_name, "Company", details.get("company")
):
update_address(
target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name
)
if source_doc.shipping_address_name and _get_linked_address(
source_doc.shipping_address_name, "Company", details.get("company")
):
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name
)
if source_doc.customer_address and _get_linked_address(
source_doc.customer_address, "Company", details.get("company")
):
update_address(target_doc, "billing_address", "billing_address_display", source_doc.customer_address)
if currency:
target_doc.currency = currency
update_taxes(
target_doc,
party=target_doc.supplier,
party_type="Supplier",
company=target_doc.company,
doctype=target_doc.doctype,
party_address=target_doc.supplier_address,
company_address=target_doc.shipping_address,
)
def _apply_sales_party_details(target_doc, source_doc, details):
currency = frappe.db.get_value("Customer", details.get("party"), "default_currency")
target_doc.company = details.get("company")
target_doc.customer = details.get("party")
target_doc.selling_price_list = source_doc.buying_price_list
if source_doc.supplier_address and _get_linked_address(
source_doc.supplier_address, "Company", details.get("company")
):
update_address(target_doc, "company_address", "company_address_display", source_doc.supplier_address)
if source_doc.shipping_address and _get_linked_address(
source_doc.shipping_address, "Customer", details.get("party")
):
update_address(target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address)
if source_doc.shipping_address and _get_linked_address(
source_doc.shipping_address, "Customer", details.get("party")
):
update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address)
if currency:
target_doc.currency = currency
update_taxes(
target_doc,
party=target_doc.customer,
party_type="Customer",
company=target_doc.company,
doctype=target_doc.doctype,
party_address=target_doc.customer_address,
company_address=target_doc.company_address,
shipping_address_name=target_doc.shipping_address_name,
)
@frappe.whitelist()
def get_received_items(reference_name: str, doctype: str, reference_fieldname: str):
reference_field = "inter_company_invoice_reference"

View File

@@ -93,7 +93,54 @@ class SalesInvoiceGLComposer(BaseGLComposer):
if enable_discount_accounting:
for item in doc.get("items"):
if item.get("discount_amount") and item.get("discount_account"):
self._append_item_discount_gl_entries(item, gl_entries)
discount_amount = item.discount_amount * item.qty
income_account = (
item.income_account
if (not item.enable_deferred_revenue or doc.is_return)
else item.deferred_revenue_account
)
account_currency = get_account_currency(item.discount_account)
gl_entries.append(
doc.get_gl_dict(
{
"account": item.discount_account,
"against": doc.customer,
"debit": flt(
discount_amount * doc.get("conversion_rate"),
item.precision("discount_amount"),
),
"debit_in_transaction_currency": flt(
discount_amount, item.precision("discount_amount")
),
"cost_center": item.cost_center,
"project": item.project,
},
account_currency,
item=item,
)
)
account_currency = get_account_currency(income_account)
gl_entries.append(
doc.get_gl_dict(
{
"account": income_account,
"against": doc.customer,
"credit": flt(
discount_amount * doc.get("conversion_rate"),
item.precision("discount_amount"),
),
"credit_in_transaction_currency": flt(
discount_amount, item.precision("discount_amount")
),
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
)
if (
(enable_discount_accounting or doc.get("is_cash_or_non_trade_discount"))
@@ -112,143 +159,81 @@ class SalesInvoiceGLComposer(BaseGLComposer):
)
)
def _append_item_discount_gl_entries(self, item, gl_entries) -> None:
doc = self.doc
discount_amount = item.discount_amount * item.qty
income_account = (
item.income_account
if (not item.enable_deferred_revenue or doc.is_return)
else item.deferred_revenue_account
)
account_currency = get_account_currency(item.discount_account)
gl_entries.append(
doc.get_gl_dict(
{
"account": item.discount_account,
"against": doc.customer,
"debit": flt(
discount_amount * doc.get("conversion_rate"),
item.precision("discount_amount"),
),
"debit_in_transaction_currency": flt(discount_amount, item.precision("discount_amount")),
"cost_center": item.cost_center,
"project": item.project,
},
account_currency,
item=item,
)
)
account_currency = get_account_currency(income_account)
gl_entries.append(
doc.get_gl_dict(
{
"account": income_account,
"against": doc.customer,
"credit": flt(
discount_amount * doc.get("conversion_rate"),
item.precision("discount_amount"),
),
"credit_in_transaction_currency": flt(discount_amount, item.precision("discount_amount")),
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
)
def stock_delivered_but_not_billed_gl_entries(self, gl_entries):
doc = self.doc
if doc.update_stock or not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
return
for item in doc.get("items"):
booking = self._get_sdbnb_booking_for_item(item)
if booking:
self._append_sdbnb_gl_entries(item, booking, gl_entries)
if not item.delivery_note and not item.dn_detail:
continue
def _get_sdbnb_booking_for_item(self, item) -> dict | None:
"""SDBNB account and valuation to reverse for a billed-from-delivery-note item, if any."""
if not item.delivery_note and not item.dn_detail:
return None
if not frappe.get_cached_value("Item", item.item_code, "is_stock_item"):
continue
if not frappe.get_cached_value("Item", item.item_code, "is_stock_item"):
return None
dn_expense_account = frappe.get_cached_value("Delivery Note Item", item.dn_detail, "expense_account")
if not self._is_sdbnb_reversal(dn_expense_account, item):
return None
delivery_note = item.delivery_note or frappe.get_cached_value(
"Delivery Note Item", item.dn_detail, "parent"
)
if not delivery_note:
return None
item_g = frappe.get_cached_value(
"Stock Ledger Entry",
{
"voucher_no": delivery_note,
"voucher_detail_no": item.dn_detail,
"item_code": item.item_code,
"is_cancelled": 0,
},
["stock_value_difference", "actual_qty"],
as_dict=True,
)
if not item_g or not flt(item_g.actual_qty):
return None
valuation_rate = flt(item_g.stock_value_difference) / flt(item_g.actual_qty)
return {
"dn_expense_account": dn_expense_account,
"valuation_amount": valuation_rate * item.stock_qty,
}
def _is_sdbnb_reversal(self, dn_expense_account, item) -> bool:
"""True when the DN booked to an SDBNB account distinct from the item's expense account."""
return bool(
dn_expense_account
and frappe.get_cached_value("Account", dn_expense_account, "account_type")
== "Stock Delivered But Not Billed"
and item.expense_account
and dn_expense_account != item.expense_account
)
def _append_sdbnb_gl_entries(self, item, booking, gl_entries) -> None:
dn_expense_account = booking["dn_expense_account"]
valuation_amount = booking["valuation_amount"]
dn_account_currency = get_account_currency(dn_expense_account)
item_account_currency = get_account_currency(item.expense_account)
gl_entries.append(
self.get_gl_dict(
{
"account": dn_expense_account,
"against": item.expense_account,
"credit": flt(valuation_amount),
"credit_in_account_currency": flt(valuation_amount),
"cost_center": item.cost_center,
},
dn_account_currency,
item=item,
dn_expense_account = frappe.get_cached_value(
"Delivery Note Item", item.dn_detail, "expense_account"
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": item.expense_account,
"against": dn_expense_account,
"debit": flt(valuation_amount),
"debit_in_account_currency": flt(valuation_amount),
"cost_center": item.cost_center,
},
item_account_currency,
item=item,
if (
not dn_expense_account
or frappe.get_cached_value("Account", dn_expense_account, "account_type")
!= "Stock Delivered But Not Billed"
or not item.expense_account
or dn_expense_account == item.expense_account
):
continue
delivery_note = item.delivery_note or frappe.get_cached_value(
"Delivery Note Item", item.dn_detail, "parent"
)
if not delivery_note:
continue
item_g = frappe.get_cached_value(
"Stock Ledger Entry",
{
"voucher_no": delivery_note,
"voucher_detail_no": item.dn_detail,
"item_code": item.item_code,
"is_cancelled": 0,
},
["stock_value_difference", "actual_qty"],
as_dict=True,
)
if not item_g or not flt(item_g.actual_qty):
continue
valuation_rate = flt(item_g.stock_value_difference) / flt(item_g.actual_qty)
valuation_amount = valuation_rate * item.stock_qty
dn_account_currency = get_account_currency(dn_expense_account)
item_account_currency = get_account_currency(item.expense_account)
gl_entries.append(
self.get_gl_dict(
{
"account": dn_expense_account,
"against": item.expense_account,
"credit": flt(valuation_amount),
"credit_in_account_currency": flt(valuation_amount),
"cost_center": item.cost_center,
},
dn_account_currency,
item=item,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": item.expense_account,
"against": dn_expense_account,
"debit": flt(valuation_amount),
"debit_in_account_currency": flt(valuation_amount),
"cost_center": item.cost_center,
},
item_account_currency,
item=item,
)
)
)
def make_customer_gl_entry(self, gl_entries):
doc = self.doc
@@ -265,6 +250,10 @@ class SalesInvoiceGLComposer(BaseGLComposer):
)
if grand_total and not doc.is_internal_transfer():
against_voucher = doc.name
if doc.is_return and doc.return_against and not doc.update_outstanding_for_self:
against_voucher = doc.return_against
# Did not use base_grand_total to book rounding loss gle
gl_entries.append(
self.get_gl_dict(
@@ -275,11 +264,11 @@ class SalesInvoiceGLComposer(BaseGLComposer):
"due_date": doc.due_date,
"against": doc.against_income_account,
"debit": base_grand_total,
"debit_in_account_currency": self._get_amount_in_account_currency(
doc.party_account_currency, base_grand_total, grand_total
),
"debit_in_account_currency": base_grand_total
if doc.party_account_currency == doc.company_currency
else grand_total,
"debit_in_transaction_currency": grand_total,
"against_voucher": self._resolve_against_voucher(),
"against_voucher": against_voucher,
"against_voucher_type": doc.doctype,
"cost_center": doc.cost_center,
"project": doc.project,
@@ -307,10 +296,10 @@ class SalesInvoiceGLComposer(BaseGLComposer):
"account": tax.account_head,
"against": doc.customer,
"credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")),
"credit_in_account_currency": self._get_amount_in_account_currency(
account_currency,
flt(base_amount, tax.precision("base_tax_amount_after_discount_amount")),
flt(amount, tax.precision("tax_amount_after_discount_amount")),
"credit_in_account_currency": (
flt(base_amount, tax.precision("base_tax_amount_after_discount_amount"))
if account_currency == doc.company_currency
else flt(amount, tax.precision("tax_amount_after_discount_amount"))
),
"credit_in_transaction_currency": flt(
amount, tax.precision("tax_amount_after_discount_amount")
@@ -352,57 +341,53 @@ class SalesInvoiceGLComposer(BaseGLComposer):
)
for item in doc.get("items"):
if not (
if (
flt(item.base_net_amount, item.precision("base_net_amount"))
or item.is_fixed_asset
or enable_discount_accounting
):
continue
# Do not book income for transfer within same company
if doc.is_internal_transfer():
continue
# Do not book income for transfer within same company
if doc.is_internal_transfer():
continue
if item.is_fixed_asset and item.asset:
self.get_gl_entries_for_fixed_asset(item, gl_entries)
else:
income_account = (
item.income_account
if (not item.enable_deferred_revenue or doc.is_return)
else item.deferred_revenue_account
)
if item.is_fixed_asset and item.asset:
self.get_gl_entries_for_fixed_asset(item, gl_entries)
else:
self._append_item_income_gl_entry(item, gl_entries, tax_service, enable_discount_accounting)
amount, base_amount = tax_service.get_amount_and_base_amount(
item, enable_discount_accounting
)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict(
{
"account": income_account,
"against": doc.customer,
"credit": flt(base_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (
flt(base_amount, item.precision("base_net_amount"))
if account_currency == doc.company_currency
else flt(amount, item.precision("net_amount"))
),
"credit_in_transaction_currency": flt(amount, item.precision("net_amount")),
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
)
# expense account gl entries
if cint(doc.update_stock) and erpnext.is_perpetual_inventory_enabled(doc.company):
gl_entries += super(SalesInvoice, doc).get_gl_entries()
def _append_item_income_gl_entry(self, item, gl_entries, tax_service, enable_discount_accounting) -> None:
doc = self.doc
income_account = (
item.income_account
if (not item.enable_deferred_revenue or doc.is_return)
else item.deferred_revenue_account
)
amount, base_amount = tax_service.get_amount_and_base_amount(item, enable_discount_accounting)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict(
{
"account": income_account,
"against": doc.customer,
"credit": flt(base_amount, item.precision("base_net_amount")),
"credit_in_account_currency": self._get_amount_in_account_currency(
account_currency,
flt(base_amount, item.precision("base_net_amount")),
flt(amount, item.precision("net_amount")),
),
"credit_in_transaction_currency": flt(amount, item.precision("net_amount")),
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
)
def get_gl_entries_for_fixed_asset(self, item, gl_entries):
doc = self.doc
asset = frappe.get_cached_doc("Asset", item.asset)
@@ -476,6 +461,10 @@ class SalesInvoiceGLComposer(BaseGLComposer):
if skip_change_gl_entries and payment_mode.account == doc.account_for_change_amount:
payment_mode.base_amount -= flt(doc.change_amount)
against_voucher = doc.name
if doc.is_return and doc.return_against and not doc.update_outstanding_for_self:
against_voucher = doc.return_against
if payment_mode.base_amount:
# POS, make payment entries
gl_entries.append(
@@ -486,11 +475,11 @@ class SalesInvoiceGLComposer(BaseGLComposer):
"party": doc.customer,
"against": payment_mode.account,
"credit": payment_mode.base_amount,
"credit_in_account_currency": self._get_amount_in_account_currency(
doc.party_account_currency, payment_mode.base_amount, payment_mode.amount
),
"credit_in_account_currency": payment_mode.base_amount
if doc.party_account_currency == doc.company_currency
else payment_mode.amount,
"credit_in_transaction_currency": payment_mode.amount,
"against_voucher": self._resolve_against_voucher(),
"against_voucher": against_voucher,
"against_voucher_type": doc.doctype,
"cost_center": doc.cost_center,
},
@@ -506,11 +495,9 @@ class SalesInvoiceGLComposer(BaseGLComposer):
"account": payment_mode.account,
"against": doc.customer,
"debit": payment_mode.base_amount,
"debit_in_account_currency": self._get_amount_in_account_currency(
payment_mode_account_currency,
payment_mode.base_amount,
payment_mode.amount,
),
"debit_in_account_currency": payment_mode.base_amount
if payment_mode_account_currency == doc.company_currency
else payment_mode.amount,
"debit_in_transaction_currency": payment_mode.amount,
"cost_center": doc.cost_center,
},
@@ -538,9 +525,9 @@ class SalesInvoiceGLComposer(BaseGLComposer):
"party": doc.customer,
"against": doc.account_for_change_amount,
"debit": flt(doc.base_change_amount),
"debit_in_account_currency": self._get_amount_in_account_currency(
doc.party_account_currency, flt(doc.base_change_amount), flt(doc.change_amount)
),
"debit_in_account_currency": flt(doc.base_change_amount)
if doc.party_account_currency == doc.company_currency
else flt(doc.change_amount),
"debit_in_transaction_currency": flt(doc.change_amount),
"against_voucher": doc.return_against
if cint(doc.is_return) and doc.return_against
@@ -583,10 +570,10 @@ class SalesInvoiceGLComposer(BaseGLComposer):
"party": doc.customer,
"against": doc.write_off_account,
"credit": flt(doc.base_write_off_amount, doc.precision("base_write_off_amount")),
"credit_in_account_currency": self._get_amount_in_account_currency(
doc.party_account_currency,
flt(doc.base_write_off_amount, doc.precision("base_write_off_amount")),
flt(doc.write_off_amount, doc.precision("write_off_amount")),
"credit_in_account_currency": (
flt(doc.base_write_off_amount, doc.precision("base_write_off_amount"))
if doc.party_account_currency == doc.company_currency
else flt(doc.write_off_amount, doc.precision("write_off_amount"))
),
"credit_in_transaction_currency": flt(
doc.write_off_amount, doc.precision("write_off_amount")
@@ -606,10 +593,10 @@ class SalesInvoiceGLComposer(BaseGLComposer):
"account": doc.write_off_account,
"against": doc.customer,
"debit": flt(doc.base_write_off_amount, doc.precision("base_write_off_amount")),
"debit_in_account_currency": self._get_amount_in_account_currency(
write_off_account_currency,
flt(doc.base_write_off_amount, doc.precision("base_write_off_amount")),
flt(doc.write_off_amount, doc.precision("write_off_amount")),
"debit_in_account_currency": (
flt(doc.base_write_off_amount, doc.precision("base_write_off_amount"))
if write_off_account_currency == doc.company_currency
else flt(doc.write_off_amount, doc.precision("write_off_amount"))
),
"debit_in_transaction_currency": flt(
doc.write_off_amount, doc.precision("write_off_amount")
@@ -672,14 +659,3 @@ class SalesInvoiceGLComposer(BaseGLComposer):
item=doc,
)
)
def _get_amount_in_account_currency(self, account_currency, base_amount, transaction_amount):
"""Base amount when the account is in company currency, else the transaction amount."""
return base_amount if account_currency == self.doc.company_currency else transaction_amount
def _resolve_against_voucher(self) -> str:
"""Settle against the original invoice for returns not kept on their own outstanding."""
doc = self.doc
if doc.is_return and doc.return_against and not doc.update_outstanding_for_self:
return doc.return_against
return doc.name

View File

@@ -4,7 +4,7 @@
"""POS helpers for Sales Invoice."""
import frappe
from frappe import _
from frappe import _, msgprint
from frappe.utils import cint, flt, get_link_to_form
@@ -13,140 +13,106 @@ class PartialPaymentValidationError(frappe.ValidationError):
class POSService:
def __init__(self, doc) -> None:
def __init__(self, doc):
self.doc = doc
def set_pos_fields(self, for_validate: bool = False) -> frappe.Document | dict | None:
"""Populate POS-profile fields on the invoice; return the profile, {} or None."""
def set_pos_fields(self, for_validate: bool = False) -> frappe.Document | None:
"""Populate POS-profile fields on the invoice; return the profile or None."""
doc = self.doc
if cint(doc.is_pos) != 1:
return None
self._set_default_change_amount_account()
if not self._ensure_pos_profile():
return None
pos = frappe.get_doc("POS Profile", doc.pos_profile) if doc.pos_profile else {}
if pos:
self._apply_pos_profile(pos, for_validate)
return pos
def _set_default_change_amount_account(self) -> None:
doc = self.doc
if not doc.account_for_change_amount:
doc.account_for_change_amount = frappe.get_cached_value(
"Company", doc.company, "default_cash_account"
)
def _ensure_pos_profile(self) -> bool:
"""Auto-pick a POS Profile for the company; return False if none could be found."""
doc = self.doc
if doc.pos_profile or doc.flags.ignore_pos_profile:
return True
from erpnext.stock.get_item_details import (
ItemDetailsCtx,
get_pos_profile,
get_pos_profile_item_details_,
)
from erpnext.stock.get_item_details import get_pos_profile
if not doc.pos_profile and not doc.flags.ignore_pos_profile:
pos_profile = get_pos_profile(doc.company) or {}
if not pos_profile:
return None
doc.pos_profile = pos_profile.get("name")
pos_profile = get_pos_profile(doc.company) or {}
if not pos_profile:
return False
pos = {}
if doc.pos_profile:
pos = frappe.get_doc("POS Profile", doc.pos_profile)
doc.pos_profile = pos_profile.get("name")
return True
if pos:
if not for_validate:
update_multi_mode_option(doc, pos)
doc.tax_category = pos.get("tax_category")
def _apply_pos_profile(self, pos, for_validate: bool) -> None:
doc = self.doc
if not for_validate:
self._apply_editable_pos_defaults(pos)
if not for_validate and not doc.customer:
doc.customer = pos.customer
if pos.get("account_for_change_amount"):
doc.account_for_change_amount = pos.get("account_for_change_amount")
if not for_validate:
doc.ignore_pricing_rule = pos.ignore_pricing_rule
self._copy_pos_profile_fields(pos, for_validate)
if pos.get("account_for_change_amount"):
doc.account_for_change_amount = pos.get("account_for_change_amount")
if pos.get("company_address"):
doc.company_address = pos.get("company_address")
for fieldname in (
"currency",
"letter_head",
"tc_name",
"company",
"select_print_heading",
"write_off_account",
"taxes_and_charges",
"write_off_cost_center",
"apply_discount_on",
"cost_center",
):
if (not for_validate) or (for_validate and not doc.get(fieldname)):
doc.set(fieldname, pos.get(fieldname))
self._set_selling_price_list(pos)
if pos.get("company_address"):
doc.company_address = pos.get("company_address")
if not for_validate:
self._set_update_stock_from_profile(pos)
if doc.customer:
customer_price_list, customer_group = frappe.get_value(
"Customer", doc.customer, ["default_price_list", "customer_group"]
)
customer_group_price_list = frappe.get_value(
"Customer Group", customer_group, "default_price_list"
)
selling_price_list = (
customer_price_list or customer_group_price_list or pos.get("selling_price_list")
)
else:
selling_price_list = pos.get("selling_price_list")
self._apply_pos_item_defaults(pos, for_validate)
self._set_terms_and_taxes(pos)
if selling_price_list:
doc.set("selling_price_list", selling_price_list)
def _apply_editable_pos_defaults(self, pos) -> None:
"""Profile defaults the user may override; only applied outside validation."""
doc = self.doc
update_multi_mode_option(doc, pos)
doc.tax_category = pos.get("tax_category")
if not doc.customer:
doc.customer = pos.customer
doc.ignore_pricing_rule = pos.ignore_pricing_rule
if not for_validate:
dn_flag = any(d.get("dn_detail") for d in doc.get("items"))
doc.update_stock = 0 if dn_flag else cint(pos.get("update_stock"))
def _copy_pos_profile_fields(self, pos, for_validate: bool) -> None:
doc = self.doc
for fieldname in (
"currency",
"letter_head",
"tc_name",
"company",
"select_print_heading",
"write_off_account",
"taxes_and_charges",
"write_off_cost_center",
"apply_discount_on",
"cost_center",
):
if (not for_validate) or (for_validate and not doc.get(fieldname)):
doc.set(fieldname, pos.get(fieldname))
for item in doc.get("items"):
if item.get("item_code"):
profile_details = get_pos_profile_item_details_(
ItemDetailsCtx(item.as_dict()), pos, pos, update_data=True
)
for fname, val in profile_details.items():
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
def _set_selling_price_list(self, pos) -> None:
doc = self.doc
if doc.customer:
customer_price_list, customer_group = frappe.get_value(
"Customer", doc.customer, ["default_price_list", "customer_group"]
)
customer_group_price_list = frappe.get_value(
"Customer Group", customer_group, "default_price_list"
)
selling_price_list = (
customer_price_list or customer_group_price_list or pos.get("selling_price_list")
)
else:
selling_price_list = pos.get("selling_price_list")
if doc.tc_name and not doc.terms:
doc.terms = frappe.db.get_value("Terms and Conditions", doc.tc_name, "terms")
if selling_price_list:
doc.set("selling_price_list", selling_price_list)
if doc.taxes_and_charges and not len(doc.get("taxes")):
from erpnext.accounts.services.taxes import TaxService
def _set_update_stock_from_profile(self, pos) -> None:
doc = self.doc
dn_flag = any(d.get("dn_detail") for d in doc.get("items"))
doc.update_stock = 0 if dn_flag else cint(pos.get("update_stock"))
TaxService(doc).set_taxes()
def _apply_pos_item_defaults(self, pos, for_validate: bool) -> None:
from erpnext.stock.get_item_details import ItemDetailsCtx, get_pos_profile_item_details_
for item in self.doc.get("items"):
if not item.get("item_code"):
continue
profile_details = get_pos_profile_item_details_(
ItemDetailsCtx(item.as_dict()), pos, pos, update_data=True
)
for fname, val in profile_details.items():
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
def _set_terms_and_taxes(self, pos) -> None:
doc = self.doc
if doc.tc_name and not doc.terms:
doc.terms = frappe.db.get_value("Terms and Conditions", doc.tc_name, "terms")
if doc.taxes_and_charges and not len(doc.get("taxes")):
from erpnext.accounts.services.taxes import TaxService
TaxService(doc).set_taxes()
return pos
def update_paid_amount(self) -> None:
doc = self.doc
@@ -178,7 +144,6 @@ class POSService:
doc.paid_amount = 0
def validate_pos_return(self) -> None:
"""Ensure POS return payments are not less than the (negative) invoice total."""
doc = self.doc
if doc.is_consolidated:
return
@@ -195,7 +160,6 @@ class POSService:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def validate_pos(self) -> None:
"""On a POS return, paid amount plus write-off cannot exceed the grand total."""
doc = self.doc
if doc.is_return:
invoice_total = doc.rounded_total or doc.grand_total
@@ -216,7 +180,6 @@ class POSService:
self.validate_pos_opening_entry()
def validate_full_payment(self) -> None:
"""Block partial payment on a submitted POS invoice unless the profile allows it."""
doc = self.doc
allow_partial_payment = frappe.db.get_value("POS Profile", doc.pos_profile, "allow_partial_payment")
invoice_total = flt(doc.rounded_total) or flt(doc.grand_total)
@@ -233,7 +196,6 @@ class POSService:
)
def validate_pos_opening_entry(self) -> None:
"""Require exactly one current, open POS Opening Entry for the profile."""
doc = self.doc
opening_entries = frappe.get_all(
"POS Opening Entry",
@@ -319,6 +281,38 @@ class POSService:
if entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
def get_warehouse(self) -> str | None:
doc = self.doc
POSProfile = frappe.qb.DocType("POS Profile")
user_query = (
frappe.qb.from_(POSProfile)
.select(POSProfile.name, POSProfile.warehouse)
.where(POSProfile.company == doc.company)
.where(
(POSProfile.user == frappe.session["user"])
| ((POSProfile.user.isnull() | (POSProfile.user == "")) & (frappe.session["user"] == ""))
)
)
user_pos_profile = user_query.run()
warehouse = user_pos_profile[0][1] if user_pos_profile else None
if not warehouse:
global_query = (
frappe.qb.from_(POSProfile)
.select(POSProfile.name, POSProfile.warehouse)
.where(POSProfile.company == doc.company)
.where(POSProfile.user.isnull() | (POSProfile.user == ""))
)
global_pos_profile = global_query.run()
if global_pos_profile:
warehouse = global_pos_profile[0][1]
elif not user_pos_profile:
msgprint(_("POS Profile required to make POS Entry"), raise_exception=True)
return warehouse
def get_bank_cash_account(mode_of_payment: str, company: str) -> dict:
account = frappe.db.get_value(
@@ -375,43 +369,61 @@ def update_multi_mode_option(doc, pos_profile) -> None:
def get_all_mode_of_payments(doc) -> list:
"""All enabled modes of payment with their default accounts for the doc's company."""
query, mopa, mop = _enabled_mode_of_payment_query(doc.company)
return query.select(mopa.default_account, mopa.parent, mop.type.as_("type")).run(as_dict=1)
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
query = (
frappe.qb.from_(ModeOfPaymentAccount)
.join(ModeOfPayment)
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
.select(
ModeOfPaymentAccount.default_account, ModeOfPaymentAccount.parent, ModeOfPayment.type.as_("type")
)
.where(ModeOfPaymentAccount.company == doc.company)
.where(ModeOfPayment.enabled == 1)
)
return query.run(as_dict=1)
def get_mode_of_payments_info(mode_of_payments: list, company: str) -> dict:
"""Map each of the named modes of payment to its account info for the company."""
query, mopa, mop = _enabled_mode_of_payment_query(company)
data = (
query.select(mopa.default_account, mopa.parent.as_("mop"), mop.type.as_("type"))
.where(mop.name.isin(mode_of_payments))
# group by all selected columns so postgres accepts it (one row per mode of payment)
.groupby(mopa.default_account, mopa.parent, mop.type)
.run(as_dict=1)
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
query = (
frappe.qb.from_(ModeOfPaymentAccount)
.join(ModeOfPayment)
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
.select(
ModeOfPaymentAccount.default_account,
ModeOfPaymentAccount.parent.as_("mop"),
ModeOfPayment.type.as_("type"),
)
.where(ModeOfPaymentAccount.company == company)
.where(ModeOfPayment.enabled == 1)
.where(ModeOfPayment.name.isin(mode_of_payments))
.groupby(ModeOfPayment.name)
)
data = query.run(as_dict=1)
return {row.get("mop"): row for row in data}
def get_mode_of_payment_info(mode_of_payment: str, company: str) -> list:
"""Account info for a single mode of payment in the company."""
query, mopa, mop = _enabled_mode_of_payment_query(company)
return (
query.select(mopa.default_account, mopa.parent, mop.type.as_("type"))
.where(mop.name == mode_of_payment)
.run(as_dict=1)
)
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
def _enabled_mode_of_payment_query(company: str):
"""Base query joining enabled modes of payment to their accounts for a company."""
mopa = frappe.qb.DocType("Mode of Payment Account")
mop = frappe.qb.DocType("Mode of Payment")
query = (
frappe.qb.from_(mopa)
.join(mop)
.on(mopa.parent == mop.name)
.where(mopa.company == company)
.where(mop.enabled == 1)
frappe.qb.from_(ModeOfPayment)
.join(ModeOfPaymentAccount)
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
.select(
ModeOfPaymentAccount.default_account, ModeOfPaymentAccount.parent, ModeOfPayment.type.as_("type")
)
.where(ModeOfPaymentAccount.company == company)
.where(ModeOfPayment.enabled == 1)
.where(ModeOfPayment.name == mode_of_payment)
)
return query, mopa, mop
return query.run(as_dict=1)

View File

@@ -21,52 +21,45 @@ class StatusService:
doc.status = "Draft"
return
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
total = get_total_in_party_account_currency(doc)
if not status:
if doc.docstatus == 2:
status = "Cancelled"
elif doc.docstatus == 1:
doc.status = self._get_submitted_status()
if doc.is_internal_transfer():
doc.status = "Internal Transfer"
elif is_overdue(doc, total):
doc.status = "Overdue"
elif 0 < outstanding_amount < total:
doc.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(doc.due_date) >= getdate():
doc.status = "Unpaid"
elif doc.is_return == 0 and frappe.db.get_value(
"Sales Invoice", {"is_return": 1, "return_against": doc.name, "docstatus": 1}
):
doc.status = "Credit Note Issued"
elif doc.is_return == 1:
doc.status = "Return"
elif outstanding_amount <= 0:
doc.status = "Paid"
else:
doc.status = "Submitted"
if (
doc.status in ("Unpaid", "Partly Paid", "Overdue")
and doc.is_discounted
and get_discounting_status(doc.name) == "Disbursed"
):
doc.status += " and Discounted"
else:
doc.status = "Draft"
if update:
doc.db_set("status", doc.status, update_modified=update_modified)
def _get_submitted_status(self) -> str:
"""Status of a submitted invoice, with the invoice-discounting suffix applied."""
doc = self.doc
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
total = get_total_in_party_account_currency(doc)
status = self._get_payment_status(outstanding_amount, total)
if (
status in ("Unpaid", "Partly Paid", "Overdue")
and doc.is_discounted
and get_discounting_status(doc.name) == "Disbursed"
):
status += " and Discounted"
return status
def _get_payment_status(self, outstanding_amount: float, total: float) -> str:
doc = self.doc
if doc.is_internal_transfer():
return "Internal Transfer"
if is_overdue(doc, total):
return "Overdue"
if 0 < outstanding_amount < total:
return "Partly Paid"
if outstanding_amount > 0 and getdate(doc.due_date) >= getdate():
return "Unpaid"
if doc.is_return == 0 and frappe.db.get_value(
"Sales Invoice", {"is_return": 1, "return_against": doc.name, "docstatus": 1}
):
return "Credit Note Issued"
if doc.is_return == 1:
return "Return"
if outstanding_amount <= 0:
return "Paid"
return "Submitted"
def set_indicator(self) -> None:
doc = self.doc
if doc.outstanding_amount < 0:

View File

@@ -99,24 +99,23 @@ class TimesheetBillingService:
doc.total_billing_hours = sum(flt(ts.billing_hours) for ts in doc.timesheets)
def _update_time_sheet_detail(self, timesheet, args, sales_invoice: str | None) -> None:
for data in timesheet.time_logs:
if args.timesheet_detail == data.name and self._should_set_sales_invoice(data, sales_invoice):
data.sales_invoice = sales_invoice
def _should_set_sales_invoice(self, time_log, sales_invoice: str | None) -> bool:
"""Whether this time log's sales-invoice link should be (re)set to sales_invoice."""
doc = self.doc
if doc.project:
return True
if not time_log.sales_invoice:
return True
if not sales_invoice and time_log.sales_invoice == doc.name:
# clearing the link on cancellation of this invoice
return True
# clearing the link on a return raised against the original invoice
return bool(
doc.is_return
and doc.return_against
and not sales_invoice
and time_log.sales_invoice == doc.return_against
)
for data in timesheet.time_logs:
if (
(doc.project and args.timesheet_detail == data.name)
or (not doc.project and not data.sales_invoice and args.timesheet_detail == data.name)
or (
not sales_invoice
and data.sales_invoice == doc.name
and args.timesheet_detail == data.name
)
or (
doc.is_return
and doc.return_against
and data.sales_invoice
and data.sales_invoice == doc.return_against
and not sales_invoice
and args.timesheet_detail == data.name
)
):
data.sales_invoice = sales_invoice

View File

@@ -20,12 +20,6 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
unlink_payment_on_cancel_of_invoice,
)
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_transaction
from erpnext.accounts.doctype.sales_invoice.services.pos import (
POSService,
get_all_mode_of_payments,
get_mode_of_payment_info,
get_mode_of_payments_info,
)
from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset
@@ -1352,101 +1346,6 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, 0)
def test_set_pos_fields_populates_invoice_from_profile(self):
terms = frappe.db.exists("Terms and Conditions", "_Test POS Terms")
if not terms:
terms = (
frappe.get_doc(
{
"doctype": "Terms and Conditions",
"title": "_Test POS Terms",
"terms": "POS terms and conditions",
"selling": 1,
}
)
.insert()
.name
)
profile = make_pos_profile()
profile.customer = "_Test Customer"
profile.tax_category = "_Test Tax Category 1"
profile.account_for_change_amount = "Cash - _TC"
profile.ignore_pricing_rule = 1
profile.update_stock = 1
profile.apply_discount_on = "Grand Total"
profile.tc_name = terms
profile.taxes_and_charges = "_Test Sales Taxes and Charges Template - _TC"
profile.save()
si = create_sales_invoice(do_not_save=True)
si.is_pos = 1
si.pos_profile = profile.name
si.customer = None
si.taxes = []
POSService(si).set_pos_fields(for_validate=False)
self.assertEqual(si.customer, "_Test Customer")
self.assertEqual(si.tax_category, "_Test Tax Category 1")
self.assertEqual(si.ignore_pricing_rule, 1)
self.assertEqual(si.account_for_change_amount, "Cash - _TC")
self.assertEqual(si.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC")
self.assertEqual(si.apply_discount_on, "Grand Total")
self.assertEqual(si.update_stock, 1)
self.assertEqual(si.terms, "POS terms and conditions")
self.assertTrue(si.get("payments"))
self.assertTrue(si.get("taxes"))
def test_set_pos_fields_for_validate_preserves_existing_values(self):
profile = make_pos_profile()
profile.tax_category = "_Test Tax Category 1"
profile.save()
si = create_sales_invoice(do_not_save=True)
si.is_pos = 1
si.pos_profile = profile.name
si.apply_discount_on = "Net Total"
existing_customer = si.customer
POSService(si).set_pos_fields(for_validate=True)
# for_validate must not overwrite a field the user already set
self.assertEqual(si.apply_discount_on, "Net Total")
# for_validate skips mode-of-payment fetch and profile-driven customer/tax_category
self.assertFalse(si.get("payments"))
self.assertEqual(si.customer, existing_customer)
self.assertFalse(si.tax_category)
def test_set_pos_fields_uses_profile_price_list_without_customer(self):
profile = make_pos_profile(selling_price_list="_Test Price List")
profile.customer = None
profile.save()
si = create_sales_invoice(do_not_save=True)
si.is_pos = 1
si.pos_profile = profile.name
si.customer = None
POSService(si).set_pos_fields(for_validate=False)
self.assertEqual(si.selling_price_list, "_Test Price List")
def test_pos_service_mode_of_payment_queries(self):
make_pos_profile() # ensures a Cash mode-of-payment account for _Test Company
si = create_sales_invoice(do_not_save=True)
single = get_mode_of_payment_info("Cash", "_Test Company")
self.assertTrue(single)
self.assertEqual(single[0].parent, "Cash")
all_modes = get_all_mode_of_payments(si)
self.assertTrue(any(row.parent == "Cash" for row in all_modes))
grouped = get_mode_of_payments_info(["Cash"], "_Test Company")
self.assertIn("Cash", grouped)
self.assertEqual(grouped["Cash"].mop, "Cash")
def test_auto_write_off_amount(self):
make_pos_profile(
company="_Test Company with perpetual inventory",
@@ -1577,75 +1476,6 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
def test_stock_delivered_but_not_billed_gl_on_invoice(self):
company = "_Test Company with perpetual inventory"
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
make_purchase_receipt(
company=company,
item_code="_Test FG Item",
warehouse="Stores - TCP1",
cost_center="Main - TCP1",
qty=5,
rate=100,
)
dn = create_delivery_note(
company=company,
item_code="_Test FG Item",
warehouse="Stores - TCP1",
cost_center="Main - TCP1",
qty=2,
rate=300,
)
# A perpetual-inventory Delivery Note books the cost to the SDBNB account
self.assertEqual(dn.items[0].expense_account, "Stock Delivered But Not Billed - TCP1")
si = make_sales_invoice(dn.name)
si.insert()
si.submit()
gl_entries = frappe.get_all(
"GL Entry",
filters={"voucher_no": si.name, "is_cancelled": 0},
fields=["account", "debit", "credit"],
)
sdbnb_credit = sum(
row.credit for row in gl_entries if row.account == "Stock Delivered But Not Billed - TCP1"
)
cogs_debit = sum(row.debit for row in gl_entries if row.account == "Cost of Goods Sold - TCP1")
# Billing reverses SDBNB and recognises the cost in COGS for an equal amount
self.assertTrue(sdbnb_credit > 0)
self.assertEqual(sdbnb_credit, cogs_debit)
def test_get_gle_for_change_amount(self):
from erpnext.accounts.doctype.sales_invoice.services.gl_composer import SalesInvoiceGLComposer
si = create_sales_invoice(do_not_save=True)
si.is_pos = 1
si.party_account_currency = "INR"
# no change amount -> no entries
si.change_amount = 0
self.assertEqual(SalesInvoiceGLComposer(si).get_gle_for_change_amount(), [])
# change amount without an account -> mandatory error
si.change_amount = 10
si.base_change_amount = 10
si.account_for_change_amount = None
self.assertRaises(frappe.ValidationError, SalesInvoiceGLComposer(si).get_gle_for_change_amount)
# change amount with an account -> debit-to debited, change account credited
si.account_for_change_amount = "Cash - _TC"
entries = SalesInvoiceGLComposer(si).get_gle_for_change_amount()
self.assertEqual(len(entries), 2)
debit_entry = next(entry for entry in entries if entry["account"] == si.debit_to)
credit_entry = next(entry for entry in entries if entry["account"] == "Cash - _TC")
self.assertEqual(debit_entry["party"], si.customer)
self.assertEqual(flt(debit_entry["debit"]), 10.0)
self.assertEqual(flt(credit_entry["credit"]), 10.0)
def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
if validate_without_change_gle:
cash_amount -= pos.change_amount
@@ -3880,27 +3710,6 @@ class TestSalesInvoice(ERPNextTestSuite):
party_link.delete()
def test_status_indicator(self):
from erpnext.accounts.doctype.sales_invoice.services.status import StatusService
si = create_sales_invoice(do_not_save=True)
cases = [
# outstanding, due_date, is_return -> indicator color, title
(-50, nowdate(), 0, "gray", "Credit Note Issued"),
(100, add_days(nowdate(), 5), 0, "orange", "Unpaid"),
(100, add_days(nowdate(), -5), 0, "red", "Overdue"),
(0, nowdate(), 1, "gray", "Return"),
(0, nowdate(), 0, "green", "Paid"),
]
for outstanding, due_date, is_return, color, title in cases:
with self.subTest(title=title):
si.outstanding_amount = outstanding
si.due_date = due_date
si.is_return = is_return
StatusService(si).set_indicator()
self.assertEqual(si.indicator_color, color)
self.assertEqual(si.indicator_title, title)
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry

View File

@@ -56,14 +56,11 @@ def valdiate_taxes_and_charges_template(doc):
# doc.is_default = 1
if doc.is_default == 1:
template = frappe.qb.DocType(doc.doctype)
(
frappe.qb.update(template)
.set(template.is_default, 0)
.where(
(template.is_default == 1) & (template.name != doc.name) & (template.company == doc.company)
)
).run()
frappe.db.sql(
f"""update `tab{doc.doctype}` set is_default = 0
where is_default = 1 and name != %s and company = %s""",
(doc.name, doc.company),
)
validate_disabled(doc)

View File

@@ -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

View File

@@ -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)
)

View File

@@ -672,7 +672,7 @@ def make_reverse_gl_entries(
)
if not immutable_ledger_enabled:
query = query.set(gle.is_cancelled, 1) # smallint column; postgres rejects boolean true
query = query.set(gle.is_cancelled, True)
query.run()
else:
@@ -683,14 +683,12 @@ def make_reverse_gl_entries(
if not all(gle_names):
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
else:
gle = frappe.qb.DocType("GL Entry")
(
frappe.qb.update(gle)
.set(gle.is_cancelled, 1)
.set(gle.modified, now())
.set(gle.modified_by, frappe.session.user)
.where(gle.name.isin(gle_names) & (gle.is_cancelled == 0))
).run()
frappe.db.sql(
"""UPDATE `tabGL Entry` SET is_cancelled = 1,
modified=%s, modified_by=%s
where name in %s and is_cancelled = 0""",
(now(), frappe.session.user, tuple(gle_names)),
)
for entry in gl_entries:
new_gle = copy.deepcopy(entry)
@@ -727,11 +725,9 @@ def set_as_cancel(voucher_type, voucher_no):
"""
Set is_cancelled=1 in all original gl entries for the voucher
"""
gle = frappe.qb.DocType("GL Entry")
(
frappe.qb.update(gle)
.set(gle.is_cancelled, 1)
.set(gle.modified, now())
.set(gle.modified_by, frappe.session.user)
.where((gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no) & (gle.is_cancelled == 0))
).run()
frappe.db.sql(
"""UPDATE `tabGL Entry` SET is_cancelled = 1,
modified=%s, modified_by=%s
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
(now(), frappe.session.user, voucher_type, voucher_no),
)

View File

@@ -543,19 +543,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
@@ -900,13 +892,16 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
d.company, {"grand_total": d.grand_total, "base_grand_total": d.base_grand_total}
)
gle = frappe.qb.DocType("GL Entry")
company_wise_total_unpaid = frappe._dict(
frappe.qb.from_(gle)
.select(gle.company, Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency))
.where((gle.party_type == party_type) & (gle.party == party) & (gle.is_cancelled == 0))
.groupby(gle.company)
.run()
frappe.db.sql(
"""
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where party_type = %s and party=%s
and is_cancelled = 0
group by company""",
(party_type, party),
)
)
for d in companies:

View File

@@ -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)

View File

@@ -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)
@@ -708,33 +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"),
)
# One row per (future-payment JE, invoice, party): group by the JE name (primary key, so the
# JE-level posting_date/cheque_no are deterministic) plus the per-reference dimensions, summing
# amounts across JE Account rows that hit the same invoice. Without this GROUP BY the implicit
# single-group aggregate collapsed every future JE payment into one row keyed by an arbitrary
# invoice, mis-allocating the whole sum.
query = query.groupby(
je.name, jea.reference_name, jea.party, jea.party_type, je.posting_date, je.cheque_no
)
# 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):
@@ -890,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()

View File

@@ -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")
@@ -699,61 +693,6 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
)
def test_future_payments_from_journal_entry(self):
# A single future-dated Journal Entry paying two different invoices must surface as one
# future-payment row PER invoice, not collapse the whole sum onto one arbitrary invoice
# (regression: the implicit single-group aggregate filed all future JE payments under one key).
si_a = self.create_sales_invoice(no_payment_schedule=True)
si_b = self.create_sales_invoice(no_payment_schedule=True)
je = frappe.get_doc(
{
"doctype": "Journal Entry",
"voucher_type": "Journal Entry",
"company": self.company,
"posting_date": add_days(today(), 1),
"accounts": [
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"reference_type": "Sales Invoice",
"reference_name": si_a.name,
"credit_in_account_currency": 50,
"credit": 50,
},
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"reference_type": "Sales Invoice",
"reference_name": si_b.name,
"credit_in_account_currency": 50,
"credit": 50,
},
{"account": self.cash, "debit_in_account_currency": 100, "debit": 100},
],
}
)
je.insert().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"show_future_payments": True,
}
report = execute(filters)[1]
rows_a = [row for row in report if row.voucher_no == si_a.name]
rows_b = [row for row in report if row.voucher_no == si_b.name]
# exactly one report row per invoice, each keeping its own future payment; the bug collapsed
# both into a single row and allocated the whole 100 to one arbitrary invoice
self.assertEqual(len(rows_a), 1)
self.assertEqual(len(rows_b), 1)
self.assertEqual(rows_a[0].future_amount, 50.0)
self.assertEqual(rows_b[0].future_amount, 50.0)
def test_sales_person(self):
sales_person = frappe.get_doc(
{"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True}

View File

@@ -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):
"""

View File

@@ -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])

View File

@@ -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)

View File

@@ -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
@@ -84,13 +84,7 @@ def build_budget_map(budget_records, filters):
budget_distributions = get_budget_distributions(budget)
for row in budget_distributions:
if not row.start_date or not row.end_date:
continue
months = get_months_in_range(row.start_date, row.end_date)
if not months:
continue
monthly_budget = flt(row.amount) / len(months)
for month_date in months:
@@ -119,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")
@@ -131,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(
@@ -142,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)
)
@@ -165,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,
)
@@ -355,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."""

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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(

View File

@@ -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")

View File

@@ -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(

View File

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

View File

@@ -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):

View File

@@ -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 = {}

View File

@@ -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],

View File

@@ -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()

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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 = [
@@ -134,17 +135,17 @@ class TestGeneralLedger(ERPNextTestSuite):
revaluation_jv.submit()
# check the balance of the account
balance = frappe.get_all(
"GL Entry",
filters={"account": account.name},
fields=[
{"SUM": "debit_in_account_currency", "as": "debit"},
{"SUM": "credit_in_account_currency", "as": "credit"},
],
group_by="account",
balance = frappe.db.sql(
"""
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where account = %s
group by account
""",
account.name,
)
self.assertEqual(flt(balance[0].debit) - flt(balance[0].credit), 100)
self.assertEqual(balance[0][0], 100)
# check if general ledger shows correct balance
columns, data = execute(

View File

@@ -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"""
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

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