mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-19 20:54:01 +00:00
Compare commits
1 Commits
l10n_devel
...
fix-je-par
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3f262b415 |
@@ -48,7 +48,7 @@
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vite": "^8.0.16"
|
||||
"vite": "^8.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
@@ -250,7 +250,7 @@ const ClosingBalanceForm = ({ defaultBalance, date, bankAccount, onClose }: { de
|
||||
{_("Enter the closing balance you see in your bank statement for {0} as of the {1}", [bankAccount?.account_name ?? bankAccount?.name ?? '', formatDate(date, 'Do MMM YYYY')])}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{error && <div className="py-2"><ErrorBanner error={error} /></div>}
|
||||
{error && <ErrorBanner error={error} />}
|
||||
<div className="py-4">
|
||||
<CurrencyFormField
|
||||
name="balance"
|
||||
|
||||
@@ -33,16 +33,6 @@ export const getErrorMessages = (error?: FrappeError | null): ParsedErrorMessage
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-expect-error - some errors have _error_message
|
||||
if (error?._error_message) {
|
||||
eMessages.push({
|
||||
// @ts-expect-error - some errors have _error_message
|
||||
message: error?._error_message,
|
||||
title: "Error",
|
||||
indicator: "red"
|
||||
})
|
||||
}
|
||||
|
||||
if (eMessages.length === 0) {
|
||||
// Get the message from the exception by removing the exc_type
|
||||
const indexOfFirstColon = error?.exception?.indexOf(':')
|
||||
|
||||
@@ -358,10 +358,10 @@
|
||||
dependencies:
|
||||
"@tybys/wasm-util" "^0.10.1"
|
||||
|
||||
"@oxc-project/types@=0.133.0":
|
||||
version "0.133.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8"
|
||||
integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==
|
||||
"@oxc-project/types@=0.128.0":
|
||||
version "0.128.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.128.0.tgz#efc7524f948ff9e8ab1404ecad1823849c6fe149"
|
||||
integrity sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==
|
||||
|
||||
"@radix-ui/number@1.1.1":
|
||||
version "1.1.1"
|
||||
@@ -1042,95 +1042,95 @@
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb"
|
||||
integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==
|
||||
|
||||
"@rolldown/binding-android-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592"
|
||||
integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==
|
||||
"@rolldown/binding-android-arm64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz#3af8b2242086125934a85c1915b76e0a6a2054c1"
|
||||
integrity sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==
|
||||
|
||||
"@rolldown/binding-darwin-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc"
|
||||
integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==
|
||||
"@rolldown/binding-darwin-arm64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz#ae0b4467d24ecd6c6589f03d4d4699616ee9649c"
|
||||
integrity sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==
|
||||
|
||||
"@rolldown/binding-darwin-x64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad"
|
||||
integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==
|
||||
"@rolldown/binding-darwin-x64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz#23cf24b0a7b96c8990bbdd8a91e7fd3ba82b00e7"
|
||||
integrity sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==
|
||||
|
||||
"@rolldown/binding-freebsd-x64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1"
|
||||
integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==
|
||||
"@rolldown/binding-freebsd-x64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz#a047a770f94dc451c062b729e5d1cf82e5c6f9c4"
|
||||
integrity sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f"
|
||||
integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==
|
||||
"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz#c0b7f346cbf50301cea669a4632bc63aabe6a72c"
|
||||
integrity sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1"
|
||||
integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==
|
||||
"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz#af56373c7996ebe6379207cd699c9f7f705e235d"
|
||||
integrity sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069"
|
||||
integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==
|
||||
"@rolldown/binding-linux-arm64-musl@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz#a8f5acd21fcffc8991aa84710e3ae603c4240ea4"
|
||||
integrity sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8"
|
||||
integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==
|
||||
"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz#1d4a89e040ff82141fc46e717cfab80b05f7c13f"
|
||||
integrity sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763"
|
||||
integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==
|
||||
"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz#97c21feeb2ed87d07820f0b2dcc5dd663e7a7f3b"
|
||||
integrity sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7"
|
||||
integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==
|
||||
"@rolldown/binding-linux-x64-gnu@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz#06310d40fe139ccc3c433b361120d337c66ebec2"
|
||||
integrity sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==
|
||||
|
||||
"@rolldown/binding-linux-x64-musl@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3"
|
||||
integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==
|
||||
"@rolldown/binding-linux-x64-musl@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz#6a711258841f42609b238050cfcd5db13ac136d0"
|
||||
integrity sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==
|
||||
|
||||
"@rolldown/binding-openharmony-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309"
|
||||
integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==
|
||||
"@rolldown/binding-openharmony-arm64@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz#15cb644beeafdbec930d79ed45c2a7c2573eac70"
|
||||
integrity sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==
|
||||
|
||||
"@rolldown/binding-wasm32-wasi@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b"
|
||||
integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==
|
||||
"@rolldown/binding-wasm32-wasi@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz#ca3a56d11dfd533d743711141b3bb4c1ec10110e"
|
||||
integrity sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==
|
||||
dependencies:
|
||||
"@emnapi/core" "1.10.0"
|
||||
"@emnapi/runtime" "1.10.0"
|
||||
"@napi-rs/wasm-runtime" "^1.1.4"
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad"
|
||||
integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==
|
||||
"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz#8c2117d68331d7de59d24631146d538fc203d27c"
|
||||
integrity sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb"
|
||||
integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==
|
||||
"@rolldown/binding-win32-x64-msvc@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz#bb5c28df3095046778cc1b020ef52fc5ee7b7e70"
|
||||
integrity sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==
|
||||
|
||||
"@rolldown/pluginutils@1.0.0-rc.18":
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz#51cf2589596a179ebe8cbf313f1358c7b51a2fdc"
|
||||
integrity sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==
|
||||
|
||||
"@rolldown/pluginutils@1.0.0-rc.7":
|
||||
version "1.0.0-rc.7"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz#0414869467f0e471a6515d4f506c85fde867e022"
|
||||
integrity sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==
|
||||
|
||||
"@rolldown/pluginutils@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be"
|
||||
integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==
|
||||
|
||||
"@socket.io/component-emitter@~3.1.0":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
|
||||
@@ -3031,10 +3031,10 @@ ms@^2.1.3:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
nanoid@^3.3.12:
|
||||
version "3.3.12"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05"
|
||||
integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==
|
||||
nanoid@^3.3.11:
|
||||
version "3.3.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -3119,17 +3119,22 @@ picocolors@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
|
||||
picomatch@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
|
||||
postcss@^8.5.15:
|
||||
version "8.5.15"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c"
|
||||
integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==
|
||||
postcss@^8.5.14:
|
||||
version "8.5.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
|
||||
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
|
||||
dependencies:
|
||||
nanoid "^3.3.12"
|
||||
nanoid "^3.3.11"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
@@ -3389,29 +3394,29 @@ resolve-from@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
rolldown@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac"
|
||||
integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==
|
||||
rolldown@1.0.0-rc.18:
|
||||
version "1.0.0-rc.18"
|
||||
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.18.tgz#c597f89a4ce12e6fc918fa91e4f892b340aa92f0"
|
||||
integrity sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==
|
||||
dependencies:
|
||||
"@oxc-project/types" "=0.133.0"
|
||||
"@rolldown/pluginutils" "^1.0.0"
|
||||
"@oxc-project/types" "=0.128.0"
|
||||
"@rolldown/pluginutils" "1.0.0-rc.18"
|
||||
optionalDependencies:
|
||||
"@rolldown/binding-android-arm64" "1.0.3"
|
||||
"@rolldown/binding-darwin-arm64" "1.0.3"
|
||||
"@rolldown/binding-darwin-x64" "1.0.3"
|
||||
"@rolldown/binding-freebsd-x64" "1.0.3"
|
||||
"@rolldown/binding-linux-arm-gnueabihf" "1.0.3"
|
||||
"@rolldown/binding-linux-arm64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-arm64-musl" "1.0.3"
|
||||
"@rolldown/binding-linux-ppc64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-s390x-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-x64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-x64-musl" "1.0.3"
|
||||
"@rolldown/binding-openharmony-arm64" "1.0.3"
|
||||
"@rolldown/binding-wasm32-wasi" "1.0.3"
|
||||
"@rolldown/binding-win32-arm64-msvc" "1.0.3"
|
||||
"@rolldown/binding-win32-x64-msvc" "1.0.3"
|
||||
"@rolldown/binding-android-arm64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-darwin-arm64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-darwin-x64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-freebsd-x64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-arm64-musl" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-x64-gnu" "1.0.0-rc.18"
|
||||
"@rolldown/binding-linux-x64-musl" "1.0.0-rc.18"
|
||||
"@rolldown/binding-openharmony-arm64" "1.0.0-rc.18"
|
||||
"@rolldown/binding-wasm32-wasi" "1.0.0-rc.18"
|
||||
"@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.18"
|
||||
"@rolldown/binding-win32-x64-msvc" "1.0.0-rc.18"
|
||||
|
||||
scheduler@^0.27.0:
|
||||
version "0.27.0"
|
||||
@@ -3535,10 +3540,18 @@ tapable@^2.3.3:
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160"
|
||||
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
|
||||
|
||||
tinyglobby@^0.2.15, tinyglobby@^0.2.17:
|
||||
version "0.2.17"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
|
||||
integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==
|
||||
tinyglobby@^0.2.15:
|
||||
version "0.2.15"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
|
||||
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.3"
|
||||
|
||||
tinyglobby@^0.2.16:
|
||||
version "0.2.16"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"
|
||||
integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.4"
|
||||
@@ -3712,16 +3725,16 @@ vfile@^6.0.0:
|
||||
"@types/unist" "^3.0.0"
|
||||
vfile-message "^4.0.0"
|
||||
|
||||
vite@^8.0.16:
|
||||
version "8.0.16"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6"
|
||||
integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==
|
||||
vite@^8.0.11:
|
||||
version "8.0.11"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.11.tgz#d128fe82a0dd24da5127d20560735f1cd7ade0a6"
|
||||
integrity sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==
|
||||
dependencies:
|
||||
lightningcss "^1.32.0"
|
||||
picomatch "^4.0.4"
|
||||
postcss "^8.5.15"
|
||||
rolldown "1.0.3"
|
||||
tinyglobby "^0.2.17"
|
||||
postcss "^8.5.14"
|
||||
rolldown "1.0.0-rc.18"
|
||||
tinyglobby "^0.2.16"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,13 +22,11 @@ class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.cash = "Cash - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.creditors_usd = "_Test Payable USD - _TC"
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_usd_payable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-04-11 19:48:13.622253",
|
||||
"doctype": "DocType",
|
||||
@@ -8,8 +7,7 @@
|
||||
"field_order": [
|
||||
"bank_account",
|
||||
"date",
|
||||
"balance",
|
||||
"company"
|
||||
"balance"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -33,20 +31,12 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Balance",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "bank_account.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-16 22:17:48.007982",
|
||||
"modified": "2026-04-11 19:49:45.374695",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account Balance",
|
||||
|
||||
@@ -16,7 +16,6 @@ class BankAccountBalance(Document):
|
||||
|
||||
balance: DF.Currency
|
||||
bank_account: DF.Link
|
||||
company: DF.Link | None
|
||||
date: DF.Date
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -17,10 +17,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -26,9 +26,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankStatementImportLog(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -5,8 +5,6 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Abs, Max, Sum
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -104,36 +104,6 @@ class TestBankTransaction(ERPNextTestSuite):
|
||||
self.assertEqual(bank_transaction.unallocated_amount, 1700)
|
||||
self.assertEqual(bank_transaction.payment_entries, [])
|
||||
|
||||
# Amending a reconciled payment entry must not carry over its clearance date
|
||||
def test_clearance_date_cleared_on_amend(self):
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
|
||||
)
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment.name,
|
||||
"amount": bank_transaction.unallocated_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
|
||||
self.assertTrue(frappe.db.get_value("Payment Entry", payment.name, "clearance_date"))
|
||||
|
||||
payment.reload()
|
||||
payment.cancel()
|
||||
|
||||
amended = frappe.copy_doc(payment)
|
||||
amended.amended_from = payment.name
|
||||
amended.docstatus = 0
|
||||
amended.insert()
|
||||
|
||||
self.assertFalse(amended.clearance_date)
|
||||
|
||||
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
|
||||
def test_debit_credit_output(self):
|
||||
bank_transaction = frappe.get_doc(
|
||||
|
||||
@@ -11,11 +11,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankTransactionRule(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Criterion, Order
|
||||
from frappe.query_builder.functions import Max, NullIf, Sum
|
||||
from frappe.query_builder.functions import NullIf, Sum
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
@@ -188,17 +188,11 @@ class ExchangeRateRevaluation(Document):
|
||||
accounts = [x[0] for x in res]
|
||||
|
||||
if accounts:
|
||||
gle = qb.DocType("GL Entry")
|
||||
having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
|
||||
(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
|
||||
)
|
||||
|
||||
# balance expressions reused in both SELECT and HAVING; postgres can't reference a
|
||||
# SELECT alias inside HAVING, so the aggregate expression must be repeated there.
|
||||
balance = Sum(gle.debit) - Sum(gle.credit)
|
||||
balance_in_account_currency = Sum(gle.debit_in_account_currency) - Sum(
|
||||
gle.credit_in_account_currency
|
||||
)
|
||||
having_clause = (balance != balance_in_account_currency) & (
|
||||
(balance_in_account_currency != 0) | (balance != 0)
|
||||
)
|
||||
gle = qb.DocType("GL Entry")
|
||||
|
||||
# conditions
|
||||
conditions = []
|
||||
@@ -215,15 +209,17 @@ class ExchangeRateRevaluation(Document):
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.account,
|
||||
# grouped by NullIf(party_type/party, ""); the bare columns + account_currency are
|
||||
# constant per group -> Max() keeps the GROUP BY valid on postgres with the same value.
|
||||
Max(gle.party_type).as_("party_type"),
|
||||
Max(gle.party).as_("party"),
|
||||
Max(gle.account_currency).as_("account_currency"),
|
||||
balance_in_account_currency.as_("balance_in_account_currency"),
|
||||
balance.as_("balance"),
|
||||
# zero_balance is recomputed in Python below (after rounding), so the SQL value is
|
||||
# unused -- dropped (it used MySQL's XOR operator, which postgres lacks).
|
||||
gle.party_type,
|
||||
gle.party,
|
||||
gle.account_currency,
|
||||
(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_(
|
||||
"balance_in_account_currency"
|
||||
),
|
||||
(Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
|
||||
(Sum(gle.debit) - Sum(gle.credit) == 0)
|
||||
^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_(
|
||||
"zero_balance"
|
||||
),
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, ""))
|
||||
|
||||
@@ -15,11 +15,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.item = "_Test Item"
|
||||
self.customer = "_Test Customer"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.set_system_and_company_settings()
|
||||
|
||||
def set_system_and_company_settings(self):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -52,11 +52,12 @@ class ModeofPayment(Document):
|
||||
|
||||
def validate_pos_mode_of_payment(self):
|
||||
if not self.enabled:
|
||||
pos_profiles = frappe.get_all(
|
||||
"Sales Invoice Payment",
|
||||
filters={"parenttype": "POS Profile", "mode_of_payment": self.name},
|
||||
pluck="parent",
|
||||
pos_profiles = frappe.db.sql(
|
||||
"""SELECT sip.parent FROM `tabSales Invoice Payment` sip
|
||||
WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""",
|
||||
(self.name),
|
||||
)
|
||||
pos_profiles = list(map(lambda x: x[0], pos_profiles))
|
||||
|
||||
if pos_profiles:
|
||||
message = _(
|
||||
|
||||
@@ -270,13 +270,6 @@ def start_import(invoices):
|
||||
errors = 0
|
||||
names = []
|
||||
for idx, d in enumerate(invoices):
|
||||
# Scope each invoice to a savepoint so a failure only undoes that invoice.
|
||||
# A plain rollback() would discard the whole transaction — including invoices
|
||||
# imported earlier in this batch and the error logs of earlier failures (the
|
||||
# latter only survive on mariadb because the Error Log table is MyISAM; on
|
||||
# postgres they would be lost). Rolling back to a savepoint keeps both.
|
||||
savepoint = f"opening_invoice_{frappe.generate_hash(length=8)}"
|
||||
frappe.db.savepoint(savepoint)
|
||||
try:
|
||||
invoice_number = None
|
||||
if d.invoice_number:
|
||||
@@ -291,7 +284,7 @@ def start_import(invoices):
|
||||
names.append(doc.name)
|
||||
except Exception:
|
||||
errors += 1
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
frappe.db.rollback()
|
||||
doc.log_error("Opening invoice creation failed")
|
||||
if errors:
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -9,8 +9,8 @@ import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Case, Tuple
|
||||
from frappe.query_builder.functions import Abs, Count, Max
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
|
||||
from pypika.functions import Coalesce, Sum
|
||||
@@ -766,19 +766,13 @@ class PaymentEntry(AccountsController):
|
||||
def validate_journal_entry(self):
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype == "Journal Entry":
|
||||
je_accounts = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"account": self.party_account,
|
||||
"party": self.party,
|
||||
"docstatus": 1,
|
||||
"parent": d.reference_name,
|
||||
},
|
||||
or_filters=[
|
||||
["reference_type", "is", "not set"],
|
||||
["reference_type", "in", ["Sales Order", "Purchase Order"]],
|
||||
],
|
||||
fields=["debit", "credit"],
|
||||
je_accounts = frappe.db.sql(
|
||||
"""select debit, credit from `tabJournal Entry Account`
|
||||
where account = %s and party=%s and docstatus = 1 and parent = %s
|
||||
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
|
||||
""",
|
||||
(self.party_account, self.party, d.reference_name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not je_accounts:
|
||||
@@ -863,17 +857,27 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision)
|
||||
|
||||
ps = frappe.qb.DocType("Payment Schedule")
|
||||
if cancel:
|
||||
(
|
||||
frappe.qb.update(ps)
|
||||
.set(ps.paid_amount, ps.paid_amount - (allocated_amount - discounted_amt))
|
||||
.set(ps.base_paid_amount, ps.base_paid_amount - base_paid_amount)
|
||||
.set(ps.discounted_amount, ps.discounted_amount - discounted_amt)
|
||||
.set(ps.outstanding, ps.outstanding + allocated_amount)
|
||||
.set(ps.base_outstanding, ps.base_outstanding - base_outstanding)
|
||||
.where((ps.parent == key[1]) & (ps.payment_term == key[0]))
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabPayment Schedule`
|
||||
SET
|
||||
paid_amount = `paid_amount` - %s,
|
||||
base_paid_amount = `base_paid_amount` - %s,
|
||||
discounted_amount = `discounted_amount` - %s,
|
||||
outstanding = `outstanding` + %s,
|
||||
base_outstanding = `base_outstanding` - %s
|
||||
WHERE parent = %s and payment_term = %s""",
|
||||
(
|
||||
allocated_amount - discounted_amt,
|
||||
base_paid_amount,
|
||||
discounted_amt,
|
||||
allocated_amount,
|
||||
base_outstanding,
|
||||
key[1],
|
||||
key[0],
|
||||
),
|
||||
)
|
||||
else:
|
||||
if allocated_amount > outstanding:
|
||||
frappe.throw(
|
||||
@@ -883,15 +887,26 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
if allocated_amount and outstanding:
|
||||
(
|
||||
frappe.qb.update(ps)
|
||||
.set(ps.paid_amount, ps.paid_amount + (allocated_amount - discounted_amt))
|
||||
.set(ps.base_paid_amount, ps.base_paid_amount + base_paid_amount)
|
||||
.set(ps.discounted_amount, ps.discounted_amount + discounted_amt)
|
||||
.set(ps.outstanding, ps.outstanding - allocated_amount)
|
||||
.set(ps.base_outstanding, ps.base_outstanding - base_outstanding)
|
||||
.where((ps.parent == key[1]) & (ps.payment_term == key[0]))
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabPayment Schedule`
|
||||
SET
|
||||
paid_amount = `paid_amount` + %s,
|
||||
base_paid_amount = `base_paid_amount` + %s,
|
||||
discounted_amount = `discounted_amount` + %s,
|
||||
outstanding = `outstanding` - %s,
|
||||
base_outstanding = `base_outstanding` - %s
|
||||
WHERE parent = %s and payment_term = %s""",
|
||||
(
|
||||
allocated_amount - discounted_amt,
|
||||
base_paid_amount,
|
||||
discounted_amt,
|
||||
allocated_amount,
|
||||
base_outstanding,
|
||||
key[1],
|
||||
key[0],
|
||||
),
|
||||
)
|
||||
|
||||
def get_allocated_amount_in_transaction_currency(
|
||||
self, allocated_amount, reference_doctype, reference_docname
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -10,22 +10,76 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
self.company = "_Test Company"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.creditors = "Creditors - _TC"
|
||||
self.bank = "Cash - _TC"
|
||||
self.item = "_Test Item"
|
||||
self.customer = "_Test Customer"
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Payment Ledger"
|
||||
company = None
|
||||
if frappe.db.exists("Company", company_name):
|
||||
company = frappe.get_doc("Company", company_name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company_name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "All Warehouses - _PL"
|
||||
self.income_account = "Sales - _PL"
|
||||
self.expense_account = "Cost of Goods Sold - _PL"
|
||||
self.debit_to = "Debtors - _PL"
|
||||
self.creditors = "Creditors - _PL"
|
||||
|
||||
# create bank account
|
||||
if frappe.db.exists("Account", "HDFC - _PL"):
|
||||
self.bank = "HDFC - _PL"
|
||||
else:
|
||||
bank_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - _PL",
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
bank_acc.save()
|
||||
self.bank = bank_acc.name
|
||||
|
||||
def create_item(self):
|
||||
item_name = "_Test PL Item"
|
||||
item = create_item(
|
||||
item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_customer(self):
|
||||
name = "_Test PL Customer"
|
||||
if frappe.db.exists("Customer", name):
|
||||
self.customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
@@ -98,6 +152,18 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
)
|
||||
return so
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
|
||||
@@ -60,32 +60,23 @@ class PaymentOrder(Document):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_mop_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||
return frappe.get_all(
|
||||
"Payment Order Reference",
|
||||
filters={"parent": filters.get("parent"), "mode_of_payment": ["like", f"%{txt}%"]},
|
||||
fields=["mode_of_payment"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
as_list=True,
|
||||
return frappe.db.sql(
|
||||
""" select mode_of_payment from `tabPayment Order Reference`
|
||||
where parent = %(parent)s and mode_of_payment like %(txt)s
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||
return frappe.get_all(
|
||||
"Payment Order Reference",
|
||||
filters={
|
||||
"parent": filters.get("parent"),
|
||||
"supplier": ["like", f"%{txt}%"],
|
||||
"payment_reference": ["is", "not set"],
|
||||
},
|
||||
fields=["supplier"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
as_list=True,
|
||||
return frappe.db.sql(
|
||||
""" select supplier from `tabPayment Order Reference`
|
||||
where parent = %(parent)s and supplier like %(txt)s and
|
||||
(payment_reference is null or payment_reference='')
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -16,14 +16,12 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_supplier()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
@@ -374,6 +372,7 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
so1 = self.create_sales_order()
|
||||
so2 = self.create_sales_order()
|
||||
|
||||
@@ -424,11 +423,7 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
def test_07_adv_from_so_to_invoice(self):
|
||||
frappe.db.set_value("Company", self.company, "book_advance_payments_in_separate_party_account", True)
|
||||
frappe.db.set_value(
|
||||
"Company", self.company, "default_advance_received_account", "Advance Received - _TC"
|
||||
)
|
||||
|
||||
self.enable_advance_as_liability()
|
||||
so = self.create_sales_order()
|
||||
pe = self.create_payment_entry()
|
||||
pe.paid_amount = 1000
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs, Max, Sum
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
@@ -72,7 +72,7 @@ class UnreconcilePayment(Document):
|
||||
alloc.party,
|
||||
)
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", 1)
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -120,20 +120,18 @@ def get_linked_payments_for_doc(
|
||||
res = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Max(ple.account).as_("account"),
|
||||
Max(ple.party_type).as_("party_type"),
|
||||
Max(ple.party).as_("party"),
|
||||
Max(ple.company).as_("company"),
|
||||
Max(ple.voucher_type).as_("reference_doctype"),
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.company,
|
||||
ple.voucher_type.as_("reference_doctype"),
|
||||
ple.voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
Max(ple.account_currency).as_("account_currency"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.voucher_no, ple.against_voucher_no)
|
||||
.having(Abs(Sum(ple.amount_in_account_currency)) > 0)
|
||||
# deterministic order across backends (postgres GROUP BY does not imply ordering)
|
||||
.orderby(ple.voucher_no)
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return res
|
||||
@@ -148,19 +146,17 @@ def get_linked_payments_for_doc(
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Max(ple.company).as_("company"),
|
||||
Max(ple.account).as_("account"),
|
||||
Max(ple.party_type).as_("party_type"),
|
||||
Max(ple.party).as_("party"),
|
||||
Max(ple.against_voucher_type).as_("reference_doctype"),
|
||||
ple.company,
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
ple.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
Max(ple.account_currency).as_("account_currency"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.against_voucher_no)
|
||||
# deterministic order across backends (postgres GROUP BY does not imply ordering)
|
||||
.orderby(ple.against_voucher_no)
|
||||
)
|
||||
|
||||
res = query.run(as_dict=True)
|
||||
@@ -184,18 +180,15 @@ def get_linked_advances(company, docname):
|
||||
return (
|
||||
qb.from_(adv)
|
||||
.select(
|
||||
# non-grouped columns are constant per against_voucher_no -> Max() is unchanged and postgres-valid
|
||||
Max(adv.company).as_("company"),
|
||||
Max(adv.against_voucher_type).as_("reference_doctype"),
|
||||
adv.company,
|
||||
adv.against_voucher_type.as_("reference_doctype"),
|
||||
adv.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(adv.amount)).as_("allocated_amount"),
|
||||
Max(adv.currency).as_("currency"),
|
||||
adv.currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.having(Abs(Sum(adv.amount)) > 0)
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.groupby(adv.against_voucher_no)
|
||||
# deterministic order across backends (postgres GROUP BY does not imply ordering)
|
||||
.orderby(adv.against_voucher_no)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -11,11 +11,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def test_01_receivable_summary_output(self):
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,10 @@ def execute(filters=None):
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
depreciation_accounts = frappe.get_all("Account", filters={"account_type": "Depreciation"}, pluck="name")
|
||||
depreciation_accounts = frappe.db.sql_list(
|
||||
""" select name from tabAccount
|
||||
where ifnull(account_type, '') = 'Depreciation' """
|
||||
)
|
||||
|
||||
filters_data = [
|
||||
["company", "=", filters.get("company")],
|
||||
@@ -30,8 +33,10 @@ def get_data(filters):
|
||||
filters_data.append(["against_voucher", "=", filters.get("asset")])
|
||||
|
||||
if filters.get("asset_category"):
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"asset_category": filters.get("asset_category"), "docstatus": 1}, pluck="name"
|
||||
assets = frappe.db.sql_list(
|
||||
"""select name from tabAsset
|
||||
where asset_category = %s and docstatus=1""",
|
||||
filters.get("asset_category"),
|
||||
)
|
||||
|
||||
filters_data.append(["against_voucher", "in", assets])
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.report.asset_depreciation_ledger.asset_depreciation_ledger import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAssetDepreciationLedger(ERPNextTestSuite):
|
||||
def test_report_executes(self):
|
||||
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
|
||||
# both MariaDB and postgres.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
columns, *_rest = execute(
|
||||
frappe._dict({"company": company, "from_date": "2020-01-01", "to_date": "2030-12-31"})
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.custom import MonthName
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.utils import add_months, flt, formatdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -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."""
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.report.budget_variance_report.budget_variance_report import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestBudgetVarianceReport(ERPNextTestSuite):
|
||||
def test_report_executes(self):
|
||||
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
|
||||
# both MariaDB and postgres.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc")
|
||||
columns, *_rest = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_fiscal_year": fy,
|
||||
"to_fiscal_year": fy,
|
||||
"period": "Yearly",
|
||||
"budget_against": "Cost Center",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
@@ -7,7 +7,6 @@ from datetime import timedelta
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cstr, flt
|
||||
from pypika import Order
|
||||
|
||||
@@ -214,43 +213,37 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
|
||||
|
||||
|
||||
def get_account_type_based_gl_data(company, filters=None):
|
||||
cond = ""
|
||||
filters = frappe._dict(filters or {})
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
account = frappe.qb.DocType("Account")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(Sum(gle.credit) - Sum(gle.debit))
|
||||
.where(
|
||||
(gle.company == company)
|
||||
& (gle.posting_date >= filters.start_date)
|
||||
& (gle.posting_date <= filters.end_date)
|
||||
& (gle.voucher_type != "Period Closing Voucher")
|
||||
& gle.account.isin(
|
||||
frappe.qb.from_(account)
|
||||
.select(account.name)
|
||||
.where(account.account_type == filters.account_type)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if filters.include_default_book_entries:
|
||||
company_fb = frappe.get_cached_value("Company", company, "default_finance_book")
|
||||
query = query.where(
|
||||
gle.finance_book.isin([filters.finance_book, company_fb, ""]) | gle.finance_book.isnull()
|
||||
cond = """ AND (finance_book in ({}, {}, '') OR finance_book IS NULL)
|
||||
""".format(
|
||||
frappe.db.escape(filters.finance_book),
|
||||
frappe.db.escape(company_fb),
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
gle.finance_book.isin([cstr(filters.finance_book), ""]) | gle.finance_book.isnull()
|
||||
cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" % (
|
||||
frappe.db.escape(cstr(filters.finance_book))
|
||||
)
|
||||
|
||||
if filters.get("cost_center"):
|
||||
cost_centers = get_cost_centers_with_children(filters.cost_center)
|
||||
query = query.where(gle.cost_center.isin(cost_centers))
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
cond += " and cost_center in %(cost_center)s"
|
||||
|
||||
gl_sum = query.run()
|
||||
return gl_sum[0][0] if gl_sum and gl_sum[0][0] else 0
|
||||
gl_sum = frappe.db.sql_list(
|
||||
f"""
|
||||
select sum(credit) - sum(debit)
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
|
||||
and voucher_type != 'Period Closing Voucher'
|
||||
and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
|
||||
""",
|
||||
filters,
|
||||
)
|
||||
|
||||
return gl_sum[0] if gl_sum and gl_sum[0] else 0
|
||||
|
||||
|
||||
def get_start_date(period, accumulated_values, company):
|
||||
@@ -374,10 +367,11 @@ def get_net_income(company, period_list, filters):
|
||||
from_date, to_date = get_opening_range_using_fiscal_year(company, period_list)
|
||||
|
||||
for root_type in ["Income", "Expense"]:
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
for root in frappe.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
company,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.report.cash_flow.cash_flow import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCashFlow(ERPNextTestSuite):
|
||||
def test_report_executes(self):
|
||||
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
|
||||
# both MariaDB and postgres.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc")
|
||||
columns, *_rest = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_fiscal_year": fy,
|
||||
"to_fiscal_year": fy,
|
||||
"filter_based_on": "Fiscal Year",
|
||||
"periodicity": "Yearly",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
pe = qb.DocType("Payment Entry")
|
||||
doctype_name = ConstantColumn("Payment Entry")
|
||||
payments = (
|
||||
@@ -100,10 +101,7 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
Case()
|
||||
.when(pe.paid_from.eq(filters.account), pe.paid_amount)
|
||||
.else_(pe.received_amount)
|
||||
.as_("amount"),
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.report.cheques_and_deposits_incorrectly_cleared.cheques_and_deposits_incorrectly_cleared import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestChequesAndDepositsIncorrectlyCleared(ERPNextTestSuite):
|
||||
def test_report_executes_with_case_amount(self):
|
||||
# Exercises the Payment Entry branch whose amount column uses a db-aware CASE expression
|
||||
# (previously a MySQL-only IF()). IF() does not compile on postgres, so running the report
|
||||
# query guards the portability fix on both databases.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
account = frappe.db.get_value(
|
||||
"Account", {"account_type": "Bank", "company": company, "is_group": 0}, "name"
|
||||
)
|
||||
columns, data = execute(frappe._dict({"account": account, "report_date": nowdate()}))
|
||||
self.assertTrue(columns)
|
||||
self.assertIsInstance(data, list)
|
||||
@@ -347,10 +347,11 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
|
||||
filters.end_date = end_date
|
||||
|
||||
gl_entries_by_account = {}
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
for root in frappe.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
start_date,
|
||||
@@ -511,11 +512,9 @@ def get_companies(filters):
|
||||
def get_subsidiary_companies(company):
|
||||
lft, rgt = frappe.get_cached_value("Company", company, ["lft", "rgt"])
|
||||
|
||||
return frappe.get_all(
|
||||
"Company",
|
||||
filters={"lft": [">=", lft], "rgt": ["<=", rgt]},
|
||||
pluck="name",
|
||||
order_by="lft, rgt",
|
||||
return frappe.db.sql_list(
|
||||
f"""select name from `tabCompany`
|
||||
where lft >= {lft} and rgt <= {rgt} order by lft, rgt"""
|
||||
)
|
||||
|
||||
|
||||
@@ -605,10 +604,14 @@ def set_gl_entries_by_account(
|
||||
|
||||
company_lft, company_rgt = frappe.get_cached_value("Company", filters.get("company"), ["lft", "rgt"])
|
||||
|
||||
companies = frappe.get_all(
|
||||
"Company",
|
||||
filters={"lft": [">=", company_lft], "rgt": ["<=", company_rgt]},
|
||||
fields=["name", "default_currency"],
|
||||
companies = frappe.db.sql(
|
||||
""" select name, default_currency from `tabCompany`
|
||||
where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
|
||||
{
|
||||
"company_lft": company_lft,
|
||||
"company_rgt": company_rgt,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
currency_info = frappe._dict(
|
||||
|
||||
@@ -126,22 +126,12 @@ def get_data(filters) -> list[list]:
|
||||
|
||||
|
||||
def get_company_wise_tb_data(filters, reporting_currency, ignore_reporting_currency):
|
||||
accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[
|
||||
"name",
|
||||
"account_number",
|
||||
"parent_account",
|
||||
"account_name",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_type",
|
||||
"is_group",
|
||||
"lft",
|
||||
"rgt",
|
||||
],
|
||||
order_by="lft",
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, account_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
@@ -11,12 +11,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False, **args):
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import functions
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder import Column, functions
|
||||
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, getdate, rounded
|
||||
|
||||
from erpnext.accounts.report.financial_statements import get_period_list
|
||||
@@ -301,10 +300,8 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
Get all sales and purchase invoices which has deferred revenue/expense items
|
||||
"""
|
||||
gle = qb.DocType("GL Entry")
|
||||
# a literal marker: real GL rows are "posted" (dummy/simulated future entries use "not").
|
||||
# ConstantColumn renders a single-quoted string literal, valid on both backends -- a plain
|
||||
# Column rendered as "posted", which MySQL reads as the string but postgres as an identifier.
|
||||
posted = ConstantColumn("posted").as_("posted")
|
||||
# column doesn't have an alias option
|
||||
posted = Column("posted")
|
||||
|
||||
if self.filters.type == "Revenue":
|
||||
inv = qb.DocType("Sales Invoice")
|
||||
@@ -330,15 +327,13 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
)
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
# non-grouped columns are constant per grouped invoice / invoice item -> Max() keeps the
|
||||
# GROUP BY valid on postgres while returning the same value MySQL picked.
|
||||
functions.Max(inv.posting_date).as_("posting_date"),
|
||||
inv.posting_date,
|
||||
inv_item.name.as_("item"),
|
||||
functions.Max(inv_item.item_name).as_("item_name"),
|
||||
functions.Max(inv_item.service_start_date).as_("service_start_date"),
|
||||
functions.Max(inv_item.service_end_date).as_("service_end_date"),
|
||||
functions.Max(inv_item.base_net_amount).as_("base_net_amount"),
|
||||
functions.Max(deferred_account_field).as_(deferred_account_field.name),
|
||||
inv_item.item_name,
|
||||
inv_item.service_start_date,
|
||||
inv_item.service_end_date,
|
||||
inv_item.base_net_amount,
|
||||
deferred_account_field,
|
||||
gle.posting_date.as_("gle_posting_date"),
|
||||
functions.Sum(gle.debit).as_("debit"),
|
||||
functions.Sum(gle.credit).as_("credit"),
|
||||
|
||||
@@ -61,16 +61,11 @@ class TestDeferredRevenueAndExpense(ERPNextTestSuite, AccountsTestMixin):
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.company_abbr = "_TC"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.create_company()
|
||||
self.create_customer("_Test Customer")
|
||||
self.create_supplier("_Test Furniture Supplier")
|
||||
self.setup_deferred_accounts_and_items()
|
||||
self.clear_old_entries()
|
||||
|
||||
@ERPNextTestSuite.change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||
def test_deferred_revenue(self):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
@@ -31,23 +31,18 @@ def execute(filters=None):
|
||||
def get_data(filters, dimension_list):
|
||||
company_currency = erpnext.get_company_currency(filters.company)
|
||||
|
||||
acc = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[
|
||||
"name",
|
||||
"account_number",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"include_in_gross",
|
||||
"account_type",
|
||||
"is_group",
|
||||
],
|
||||
order_by="lft",
|
||||
acc = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, account_number, parent_account, lft, rgt, root_type,
|
||||
report_type, account_name, include_in_gross, account_type, is_group
|
||||
from
|
||||
`tabAccount`
|
||||
where
|
||||
company=%s
|
||||
order by lft""",
|
||||
(filters.company),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not acc:
|
||||
@@ -55,17 +50,16 @@ def get_data(filters, dimension_list):
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
|
||||
|
||||
lft_rgt = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[{"MIN": "lft", "as": "min_lft"}, {"MAX": "rgt", "as": "max_rgt"}],
|
||||
min_lft, max_rgt = frappe.db.sql(
|
||||
"""select min(lft), max(rgt) from `tabAccount`
|
||||
where company=%s""",
|
||||
(filters.company),
|
||||
)[0]
|
||||
min_lft, max_rgt = lft_rgt.min_lft, lft_rgt.max_rgt
|
||||
|
||||
account = frappe.get_all(
|
||||
"Account",
|
||||
filters={"lft": [">=", min_lft], "rgt": ["<=", max_rgt], "company": filters.company},
|
||||
pluck="name",
|
||||
account = frappe.db.sql_list(
|
||||
"""select name from `tabAccount`
|
||||
where lft >= %s and rgt <= %s and company = %s""",
|
||||
(min_lft, max_rgt, filters.company),
|
||||
)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
@@ -81,34 +75,42 @@ def get_data(filters, dimension_list):
|
||||
|
||||
|
||||
def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account):
|
||||
dimension_field = frappe.scrub(filters.get("dimension"))
|
||||
condition = get_condition(filters.get("dimension"))
|
||||
|
||||
if account:
|
||||
condition += " and account in ({})".format(", ".join([frappe.db.escape(d) for d in account]))
|
||||
|
||||
gl_filters = {
|
||||
"company": filters.get("company"),
|
||||
dimension_field: ["in", list(set(dimension_list))],
|
||||
"posting_date": ["between", [filters.get("from_date"), filters.get("to_date")]],
|
||||
"is_cancelled": 0,
|
||||
"from_date": filters.get("from_date"),
|
||||
"to_date": filters.get("to_date"),
|
||||
"finance_book": cstr(filters.get("finance_book")),
|
||||
}
|
||||
if account:
|
||||
gl_filters["account"] = ["in", account]
|
||||
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters=gl_filters,
|
||||
fields=[
|
||||
"posting_date",
|
||||
"account",
|
||||
dimension_field,
|
||||
"debit",
|
||||
"credit",
|
||||
"is_opening",
|
||||
"fiscal_year",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
"account_currency",
|
||||
],
|
||||
order_by="account, posting_date",
|
||||
)
|
||||
gl_filters["dimensions"] = tuple(set(dimension_list))
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year,
|
||||
debit_in_account_currency, credit_in_account_currency, account_currency
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
company=%(company)s
|
||||
{condition}
|
||||
and posting_date >= %(from_date)s
|
||||
and posting_date <= %(to_date)s
|
||||
and is_cancelled = 0
|
||||
order by account, posting_date""".format(
|
||||
dimension=frappe.scrub(filters.get("dimension")), condition=condition
|
||||
),
|
||||
gl_filters,
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
|
||||
for entry in gl_entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
@@ -176,6 +178,14 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
|
||||
].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0)
|
||||
|
||||
|
||||
def get_condition(dimension):
|
||||
conditions = []
|
||||
|
||||
conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s")
|
||||
|
||||
return " and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
|
||||
def get_dimensions(filters):
|
||||
meta = frappe.get_meta(filters.get("dimension"), cached=False)
|
||||
query_filters = {}
|
||||
|
||||
@@ -71,7 +71,6 @@ def get_ratios_data(filters, period_list, years):
|
||||
assets, liabilities, income, expense = get_gl_data(filters, period_list, years)
|
||||
|
||||
current_asset, total_asset = {}, {}
|
||||
fixed_asset = {}
|
||||
current_liability, total_liability = {}, {}
|
||||
net_sales, total_income = {}, {}
|
||||
cogs, total_expense = {}, {}
|
||||
@@ -94,7 +93,6 @@ def get_ratios_data(filters, period_list, years):
|
||||
quick_asset,
|
||||
total_quick_asset,
|
||||
],
|
||||
[fixed_asset, total_asset, "Fixed Asset", year, assets, "Asset", {}, 0],
|
||||
[
|
||||
current_liability,
|
||||
total_liability,
|
||||
@@ -114,7 +112,7 @@ def get_ratios_data(filters, period_list, years):
|
||||
add_solvency_ratios(
|
||||
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||||
)
|
||||
add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense)
|
||||
add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense)
|
||||
|
||||
return data
|
||||
|
||||
@@ -195,7 +193,7 @@ def add_solvency_ratios(
|
||||
data.append(return_on_equity_ratio)
|
||||
|
||||
|
||||
def add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense):
|
||||
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": _("Turnover Ratios")})
|
||||
|
||||
@@ -210,7 +208,7 @@ def add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sale
|
||||
)
|
||||
|
||||
ratio_data = [
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, fixed_asset],
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
|
||||
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
|
||||
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
|
||||
[_("Inventory Turnover Ratio"), cogs, avg_stock],
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.report.financial_ratios.financial_ratios import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestFinancialRatios(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.abbr = "_TC"
|
||||
# The report matches the group accounts by their account_type, which the
|
||||
# standard chart of accounts does not set on group accounts by default.
|
||||
self.set_account_type("Fixed Assets", "Fixed Asset")
|
||||
self.set_account_type("Direct Income", "Direct Income")
|
||||
|
||||
def set_account_type(self, account_name, account_type):
|
||||
frappe.db.set_value("Account", f"{account_name} - {self.abbr}", "account_type", account_type)
|
||||
|
||||
def test_fixed_asset_turnover_uses_net_fixed_assets(self):
|
||||
# Acquire a fixed asset worth 10,000 funded by equity.
|
||||
self.make_journal_entry("Buildings", "Capital Stock", 10000)
|
||||
# Book sales of 20,000 collected in cash. Total assets now = 30,000
|
||||
# (Buildings 10,000 + Cash 20,000), while net fixed assets stay at 10,000.
|
||||
self.make_journal_entry("Cash", "Sales", 20000)
|
||||
|
||||
columns, data = execute(self.get_report_filters())
|
||||
year_key = columns[1]["fieldname"]
|
||||
ratio_row = next((row for row in data if row.get("ratio") == "Fixed Asset Turnover Ratio"), None)
|
||||
self.assertIsNotNone(ratio_row, "Fixed Asset Turnover Ratio row not found in report output")
|
||||
|
||||
# Net Sales / Net Fixed Assets = 20,000 / 10,000 = 2.0
|
||||
# (the old behaviour divided by total assets, giving 20,000 / 30,000 = 0.667)
|
||||
self.assertEqual(ratio_row[year_key], 2.0)
|
||||
|
||||
def get_report_filters(self):
|
||||
active_fy = frappe.db.get_value(
|
||||
"Fiscal Year",
|
||||
{"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())},
|
||||
["name", "year_start_date", "year_end_date"],
|
||||
as_dict=True,
|
||||
)
|
||||
return frappe._dict(
|
||||
company=self.company,
|
||||
from_fiscal_year=active_fy.name,
|
||||
to_fiscal_year=active_fy.name,
|
||||
period_start_date=active_fy.year_start_date,
|
||||
period_end_date=active_fy.year_end_date,
|
||||
filter_based_on="Fiscal Year",
|
||||
periodicity="Yearly",
|
||||
)
|
||||
|
||||
def make_journal_entry(self, debit_account, credit_account, amount):
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.posting_date = today()
|
||||
journal_entry.company = self.company
|
||||
for account, debit, credit in (
|
||||
(debit_account, amount, 0),
|
||||
(credit_account, 0, amount),
|
||||
):
|
||||
journal_entry.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": f"{account} - {self.abbr}",
|
||||
"debit_in_account_currency": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
},
|
||||
)
|
||||
journal_entry.insert()
|
||||
journal_entry.submit()
|
||||
@@ -179,10 +179,11 @@ def get_data(
|
||||
company_currency = get_appropriate_currency(company, filters)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
for root in frappe.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
company,
|
||||
@@ -372,23 +373,13 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
|
||||
|
||||
|
||||
def get_accounts(company, root_type):
|
||||
return frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": company, "root_type": root_type},
|
||||
fields=[
|
||||
"name",
|
||||
"account_number",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"include_in_gross",
|
||||
"account_type",
|
||||
"is_group",
|
||||
],
|
||||
order_by="lft",
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group, lft, rgt
|
||||
from `tabAccount`
|
||||
where company=%s and root_type=%s order by lft""",
|
||||
(company, root_type),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -538,11 +529,7 @@ def get_accounting_entries(
|
||||
gl_entry.credit_in_account_currency
|
||||
if not group_by_account
|
||||
else Sum(gl_entry.credit_in_account_currency).as_("credit_in_account_currency"),
|
||||
# when grouping by account the non-aggregated columns must be aggregated for postgres;
|
||||
# account_currency is constant per account so Max() returns the same value.
|
||||
gl_entry.account_currency
|
||||
if not group_by_account
|
||||
else Max(gl_entry.account_currency).as_("account_currency"),
|
||||
gl_entry.account_currency,
|
||||
)
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
@@ -560,29 +547,15 @@ def get_accounting_entries(
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
if doctype == "GL Entry":
|
||||
# aggregate the non-grouped columns when grouping by account (postgres requirement)
|
||||
if group_by_account:
|
||||
query = query.select(
|
||||
Max(gl_entry.posting_date).as_("posting_date"),
|
||||
Max(gl_entry.is_opening).as_("is_opening"),
|
||||
Max(gl_entry.fiscal_year).as_("fiscal_year"),
|
||||
)
|
||||
else:
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
query = query.where(gl_entry.is_cancelled == 0)
|
||||
query = query.where(gl_entry.posting_date <= to_date)
|
||||
# FORCE INDEX is MySQL-only; postgres has no index hints (its planner uses the index anyway)
|
||||
if frappe.db.db_type != "postgres":
|
||||
query = query.force_index("posting_date_company_index")
|
||||
query = query.force_index("posting_date_company_index")
|
||||
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
query = query.where(gl_entry.is_opening == "No")
|
||||
else:
|
||||
query = query.select(
|
||||
Max(gl_entry.closing_date).as_("posting_date")
|
||||
if group_by_account
|
||||
else gl_entry.closing_date.as_("posting_date")
|
||||
)
|
||||
query = query.select(gl_entry.closing_date.as_("posting_date"))
|
||||
query = query.where(gl_entry.period_closing_voucher == period_closing_voucher)
|
||||
|
||||
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
|
||||
|
||||
@@ -12,13 +12,7 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestGeneralAndPaymentLedger(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.creditors = "Creditors - _TC"
|
||||
self.create_company()
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
|
||||
@@ -35,7 +35,7 @@ def execute(filters=None):
|
||||
if filters and filters.get("print_in_account_currency") and not filters.get("account"):
|
||||
frappe.throw(_("Select an account to print in account currency"))
|
||||
|
||||
for acc in frappe.get_all("Account", fields=["name", "is_group"]):
|
||||
for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1):
|
||||
account_details.setdefault(acc.name, acc)
|
||||
|
||||
if filters.get("party"):
|
||||
@@ -650,8 +650,10 @@ def get_result_as_list(data, filters):
|
||||
|
||||
def get_supplier_invoice_details():
|
||||
inv_details = {}
|
||||
for d in frappe.get_all(
|
||||
"Purchase Invoice", filters={"docstatus": 1, "bill_no": ["is", "set"]}, fields=["name", "bill_no"]
|
||||
for d in frappe.db.sql(
|
||||
""" select name, bill_no from `tabPurchase Invoice`
|
||||
where docstatus = 1 and bill_no is not null and bill_no != '' """,
|
||||
as_dict=1,
|
||||
):
|
||||
inv_details[d.name] = d.bill_no
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class TestGeneralLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.clear_old_entries()
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
@@ -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(
|
||||
|
||||
@@ -713,25 +713,20 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
def get_returned_invoice_items(self):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
si_item = frappe.qb.DocType("Sales Invoice Item")
|
||||
returned_invoices = (
|
||||
frappe.qb.from_(si)
|
||||
.inner_join(si_item)
|
||||
.on(si.name == si_item.parent)
|
||||
.select(
|
||||
si.name,
|
||||
si_item.item_code,
|
||||
si_item.stock_qty.as_("qty"),
|
||||
si_item.base_net_amount.as_("base_amount"),
|
||||
si.return_against,
|
||||
)
|
||||
.where(
|
||||
(si.docstatus == 1)
|
||||
& (si.is_return == 1)
|
||||
& si.posting_date.between(self.filters.from_date, self.filters.to_date)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
returned_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against
|
||||
from
|
||||
`tabSales Invoice` si, `tabSales Invoice Item` si_item
|
||||
where
|
||||
si.name = si_item.parent
|
||||
and si.docstatus = 1
|
||||
and si.is_return = 1
|
||||
and si.posting_date between %(from_date)s and %(to_date)s
|
||||
""",
|
||||
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.returned_invoices = frappe._dict()
|
||||
@@ -1246,4 +1241,7 @@ class GrossProfitGenerator:
|
||||
).setdefault(d.parent_item, []).append(d)
|
||||
|
||||
def load_non_stock_items(self):
|
||||
self.non_stock_items = frappe.get_all("Item", filters={"is_stock_item": 0}, pluck="name")
|
||||
self.non_stock_items = frappe.db.sql_list(
|
||||
"""select name from tabItem
|
||||
where is_stock_item=0"""
|
||||
)
|
||||
|
||||
@@ -14,17 +14,73 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestGrossProfit(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.finished_warehouse = "Finished Goods - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.item = "_Test Item"
|
||||
self.item2 = "_Test Item Home Desktop 100"
|
||||
self.bundle = "_Test Product Bundle Item"
|
||||
self.customer = "_Test Customer"
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_bundle()
|
||||
self.create_customer()
|
||||
self.create_sales_invoice()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Gross Profit"
|
||||
abbr = "_GP"
|
||||
if frappe.db.exists("Company", company_name):
|
||||
company = frappe.get_doc("Company", company_name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company_name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "Stores - " + abbr
|
||||
self.finished_warehouse = "Finished Goods - " + abbr
|
||||
self.income_account = "Sales - " + abbr
|
||||
self.expense_account = "Cost of Goods Sold - " + abbr
|
||||
self.debit_to = "Debtors - " + abbr
|
||||
self.creditors = "Creditors - " + abbr
|
||||
|
||||
def create_item(self):
|
||||
item = create_item(
|
||||
item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_bundle(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
|
||||
item2 = create_item(
|
||||
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item2 = item2 if isinstance(item2, str) else item2.item_code
|
||||
|
||||
# This will be parent item
|
||||
bundle = create_item(
|
||||
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
|
||||
|
||||
# Create Product Bundle
|
||||
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
|
||||
|
||||
def create_customer(self):
|
||||
name = "_Test GP Customer"
|
||||
if frappe.db.exists("Customer", name):
|
||||
self.customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
@@ -158,7 +214,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 150.0,
|
||||
@@ -187,7 +243,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 100.0,
|
||||
@@ -219,7 +275,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"item_code": self.item2,
|
||||
"s_warehouse": "",
|
||||
"t_warehouse": self.finished_warehouse,
|
||||
"qty": 2,
|
||||
"qty": 1,
|
||||
"basic_rate": 100,
|
||||
"conversion_factor": item.conversion_factor or 1.0,
|
||||
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
||||
@@ -319,7 +375,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 4.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 125.0,
|
||||
@@ -360,10 +416,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 100.0,
|
||||
"avg._selling_rate": 100,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": 0.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": 0.0,
|
||||
@@ -383,7 +439,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"""
|
||||
# Make Cr Note
|
||||
sinv = self.create_sales_invoice(
|
||||
qty=-1, rate=200, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
sinv.is_return = 1
|
||||
sinv.items[0].allow_zero_valuation_rate = 1
|
||||
@@ -406,14 +462,14 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": -1.0,
|
||||
"avg._selling_rate": 200.0,
|
||||
"valuation_rate": 100.0,
|
||||
"selling_amount": -200.0,
|
||||
"buying_amount": -100.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": -50.0,
|
||||
"gross_profit_%": -100.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry}
|
||||
@@ -499,7 +555,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _TC",
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 4.0,
|
||||
"avg._selling_rate": 800.0,
|
||||
"valuation_rate": 700.0,
|
||||
@@ -562,7 +618,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
def test_gross_profit_groupby_invoices(self):
|
||||
create_sales_invoice(
|
||||
qty=1,
|
||||
rate=200,
|
||||
rate=100,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
@@ -584,10 +640,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 200.0)
|
||||
self.assertEqual(total.buying_amount, 100.0)
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 50.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
@@ -596,7 +652,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
return_inv_date = add_days(month_end_date, 1)
|
||||
|
||||
# create sales invoice on month start date
|
||||
sinv = self.create_sales_invoice(qty=1, rate=200, do_not_save=True, do_not_submit=True)
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = sales_inv_date
|
||||
sinv.save().submit()
|
||||
@@ -615,10 +671,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 200.0)
|
||||
self.assertEqual(total.buying_amount, 100.0)
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 50.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update({"to_date": return_inv_date})
|
||||
@@ -636,10 +692,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, -200.0)
|
||||
self.assertEqual(total.buying_amount, -100.0)
|
||||
self.assertEqual(total.selling_amount, -100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, -100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), -50.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), -100.0)
|
||||
|
||||
def test_sales_person_wise_gross_profit(self):
|
||||
sales_person = make_sales_person("_Test Sales Person")
|
||||
@@ -670,10 +726,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total[5], 1000.0) # selling amount
|
||||
self.assertEqual(total[6], 1000.0) # buying amount
|
||||
self.assertEqual(total[7], 0.0) # gross profit
|
||||
self.assertEqual(total[8], 0.0) # gross profit %
|
||||
self.assertEqual(total[5], 1000.0)
|
||||
self.assertEqual(total[6], 0.0)
|
||||
self.assertEqual(total[7], 1000.0)
|
||||
self.assertEqual(total[8], 100.0)
|
||||
|
||||
def test_drop_ship(self):
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import CurDate, DateDiff
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@@ -102,11 +102,11 @@ def get_sales_details(filters):
|
||||
child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item"
|
||||
child = frappe.qb.DocType(child_doctype)
|
||||
|
||||
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
|
||||
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
|
||||
current_date = CustomFunction("CURRENT_DATE", [])
|
||||
|
||||
# DateDiff is cross-database (DATEDIFF on MariaDB, date subtraction on postgres); CurDate()
|
||||
# renders the bare CURRENT_DATE keyword. Yields the integer number of days.
|
||||
days_since_last_order = DateDiff(CurDate(), date_col)
|
||||
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
|
||||
days_since_last_order = date_diff(current_date(), date_col)
|
||||
|
||||
sales_data = (
|
||||
frappe.qb.from_(parent)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.report.inactive_sales_items.inactive_sales_items import execute
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestInactiveSalesItems(ERPNextTestSuite):
|
||||
def test_days_since_last_order_is_computed(self):
|
||||
# Exercises the date-arithmetic path (DATEDIFF/CURRENT_DATE on mariadb, date subtraction on
|
||||
# postgres) which must produce the same integer day count on both databases.
|
||||
item = make_item("_Test Inactive Sales Item").name
|
||||
old_date = add_days(today(), -120)
|
||||
so = make_sales_order(item=item, qty=3, rate=150, transaction_date=old_date)
|
||||
so.items[0].delivery_date = add_days(old_date, 7)
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
columns, data = execute(frappe._dict({"based_on": "Sales Order", "days": 30}))
|
||||
self.assertTrue(columns)
|
||||
row = next((r for r in data if r.get("item") == item and r.get("days_since_last_order")), None)
|
||||
self.assertIsNotNone(row, "Inactive item should appear in the report")
|
||||
self.assertGreaterEqual(row["days_since_last_order"], 30)
|
||||
|
||||
def test_report_runs_for_sales_invoice(self):
|
||||
columns, _data = execute(frappe._dict({"based_on": "Sales Invoice", "days": 30}))
|
||||
self.assertTrue(columns)
|
||||
@@ -376,7 +376,7 @@ def get_items(filters, additional_table_columns):
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
return dict(frappe.get_all("Company", fields=["name", "stock_received_but_not_billed"], as_list=True))
|
||||
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
|
||||
|
||||
|
||||
def get_purchase_receipts_against_purchase_order(item_list):
|
||||
@@ -384,11 +384,16 @@ def get_purchase_receipts_against_purchase_order(item_list):
|
||||
po_item_rows = list(set(d.po_detail for d in item_list))
|
||||
|
||||
if po_item_rows:
|
||||
purchase_receipts = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"docstatus": 1, "purchase_order_item": ["in", po_item_rows]},
|
||||
fields=["parent", "purchase_order_item"],
|
||||
group_by="purchase_order_item, parent",
|
||||
purchase_receipts = frappe.db.sql(
|
||||
"""
|
||||
select parent, purchase_order_item
|
||||
from `tabPurchase Receipt Item`
|
||||
where docstatus=1 and purchase_order_item in (%s)
|
||||
group by purchase_order_item, parent
|
||||
"""
|
||||
% (", ".join(["%s"] * len(po_item_rows))),
|
||||
tuple(po_item_rows),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for pr in purchase_receipts:
|
||||
|
||||
@@ -9,9 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
pi = make_purchase_invoice(
|
||||
|
||||
@@ -9,11 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
|
||||
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -9,12 +9,42 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestPaymentLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.create_company()
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
doctypes = []
|
||||
doctypes.append(qb.DocType("GL Entry"))
|
||||
doctypes.append(qb.DocType("Payment Ledger Entry"))
|
||||
doctypes.append(qb.DocType("Sales Invoice"))
|
||||
doctypes.append(qb.DocType("Payment Entry"))
|
||||
|
||||
for doctype in doctypes:
|
||||
qb.from_(doctype).delete().where(doctype.company == self.company).run()
|
||||
|
||||
def create_company(self):
|
||||
name = "Test Payment Ledger"
|
||||
company = None
|
||||
if frappe.db.exists("Company", name):
|
||||
company = frappe.get_doc("Company", name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "All Warehouses" + " - " + company.abbr
|
||||
self.income_account = company.default_income_account
|
||||
self.expense_account = company.default_expense_account
|
||||
self.debit_to = company.default_receivable_account
|
||||
|
||||
def test_unpaid_invoice_outstanding(self):
|
||||
sinv = create_sales_invoice(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user