mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-19 12:44:03 +00:00
Compare commits
135 Commits
fix-je-par
...
l10n_devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
060cd9f320 | ||
|
|
eb6530208b | ||
|
|
526f91f6b5 | ||
|
|
4465ebaeb5 | ||
|
|
65d9f78409 | ||
|
|
acda04a4bd | ||
|
|
d1e167815f | ||
|
|
588dfac4cd | ||
|
|
ac26c01e52 | ||
|
|
c0d2bd7bce | ||
|
|
4d03e915f7 | ||
|
|
09beed9cc3 | ||
|
|
0737a4cfbb | ||
|
|
1332ad7583 | ||
|
|
058be399c3 | ||
|
|
e482c846c8 | ||
|
|
6a60f072a8 | ||
|
|
18c1f0f04d | ||
|
|
217c107549 | ||
|
|
44ca5878b8 | ||
|
|
66e82c56b1 | ||
|
|
65c0d35f2e | ||
|
|
e9eca10927 | ||
|
|
6849d292f8 | ||
|
|
261b7fe7aa | ||
|
|
30568d36d0 | ||
|
|
8527e78820 | ||
|
|
ae4a5e82b0 | ||
|
|
196fce9792 | ||
|
|
449004d29a | ||
|
|
bae3668bd0 | ||
|
|
8ce0e5386a | ||
|
|
4ba042c7c7 | ||
|
|
59ad76c21e | ||
|
|
1efe0be379 | ||
|
|
6bb7fa6d68 | ||
|
|
ef5feb613a | ||
|
|
2fa9d7cee6 | ||
|
|
431dc208b3 | ||
|
|
0b795a628f | ||
|
|
595a4c8517 | ||
|
|
9ec224c3fd | ||
|
|
68415c341b | ||
|
|
a43df3278f | ||
|
|
4d06b01abf | ||
|
|
a04d54b2fb | ||
|
|
0476f318e4 | ||
|
|
7fbfa35f95 | ||
|
|
bbf506e848 | ||
|
|
725fd8ca97 | ||
|
|
85191d1cac | ||
|
|
93021a9d45 | ||
|
|
0afc6dd363 | ||
|
|
6dc2e43dd6 | ||
|
|
d34e4b8783 | ||
|
|
501acd0414 | ||
|
|
113943f851 | ||
|
|
c124e90a89 | ||
|
|
9096b4a9df | ||
|
|
e69eaa5102 | ||
|
|
e77b27ae99 | ||
|
|
60235f4b2b | ||
|
|
463103ebf1 | ||
|
|
a3ec98a57c | ||
|
|
9ab8803fed | ||
|
|
8a566e6ba5 | ||
|
|
812a06cf44 | ||
|
|
23778c3875 | ||
|
|
24a66d10e7 | ||
|
|
3faaa87645 | ||
|
|
afeaba5142 | ||
|
|
1a016cbcd6 | ||
|
|
7532ec9f9a | ||
|
|
48e66d04e6 | ||
|
|
59a69fc497 | ||
|
|
a899183087 | ||
|
|
3d109571ee | ||
|
|
d079677500 | ||
|
|
6e62750c2f | ||
|
|
faadc1620b | ||
|
|
f249d57b30 | ||
|
|
3f66541b99 | ||
|
|
e7c2f8ee11 | ||
|
|
8dacf62da0 | ||
|
|
935746e752 | ||
|
|
4e2a10e496 | ||
|
|
a32c784084 | ||
|
|
2d24eedab2 | ||
|
|
ef1fbb7899 | ||
|
|
b5ecc9e6bd | ||
|
|
f195044fd1 | ||
|
|
004087097c | ||
|
|
992015424b | ||
|
|
a86b169d8b | ||
|
|
e183e32619 | ||
|
|
28992eb2f4 | ||
|
|
0ae61c4921 | ||
|
|
8190696d36 | ||
|
|
b12032485b | ||
|
|
be0f571d62 | ||
|
|
3a1e4d14f3 | ||
|
|
1fda0dfb9b | ||
|
|
1b4487450c | ||
|
|
9564f677e4 | ||
|
|
62f6d18143 | ||
|
|
1dbdf85ddc | ||
|
|
026ec8a6d9 | ||
|
|
a9207f1e12 | ||
|
|
2a5ba9050e | ||
|
|
02d41b1dac | ||
|
|
501c8087cb | ||
|
|
13e1f84eb1 | ||
|
|
75394baa28 | ||
|
|
fb59f825ee | ||
|
|
34f78f7261 | ||
|
|
987f606b4d | ||
|
|
b1b510c824 | ||
|
|
066158174e | ||
|
|
07d073da0d | ||
|
|
ba1b1ee20d | ||
|
|
213adc9ebe | ||
|
|
0e7d45b1af | ||
|
|
609ccc3cb1 | ||
|
|
dceb9a3c6c | ||
|
|
1a8d73cbbe | ||
|
|
40942401df | ||
|
|
c1bef53f92 | ||
|
|
986af3852c | ||
|
|
021b807057 | ||
|
|
bc7c0de208 | ||
|
|
de3df6bcef | ||
|
|
6771daf6a1 | ||
|
|
2d93c5835a | ||
|
|
288f36bbd7 | ||
|
|
a3c9072812 |
@@ -48,7 +48,7 @@
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vite": "^8.0.11"
|
||||
"vite": "^8.0.16"
|
||||
},
|
||||
"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 && <ErrorBanner error={error} />}
|
||||
{error && <div className="py-2"><ErrorBanner error={error} /></div>}
|
||||
<div className="py-4">
|
||||
<CurrencyFormField
|
||||
name="balance"
|
||||
|
||||
@@ -33,6 +33,16 @@ 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.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==
|
||||
"@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==
|
||||
|
||||
"@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.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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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==
|
||||
"@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==
|
||||
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.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-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-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/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/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.11:
|
||||
version "3.3.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
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==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -3119,22 +3119,17 @@ 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.14:
|
||||
version "8.5.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
|
||||
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
|
||||
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==
|
||||
dependencies:
|
||||
nanoid "^3.3.11"
|
||||
nanoid "^3.3.12"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
@@ -3394,29 +3389,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.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==
|
||||
rolldown@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac"
|
||||
integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==
|
||||
dependencies:
|
||||
"@oxc-project/types" "=0.128.0"
|
||||
"@rolldown/pluginutils" "1.0.0-rc.18"
|
||||
"@oxc-project/types" "=0.133.0"
|
||||
"@rolldown/pluginutils" "^1.0.0"
|
||||
optionalDependencies:
|
||||
"@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"
|
||||
"@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"
|
||||
|
||||
scheduler@^0.27.0:
|
||||
version "0.27.0"
|
||||
@@ -3540,18 +3535,10 @@ 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:
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.4"
|
||||
@@ -3725,16 +3712,16 @@ vfile@^6.0.0:
|
||||
"@types/unist" "^3.0.0"
|
||||
vfile-message "^4.0.0"
|
||||
|
||||
vite@^8.0.11:
|
||||
version "8.0.11"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.11.tgz#d128fe82a0dd24da5127d20560735f1cd7ade0a6"
|
||||
integrity sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==
|
||||
vite@^8.0.16:
|
||||
version "8.0.16"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6"
|
||||
integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==
|
||||
dependencies:
|
||||
lightningcss "^1.32.0"
|
||||
picomatch "^4.0.4"
|
||||
postcss "^8.5.14"
|
||||
rolldown "1.0.0-rc.18"
|
||||
tinyglobby "^0.2.16"
|
||||
postcss "^8.5.15"
|
||||
rolldown "1.0.3"
|
||||
tinyglobby "^0.2.17"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
|
||||
@@ -22,11 +22,13 @@ class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_usd_payable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
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"
|
||||
|
||||
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-04-11 19:48:13.622253",
|
||||
"doctype": "DocType",
|
||||
@@ -7,7 +8,8 @@
|
||||
"field_order": [
|
||||
"bank_account",
|
||||
"date",
|
||||
"balance"
|
||||
"balance",
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -31,12 +33,20 @@
|
||||
"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-04-11 19:49:45.374695",
|
||||
"modified": "2026-06-16 22:17:48.007982",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account Balance",
|
||||
|
||||
@@ -16,6 +16,7 @@ class BankAccountBalance(Document):
|
||||
|
||||
balance: DF.Currency
|
||||
bank_account: DF.Link
|
||||
company: DF.Link | None
|
||||
date: DF.Date
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -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 Sum
|
||||
from frappe.query_builder.functions import Max, Sum
|
||||
from frappe.utils import cint, create_batch, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
@@ -1410,12 +1410,14 @@ def get_je_matching_query(
|
||||
Sum(getattr(jea, amount_field)).as_("paid_amount"),
|
||||
ConstantColumn("Journal Entry").as_("doctype"),
|
||||
je.name,
|
||||
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"),
|
||||
# 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"),
|
||||
)
|
||||
.where(je.docstatus == 1)
|
||||
.where(je.voucher_type != "Opening Entry")
|
||||
@@ -1423,7 +1425,7 @@ def get_je_matching_query(
|
||||
.where(jea.account == common_filters.bank_account)
|
||||
.where(filter_by_date)
|
||||
.groupby(je.name)
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
.orderby(Max(je.cheque_date) if cint(filter_by_reference_date) else Max(je.posting_date))
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
|
||||
@@ -17,9 +17,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
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.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -5,6 +5,8 @@ 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
|
||||
|
||||
|
||||
@@ -478,30 +480,28 @@ 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 {}
|
||||
|
||||
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,
|
||||
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)
|
||||
)
|
||||
|
||||
entries = {}
|
||||
@@ -523,31 +523,32 @@ def get_total_allocated_amount(docs):
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
# 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,
|
||||
# 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)
|
||||
)
|
||||
|
||||
payment_allocation_details = {}
|
||||
|
||||
@@ -104,6 +104,36 @@ 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,9 +11,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankTransactionRule(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
@@ -43,13 +44,17 @@ class CashierClosing(Document):
|
||||
self.make_calculations()
|
||||
|
||||
def get_outstanding(self):
|
||||
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),
|
||||
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()
|
||||
)
|
||||
self.outstanding_amount = flt(values[0][0] if values else 0)
|
||||
|
||||
|
||||
@@ -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 NullIf, Sum
|
||||
from frappe.query_builder.functions import Max, NullIf, Sum
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
@@ -188,12 +188,18 @@ class ExchangeRateRevaluation(Document):
|
||||
accounts = [x[0] for x in res]
|
||||
|
||||
if accounts:
|
||||
having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
|
||||
(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
|
||||
)
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
|
||||
# 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)
|
||||
)
|
||||
|
||||
# conditions
|
||||
conditions = []
|
||||
conditions.append(gle.account.isin(accounts))
|
||||
@@ -209,17 +215,15 @@ class ExchangeRateRevaluation(Document):
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.account,
|
||||
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"
|
||||
),
|
||||
# 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).
|
||||
)
|
||||
.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.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
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.set_system_and_company_settings()
|
||||
|
||||
def set_system_and_company_settings(self):
|
||||
|
||||
@@ -319,56 +319,48 @@ class InvoiceDiscounting(AccountsController):
|
||||
@frappe.whitelist()
|
||||
def get_invoices(filters: str):
|
||||
filters = frappe._dict(json.loads(filters))
|
||||
cond = []
|
||||
if filters.customer:
|
||||
cond.append("customer=%(customer)s")
|
||||
if filters.from_date:
|
||||
cond.append("posting_date >= %(from_date)s")
|
||||
if filters.to_date:
|
||||
cond.append("posting_date <= %(to_date)s")
|
||||
if filters.min_amount:
|
||||
cond.append("base_grand_total >= %(min_amount)s")
|
||||
if filters.max_amount:
|
||||
cond.append("base_grand_total <= %(max_amount)s")
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
di = frappe.qb.DocType("Discounted Invoice")
|
||||
|
||||
where_condition = ""
|
||||
if cond:
|
||||
where_condition += " and " + " and ".join(cond)
|
||||
discounted = frappe.qb.from_(di).select(di.sales_invoice).where(di.docstatus == 1)
|
||||
|
||||
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,
|
||||
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))
|
||||
)
|
||||
|
||||
if filters.customer:
|
||||
query = query.where(si.customer == filters.customer)
|
||||
if filters.from_date:
|
||||
query = query.where(si.posting_date >= filters.from_date)
|
||||
if filters.to_date:
|
||||
query = query.where(si.posting_date <= filters.to_date)
|
||||
if filters.min_amount:
|
||||
query = query.where(si.base_grand_total >= filters.min_amount)
|
||||
if filters.max_amount:
|
||||
query = query.where(si.base_grand_total <= filters.max_amount)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def get_party_account_based_on_invoice_discounting(sales_invoice):
|
||||
party_account = None
|
||||
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,
|
||||
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)
|
||||
)
|
||||
if invoice_discounting:
|
||||
if invoice_discounting[0].status == "Disbursed":
|
||||
|
||||
@@ -484,12 +484,6 @@ 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,13 +662,6 @@ 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,10 +12,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.configure_monitoring_tool()
|
||||
self.clear_old_entries()
|
||||
|
||||
def configure_monitoring_tool(self):
|
||||
monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
|
||||
@@ -52,12 +52,11 @@ class ModeofPayment(Document):
|
||||
|
||||
def validate_pos_mode_of_payment(self):
|
||||
if not self.enabled:
|
||||
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 = frappe.get_all(
|
||||
"Sales Invoice Payment",
|
||||
filters={"parenttype": "POS Profile", "mode_of_payment": self.name},
|
||||
pluck="parent",
|
||||
)
|
||||
pos_profiles = list(map(lambda x: x[0], pos_profiles))
|
||||
|
||||
if pos_profiles:
|
||||
message = _(
|
||||
|
||||
@@ -270,6 +270,13 @@ 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:
|
||||
@@ -284,7 +291,7 @@ def start_import(invoices):
|
||||
names.append(doc.name)
|
||||
except Exception:
|
||||
errors += 1
|
||||
frappe.db.rollback()
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
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 Tuple
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.query_builder import Case, Tuple
|
||||
from frappe.query_builder.functions import Abs, Count, Max
|
||||
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,13 +766,19 @@ 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.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,
|
||||
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"],
|
||||
)
|
||||
|
||||
if not je_accounts:
|
||||
@@ -857,27 +863,17 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision)
|
||||
|
||||
ps = frappe.qb.DocType("Payment Schedule")
|
||||
if cancel:
|
||||
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],
|
||||
),
|
||||
)
|
||||
(
|
||||
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()
|
||||
else:
|
||||
if allocated_amount > outstanding:
|
||||
frappe.throw(
|
||||
@@ -887,26 +883,15 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
if allocated_amount and outstanding:
|
||||
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],
|
||||
),
|
||||
)
|
||||
(
|
||||
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()
|
||||
|
||||
def get_allocated_amount_in_transaction_currency(
|
||||
self, allocated_amount, reference_doctype, reference_docname
|
||||
@@ -1216,11 +1201,7 @@ 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.sql(
|
||||
"""delete from `tabPayment Entry Reference`
|
||||
where parent = %s and allocated_amount = 0""",
|
||||
self.name,
|
||||
)
|
||||
frappe.db.delete("Payment Entry Reference", {"parent": self.name, "allocated_amount": 0})
|
||||
|
||||
def set_title(self):
|
||||
if frappe.flags.in_import and self.title:
|
||||
@@ -1876,7 +1857,7 @@ def get_matched_payment_request_of_references(references=None):
|
||||
PR.reference_doctype,
|
||||
PR.reference_name,
|
||||
PR.outstanding_amount.as_("allocated_amount"),
|
||||
PR.name.as_("payment_request"),
|
||||
Max(PR.name).as_("payment_request"), # count == 1 below ⇒ one row per group; postgres-safe
|
||||
Count("*").as_("count"),
|
||||
)
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
|
||||
@@ -2315,12 +2296,7 @@ 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"
|
||||
@@ -2329,38 +2305,38 @@ def get_orders_to_be_billed(
|
||||
grand_total_field = "grand_total"
|
||||
rounded_total_field = "rounded_total"
|
||||
|
||||
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,
|
||||
voucher = frappe.qb.DocType(voucher_type)
|
||||
invoice_amount = (
|
||||
Case()
|
||||
.when(voucher[rounded_total_field] != 0, voucher[rounded_total_field])
|
||||
.else_(voucher[grand_total_field])
|
||||
)
|
||||
|
||||
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 (
|
||||
@@ -2409,8 +2385,8 @@ def get_negative_outstanding_invoices(
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"{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,
|
||||
'{voucher_type}' as voucher_type, name as voucher_no, {account} as account,
|
||||
coalesce(nullif({rounded_total_field}, 0), {grand_total_field}) as invoice_amount,
|
||||
outstanding_amount, posting_date,
|
||||
due_date, conversion_rate as exchange_rate
|
||||
from
|
||||
@@ -3272,27 +3248,28 @@ 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 = "credit_in_account_currency - debit_in_account_currency"
|
||||
dr_or_cr = gle.credit_in_account_currency - gle.debit_in_account_currency
|
||||
else:
|
||||
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
|
||||
dr_or_cr = gle.debit_in_account_currency - gle.credit_in_account_currency
|
||||
|
||||
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),
|
||||
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()
|
||||
)
|
||||
|
||||
return paid_amount[0][0] if paid_amount else 0
|
||||
return (paid_amount[0][0] or 0) if paid_amount else 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -34,8 +34,14 @@ 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,14 +1037,17 @@ 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),
|
||||
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0),
|
||||
(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),
|
||||
)
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
|
||||
@@ -10,76 +10,22 @@ 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.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
|
||||
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"
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
@@ -152,18 +98,6 @@ 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,23 +60,32 @@ 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.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},
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@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.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},
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -629,11 +629,9 @@ class PaymentRequest(Document):
|
||||
|
||||
def check_if_payment_entry_exists(self):
|
||||
if self.status == "Paid":
|
||||
if frappe.get_all(
|
||||
if frappe.db.exists(
|
||||
"Payment Entry Reference",
|
||||
filters={"reference_name": self.reference_name, "docstatus": ["<", 2]},
|
||||
fields=["parent"],
|
||||
limit=1,
|
||||
{"reference_name": self.reference_name, "docstatus": ["<", 2]},
|
||||
):
|
||||
frappe.throw(_("Payment Entry already exists"), title=_("Error"))
|
||||
|
||||
@@ -1212,10 +1210,11 @@ def get_dummy_message(doc):
|
||||
@frappe.whitelist()
|
||||
def get_subscription_details(reference_doctype: str, reference_name: str):
|
||||
if reference_doctype == "Sales Invoice":
|
||||
subscriptions = frappe.db.sql(
|
||||
"""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",
|
||||
reference_name,
|
||||
as_dict=1,
|
||||
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
|
||||
)
|
||||
subscription_plans = []
|
||||
for subscription in subscriptions:
|
||||
|
||||
@@ -18,7 +18,6 @@ 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(
|
||||
@@ -27,10 +26,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
jv1.company = "Test PCV Company"
|
||||
jv1.save()
|
||||
jv1.submit()
|
||||
|
||||
@@ -40,10 +39,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cost of Goods Sold - TPC",
|
||||
account2="Cash - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
jv2.company = "Test PCV Company"
|
||||
jv2.save()
|
||||
jv2.submit()
|
||||
|
||||
@@ -67,14 +66,13 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertEqual(pcv_gle, expected_gle)
|
||||
|
||||
def test_cost_center_wise_posting(self):
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
|
||||
cost_center1 = create_cost_center("Main")
|
||||
cost_center2 = create_cost_center("Western Branch")
|
||||
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
cost_center=cost_center1,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
@@ -85,7 +83,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
cost_center=cost_center2,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
@@ -130,12 +128,11 @@ 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=company,
|
||||
company="Test PCV Company",
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
cost_center=cost_center,
|
||||
@@ -152,9 +149,9 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
amount=400,
|
||||
cost_center=cost_center,
|
||||
posting_date="2021-03-15",
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
)
|
||||
jv.company = company
|
||||
jv.company = "Test PCV Company"
|
||||
jv.finance_book = create_finance_book().name
|
||||
jv.save()
|
||||
jv.submit()
|
||||
@@ -181,7 +178,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertSequenceEqual(pcv_gle, expected_gle)
|
||||
|
||||
def test_gl_entries_restrictions(self):
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
@@ -192,16 +188,15 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
jv1.company = "Test PCV 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")
|
||||
|
||||
@@ -211,10 +206,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center1,
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
jv1.company = "Test PCV Company"
|
||||
jv1.save()
|
||||
jv1.submit()
|
||||
|
||||
@@ -224,10 +219,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
jv2.company = "Test PCV Company"
|
||||
jv2.save()
|
||||
jv2.submit()
|
||||
|
||||
@@ -254,11 +249,11 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
save=False,
|
||||
)
|
||||
|
||||
jv3.company = company
|
||||
jv3.company = "Test PCV Company"
|
||||
jv3.save()
|
||||
jv3.submit()
|
||||
|
||||
@@ -293,12 +288,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": company}, "name")
|
||||
warehouse = frappe.db.get_value("Warehouse", {"company": "Test PCV Company"}, "name")
|
||||
|
||||
repost_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"company": company,
|
||||
"company": "Test PCV Company",
|
||||
"posting_date": "2020-03-15",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": "Test Item 1",
|
||||
@@ -339,7 +334,6 @@ 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(
|
||||
@@ -348,10 +342,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
company="Test PCV Company",
|
||||
save=False,
|
||||
)
|
||||
jv.company = company
|
||||
jv.company = "Test PCV Company"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
@@ -377,19 +371,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
|
||||
|
||||
|
||||
def create_company():
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": "Test PCV Company",
|
||||
"country": "United States",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
)
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
return company.name
|
||||
|
||||
|
||||
def create_account():
|
||||
account = frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -553,7 +553,8 @@ 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"),
|
||||
gle.account_currency,
|
||||
# account_currency is constant per grouped account -> Max() keeps the GROUP BY postgres-valid
|
||||
Max(gle.account_currency).as_("account_currency"),
|
||||
).where(
|
||||
(gle.company.eq(company))
|
||||
& (gle.is_cancelled.eq(0))
|
||||
|
||||
@@ -25,10 +25,8 @@ class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.company = "_Test Company"
|
||||
self.create_customer(customer_name="Other Customer")
|
||||
self.clear_old_entries()
|
||||
self.si = create_sales_invoice()
|
||||
create_sales_invoice(customer="Other Customer")
|
||||
|
||||
|
||||
@@ -16,12 +16,14 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_supplier()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
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"
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
@@ -372,7 +374,6 @@ 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()
|
||||
|
||||
@@ -423,7 +424,11 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
def test_07_adv_from_so_to_invoice(self):
|
||||
self.enable_advance_as_liability()
|
||||
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"
|
||||
)
|
||||
|
||||
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, Sum
|
||||
from frappe.query_builder.functions import Abs, Max, 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", True)
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", 1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -120,18 +120,20 @@ def get_linked_payments_for_doc(
|
||||
res = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.company,
|
||||
ple.voucher_type.as_("reference_doctype"),
|
||||
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.voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
Max(ple.account_currency).as_("account_currency"),
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.voucher_no, ple.against_voucher_no)
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.having(Abs(Sum(ple.amount_in_account_currency)) > 0)
|
||||
# deterministic order across backends (postgres GROUP BY does not imply ordering)
|
||||
.orderby(ple.voucher_no)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return res
|
||||
@@ -146,17 +148,19 @@ def get_linked_payments_for_doc(
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company,
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
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.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
Max(ple.account_currency).as_("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)
|
||||
@@ -180,15 +184,18 @@ def get_linked_advances(company, docname):
|
||||
return (
|
||||
qb.from_(adv)
|
||||
.select(
|
||||
adv.company,
|
||||
adv.against_voucher_type.as_("reference_doctype"),
|
||||
# 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.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(adv.amount)).as_("allocated_amount"),
|
||||
adv.currency,
|
||||
Max(adv.currency).as_("currency"),
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.having(Abs(Sum(adv.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)
|
||||
)
|
||||
|
||||
|
||||
@@ -543,11 +543,19 @@ def get_party_gle_currency(party_type, party, company):
|
||||
|
||||
def get_party_gle_account(party_type, party, company):
|
||||
def generator():
|
||||
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},
|
||||
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()
|
||||
)
|
||||
|
||||
return existing_gle_account[0][0] if existing_gle_account else None
|
||||
|
||||
@@ -9,11 +9,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_supplier(currency="USD", supplier_name="Test Supplier2")
|
||||
self.create_usd_payable_account()
|
||||
self.company = "_Test Company"
|
||||
self.item = "_Test Item"
|
||||
self.supplier = "_Test Supplier 2"
|
||||
self.creditors_usd = "_Test Payable USD - _TC"
|
||||
|
||||
def test_accounts_payable_for_foreign_currency_supplier(self):
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.query_builder.functions import Date, Max, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -427,32 +427,21 @@ class ReceivablePayableReport:
|
||||
self.delivery_notes = frappe._dict()
|
||||
|
||||
# delivery note link inside sales invoice
|
||||
# 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,
|
||||
si_against_dn = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"docstatus": 1, "parent": ["in", list(self.invoices)]},
|
||||
fields=["parent", "delivery_note"],
|
||||
)
|
||||
|
||||
for d in si_against_dn:
|
||||
if d.delivery_note:
|
||||
self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note)
|
||||
|
||||
# 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,
|
||||
dn_against_si = frappe.get_all(
|
||||
"Delivery Note Item",
|
||||
filters={"against_sales_invoice": ["in", list(self.invoices)]},
|
||||
fields=["parent", "against_sales_invoice"],
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
for d in dn_against_si:
|
||||
@@ -476,14 +465,10 @@ class ReceivablePayableReport:
|
||||
|
||||
# Get Sales Team
|
||||
if self.filters.show_sales_person:
|
||||
# nosemgrep
|
||||
sales_team = frappe.db.sql(
|
||||
"""
|
||||
select parent, sales_person
|
||||
from `tabSales Team`
|
||||
where parenttype = 'Sales Invoice'
|
||||
""",
|
||||
as_dict=1,
|
||||
sales_team = frappe.get_all(
|
||||
"Sales Team",
|
||||
filters={"parenttype": "Sales Invoice"},
|
||||
fields=["parent", "sales_person"],
|
||||
)
|
||||
for d in sales_team:
|
||||
self.invoice_details.setdefault(d.parent, {}).setdefault("sales_team", []).append(
|
||||
@@ -548,22 +533,31 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_payment_terms(self, row):
|
||||
# build payment_terms for row
|
||||
# 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,
|
||||
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)
|
||||
)
|
||||
|
||||
original_row = frappe._dict(row)
|
||||
@@ -661,7 +655,6 @@ 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)
|
||||
@@ -674,11 +667,14 @@ class ReceivablePayableReport:
|
||||
(pe.posting_date).as_("future_date"),
|
||||
(pe_ref.allocated_amount).as_("future_amount"),
|
||||
(pe.reference_no).as_("future_ref"),
|
||||
ifelse(
|
||||
# CASE is portable; MySQL's IF() does not exist on postgres
|
||||
query_builder.Case()
|
||||
.when(
|
||||
pe.payment_type == "Receive",
|
||||
pe.source_exchange_rate * pe_ref.allocated_amount,
|
||||
pe.target_exchange_rate * pe_ref.allocated_amount,
|
||||
).as_("future_amount_in_base_currency"),
|
||||
)
|
||||
.else_(pe.target_exchange_rate * pe_ref.allocated_amount)
|
||||
.as_("future_amount_in_base_currency"),
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus < 2)
|
||||
@@ -695,11 +691,13 @@ class ReceivablePayableReport:
|
||||
.inner_join(jea)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
jea.reference_name.as_("invoice_no"),
|
||||
jea.party,
|
||||
jea.party_type,
|
||||
je.posting_date.as_("future_date"),
|
||||
je.cheque_no.as_("future_ref"),
|
||||
# Sum() below makes this an implicit aggregate (no GROUP BY); the non-aggregated columns
|
||||
# are arbitrary per the single group on MySQL -> Max() keeps it valid on postgres.
|
||||
Max(jea.reference_name).as_("invoice_no"),
|
||||
Max(jea.party).as_("party"),
|
||||
Max(jea.party_type).as_("party_type"),
|
||||
Max(je.posting_date).as_("future_date"),
|
||||
Max(je.cheque_no).as_("future_ref"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus < 2)
|
||||
@@ -712,30 +710,25 @@ class ReceivablePayableReport:
|
||||
|
||||
if self.filters.get("party"):
|
||||
if self.account_type == "Payable":
|
||||
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"))
|
||||
future_amount = Sum(jea.debit_in_account_currency - jea.credit_in_account_currency)
|
||||
future_amount_in_base_currency = Sum(jea.debit - jea.credit)
|
||||
else:
|
||||
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"))
|
||||
future_amount = Sum(jea.credit_in_account_currency - jea.debit_in_account_currency)
|
||||
future_amount_in_base_currency = Sum(jea.credit - jea.debit)
|
||||
else:
|
||||
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")
|
||||
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.having(qb.Field("future_amount") > 0)
|
||||
query = query.select(
|
||||
future_amount.as_("future_amount"),
|
||||
future_amount_in_base_currency.as_("future_amount_in_base_currency"),
|
||||
)
|
||||
# use the aggregate expression in HAVING; postgres can't reference a SELECT alias there
|
||||
query = query.having(future_amount > 0)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def allocate_future_payments(self, row):
|
||||
@@ -891,16 +884,19 @@ class ReceivablePayableReport:
|
||||
if self.filters.get("sales_person"):
|
||||
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
|
||||
|
||||
# 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,
|
||||
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)
|
||||
)
|
||||
|
||||
self.sales_person_records = frappe._dict()
|
||||
|
||||
@@ -12,11 +12,17 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_usd_receivable_account()
|
||||
self.clear_old_entries()
|
||||
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"
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -11,10 +11,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
|
||||
def test_01_receivable_summary_output(self):
|
||||
"""
|
||||
|
||||
@@ -15,10 +15,7 @@ def execute(filters=None):
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
depreciation_accounts = frappe.db.sql_list(
|
||||
""" select name from tabAccount
|
||||
where ifnull(account_type, '') = 'Depreciation' """
|
||||
)
|
||||
depreciation_accounts = frappe.get_all("Account", filters={"account_type": "Depreciation"}, pluck="name")
|
||||
|
||||
filters_data = [
|
||||
["company", "=", filters.get("company")],
|
||||
@@ -33,10 +30,8 @@ def get_data(filters):
|
||||
filters_data.append(["against_voucher", "=", filters.get("asset")])
|
||||
|
||||
if filters.get("asset_category"):
|
||||
assets = frappe.db.sql_list(
|
||||
"""select name from tabAsset
|
||||
where asset_category = %s and docstatus=1""",
|
||||
filters.get("asset_category"),
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"asset_category": filters.get("asset_category"), "docstatus": 1}, pluck="name"
|
||||
)
|
||||
|
||||
filters_data.append(["against_voucher", "in", assets])
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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 import CustomFunction
|
||||
from frappe.query_builder.custom import MonthName
|
||||
from frappe.utils import add_months, flt, formatdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -113,7 +113,6 @@ 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")
|
||||
@@ -126,7 +125,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(
|
||||
@@ -137,7 +136,10 @@ def get_actual_transactions(dimension_name, filters):
|
||||
& (gle.is_cancelled == 0)
|
||||
& (budget[budget_against] == dimension_name)
|
||||
)
|
||||
.groupby(gle.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])
|
||||
.orderby(gle.fiscal_year)
|
||||
)
|
||||
|
||||
@@ -157,15 +159,11 @@ def get_actual_transactions(dimension_name, filters):
|
||||
|
||||
|
||||
def get_budget_distributions(budget):
|
||||
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,
|
||||
return frappe.get_all(
|
||||
"Budget Distribution",
|
||||
filters={"parent": budget.name},
|
||||
fields=["start_date", "end_date", "amount", "percent"],
|
||||
order_by="start_date asc",
|
||||
)
|
||||
|
||||
|
||||
@@ -351,20 +349,16 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_fiscal_years(filters):
|
||||
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 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,
|
||||
)
|
||||
|
||||
return fiscal_year
|
||||
|
||||
|
||||
def get_cost_center_with_children(cost_centers):
|
||||
"""Expand each cost center to include itself and all its descendants."""
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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,6 +7,7 @@ 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
|
||||
|
||||
@@ -213,37 +214,43 @@ 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")
|
||||
cond = """ AND (finance_book in ({}, {}, '') OR finance_book IS NULL)
|
||||
""".format(
|
||||
frappe.db.escape(filters.finance_book),
|
||||
frappe.db.escape(company_fb),
|
||||
query = query.where(
|
||||
gle.finance_book.isin([filters.finance_book, company_fb, ""]) | gle.finance_book.isnull()
|
||||
)
|
||||
else:
|
||||
cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" % (
|
||||
frappe.db.escape(cstr(filters.finance_book))
|
||||
query = query.where(
|
||||
gle.finance_book.isin([cstr(filters.finance_book), ""]) | gle.finance_book.isnull()
|
||||
)
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
cond += " and cost_center in %(cost_center)s"
|
||||
cost_centers = get_cost_centers_with_children(filters.cost_center)
|
||||
query = query.where(gle.cost_center.isin(cost_centers))
|
||||
|
||||
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
|
||||
gl_sum = query.run()
|
||||
return gl_sum[0][0] if gl_sum and gl_sum[0][0] else 0
|
||||
|
||||
|
||||
def get_start_date(period, accumulated_values, company):
|
||||
@@ -367,11 +374,10 @@ 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.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
company,
|
||||
|
||||
27
erpnext/accounts/report/cash_flow/test_cash_flow.py
Normal file
27
erpnext/accounts/report/cash_flow/test_cash_flow.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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 CustomFunction
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
@@ -93,7 +93,6 @@ 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 = (
|
||||
@@ -101,7 +100,10 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
Case()
|
||||
.when(pe.paid_from.eq(filters.account), pe.paid_amount)
|
||||
.else_(pe.received_amount)
|
||||
.as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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,11 +347,10 @@ 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.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
start_date,
|
||||
@@ -512,9 +511,11 @@ def get_companies(filters):
|
||||
def get_subsidiary_companies(company):
|
||||
lft, rgt = frappe.get_cached_value("Company", company, ["lft", "rgt"])
|
||||
|
||||
return frappe.db.sql_list(
|
||||
f"""select name from `tabCompany`
|
||||
where lft >= {lft} and rgt <= {rgt} order by lft, rgt"""
|
||||
return frappe.get_all(
|
||||
"Company",
|
||||
filters={"lft": [">=", lft], "rgt": ["<=", rgt]},
|
||||
pluck="name",
|
||||
order_by="lft, rgt",
|
||||
)
|
||||
|
||||
|
||||
@@ -604,14 +605,10 @@ def set_gl_entries_by_account(
|
||||
|
||||
company_lft, company_rgt = frappe.get_cached_value("Company", filters.get("company"), ["lft", "rgt"])
|
||||
|
||||
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,
|
||||
companies = frappe.get_all(
|
||||
"Company",
|
||||
filters={"lft": [">=", company_lft], "rgt": ["<=", company_rgt]},
|
||||
fields=["name", "default_currency"],
|
||||
)
|
||||
|
||||
currency_info = frappe._dict(
|
||||
|
||||
@@ -126,12 +126,22 @@ def get_data(filters) -> list[list]:
|
||||
|
||||
|
||||
def get_company_wise_tb_data(filters, reporting_currency, ignore_reporting_currency):
|
||||
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,
|
||||
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",
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
@@ -11,10 +11,12 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
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"
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False, **args):
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Column, functions
|
||||
from frappe.query_builder import functions
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
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
|
||||
@@ -300,8 +301,10 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
Get all sales and purchase invoices which has deferred revenue/expense items
|
||||
"""
|
||||
gle = qb.DocType("GL Entry")
|
||||
# column doesn't have an alias option
|
||||
posted = Column("posted")
|
||||
# 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")
|
||||
|
||||
if self.filters.type == "Revenue":
|
||||
inv = qb.DocType("Sales Invoice")
|
||||
@@ -327,13 +330,15 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
)
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
inv.posting_date,
|
||||
# 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_item.name.as_("item"),
|
||||
inv_item.item_name,
|
||||
inv_item.service_start_date,
|
||||
inv_item.service_end_date,
|
||||
inv_item.base_net_amount,
|
||||
deferred_account_field,
|
||||
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),
|
||||
gle.posting_date.as_("gle_posting_date"),
|
||||
functions.Sum(gle.debit).as_("debit"),
|
||||
functions.Sum(gle.credit).as_("credit"),
|
||||
|
||||
@@ -61,11 +61,16 @@ class TestDeferredRevenueAndExpense(ERPNextTestSuite, AccountsTestMixin):
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer("_Test Customer")
|
||||
self.create_supplier("_Test Furniture Supplier")
|
||||
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.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 cstr, flt
|
||||
from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
@@ -31,18 +31,23 @@ def execute(filters=None):
|
||||
def get_data(filters, dimension_list):
|
||||
company_currency = erpnext.get_company_currency(filters.company)
|
||||
|
||||
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,
|
||||
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",
|
||||
)
|
||||
|
||||
if not acc:
|
||||
@@ -50,16 +55,17 @@ def get_data(filters, dimension_list):
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
|
||||
|
||||
min_lft, max_rgt = frappe.db.sql(
|
||||
"""select min(lft), max(rgt) from `tabAccount`
|
||||
where company=%s""",
|
||||
(filters.company),
|
||||
lft_rgt = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[{"MIN": "lft", "as": "min_lft"}, {"MAX": "rgt", "as": "max_rgt"}],
|
||||
)[0]
|
||||
min_lft, max_rgt = lft_rgt.min_lft, lft_rgt.max_rgt
|
||||
|
||||
account = frappe.db.sql_list(
|
||||
"""select name from `tabAccount`
|
||||
where lft >= %s and rgt <= %s and company = %s""",
|
||||
(min_lft, max_rgt, filters.company),
|
||||
account = frappe.get_all(
|
||||
"Account",
|
||||
filters={"lft": [">=", min_lft], "rgt": ["<=", max_rgt], "company": filters.company},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
@@ -75,42 +81,34 @@ def get_data(filters, dimension_list):
|
||||
|
||||
|
||||
def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account):
|
||||
condition = get_condition(filters.get("dimension"))
|
||||
|
||||
if account:
|
||||
condition += " and account in ({})".format(", ".join([frappe.db.escape(d) for d in account]))
|
||||
dimension_field = frappe.scrub(filters.get("dimension"))
|
||||
|
||||
gl_filters = {
|
||||
"company": filters.get("company"),
|
||||
"from_date": filters.get("from_date"),
|
||||
"to_date": filters.get("to_date"),
|
||||
"finance_book": cstr(filters.get("finance_book")),
|
||||
dimension_field: ["in", list(set(dimension_list))],
|
||||
"posting_date": ["between", [filters.get("from_date"), filters.get("to_date")]],
|
||||
"is_cancelled": 0,
|
||||
}
|
||||
if account:
|
||||
gl_filters["account"] = ["in", account]
|
||||
|
||||
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
|
||||
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",
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
@@ -178,14 +176,6 @@ 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,6 +71,7 @@ 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 = {}, {}
|
||||
@@ -93,6 +94,7 @@ 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,
|
||||
@@ -112,7 +114,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, total_asset, net_sales, cogs, direct_expense)
|
||||
add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense)
|
||||
|
||||
return data
|
||||
|
||||
@@ -193,7 +195,7 @@ def add_solvency_ratios(
|
||||
data.append(return_on_equity_ratio)
|
||||
|
||||
|
||||
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
|
||||
def add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": _("Turnover Ratios")})
|
||||
|
||||
@@ -208,7 +210,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
|
||||
)
|
||||
|
||||
ratio_data = [
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, fixed_asset],
|
||||
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
|
||||
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
|
||||
[_("Inventory Turnover Ratio"), cogs, avg_stock],
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# 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,11 +179,10 @@ def get_data(
|
||||
company_currency = get_appropriate_currency(company, filters)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
for root in frappe.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
for root in frappe.get_all(
|
||||
"Account",
|
||||
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
|
||||
fields=["lft", "rgt"],
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
company,
|
||||
@@ -373,13 +372,23 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
|
||||
|
||||
|
||||
def get_accounts(company, root_type):
|
||||
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,
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
@@ -529,7 +538,11 @@ 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"),
|
||||
gl_entry.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"),
|
||||
)
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
@@ -547,15 +560,29 @@ def get_accounting_entries(
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
if doctype == "GL Entry":
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
# 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.where(gl_entry.is_cancelled == 0)
|
||||
query = query.where(gl_entry.posting_date <= to_date)
|
||||
query = query.force_index("posting_date_company_index")
|
||||
# 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")
|
||||
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
query = query.where(gl_entry.is_opening == "No")
|
||||
else:
|
||||
query = query.select(gl_entry.closing_date.as_("posting_date"))
|
||||
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.where(gl_entry.period_closing_voucher == period_closing_voucher)
|
||||
|
||||
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
|
||||
|
||||
@@ -12,7 +12,13 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestGeneralAndPaymentLedger(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
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.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.db.sql("""select name, is_group from tabAccount""", as_dict=1):
|
||||
for acc in frappe.get_all("Account", fields=["name", "is_group"]):
|
||||
account_details.setdefault(acc.name, acc)
|
||||
|
||||
if filters.get("party"):
|
||||
@@ -650,10 +650,8 @@ def get_result_as_list(data, filters):
|
||||
|
||||
def get_supplier_invoice_details():
|
||||
inv_details = {}
|
||||
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,
|
||||
for d in frappe.get_all(
|
||||
"Purchase Invoice", filters={"docstatus": 1, "bill_no": ["is", "set"]}, fields=["name", "bill_no"]
|
||||
):
|
||||
inv_details[d.name] = d.bill_no
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class TestGeneralLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.clear_old_entries()
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
|
||||
@@ -713,20 +713,25 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
def get_returned_invoice_items(self):
|
||||
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,
|
||||
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)
|
||||
)
|
||||
|
||||
self.returned_invoices = frappe._dict()
|
||||
@@ -1241,7 +1246,4 @@ class GrossProfitGenerator:
|
||||
).setdefault(d.parent_item, []).append(d)
|
||||
|
||||
def load_non_stock_items(self):
|
||||
self.non_stock_items = frappe.db.sql_list(
|
||||
"""select name from tabItem
|
||||
where is_stock_item=0"""
|
||||
)
|
||||
self.non_stock_items = frappe.get_all("Item", filters={"is_stock_item": 0}, pluck="name")
|
||||
|
||||
@@ -14,73 +14,17 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestGrossProfit(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
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
|
||||
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"
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
@@ -214,7 +158,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty": 1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 150.0,
|
||||
@@ -243,7 +187,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty": 1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 100.0,
|
||||
@@ -275,7 +219,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"item_code": self.item2,
|
||||
"s_warehouse": "",
|
||||
"t_warehouse": self.finished_warehouse,
|
||||
"qty": 1,
|
||||
"qty": 2,
|
||||
"basic_rate": 100,
|
||||
"conversion_factor": item.conversion_factor or 1.0,
|
||||
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
||||
@@ -375,7 +319,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty": 4.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 125.0,
|
||||
@@ -416,10 +360,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 100,
|
||||
"valuation_rate": 0.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 100.0,
|
||||
"selling_amount": 0.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": 0.0,
|
||||
@@ -439,7 +383,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"""
|
||||
# Make Cr Note
|
||||
sinv = self.create_sales_invoice(
|
||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
qty=-1, rate=200, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
sinv.is_return = 1
|
||||
sinv.items[0].allow_zero_valuation_rate = 1
|
||||
@@ -462,14 +406,14 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty": -1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"avg._selling_rate": 200.0,
|
||||
"valuation_rate": 100.0,
|
||||
"selling_amount": -200.0,
|
||||
"buying_amount": -100.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": -100.0,
|
||||
"gross_profit_%": -50.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}
|
||||
@@ -555,7 +499,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty": 4.0,
|
||||
"avg._selling_rate": 800.0,
|
||||
"valuation_rate": 700.0,
|
||||
@@ -618,7 +562,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
def test_gross_profit_groupby_invoices(self):
|
||||
create_sales_invoice(
|
||||
qty=1,
|
||||
rate=100,
|
||||
rate=200,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
@@ -640,10 +584,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.selling_amount, 200.0)
|
||||
self.assertEqual(total.buying_amount, 100.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 50.0)
|
||||
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
@@ -652,7 +596,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=100, do_not_save=True, do_not_submit=True)
|
||||
sinv = self.create_sales_invoice(qty=1, rate=200, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = sales_inv_date
|
||||
sinv.save().submit()
|
||||
@@ -671,10 +615,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.selling_amount, 200.0)
|
||||
self.assertEqual(total.buying_amount, 100.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 50.0)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update({"to_date": return_inv_date})
|
||||
@@ -692,10 +636,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, -100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.selling_amount, -200.0)
|
||||
self.assertEqual(total.buying_amount, -100.0)
|
||||
self.assertEqual(total.gross_profit, -100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), -100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), -50.0)
|
||||
|
||||
def test_sales_person_wise_gross_profit(self):
|
||||
sales_person = make_sales_person("_Test Sales Person")
|
||||
@@ -726,10 +670,10 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total[5], 1000.0)
|
||||
self.assertEqual(total[6], 0.0)
|
||||
self.assertEqual(total[7], 1000.0)
|
||||
self.assertEqual(total[8], 100.0)
|
||||
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 %
|
||||
|
||||
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 import CustomFunction
|
||||
from frappe.query_builder.functions import CurDate, DateDiff
|
||||
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_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
|
||||
current_date = CustomFunction("CURRENT_DATE", [])
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
sales_data = (
|
||||
frappe.qb.from_(parent)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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.db.sql("select name, stock_received_but_not_billed from tabCompany"))
|
||||
return dict(frappe.get_all("Company", fields=["name", "stock_received_but_not_billed"], as_list=True))
|
||||
|
||||
|
||||
def get_purchase_receipts_against_purchase_order(item_list):
|
||||
@@ -384,16 +384,11 @@ 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.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,
|
||||
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",
|
||||
)
|
||||
|
||||
for pr in purchase_receipts:
|
||||
|
||||
@@ -9,9 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
self.company = "_Test Company"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
pi = make_purchase_invoice(
|
||||
|
||||
@@ -9,9 +9,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
|
||||
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -9,42 +9,12 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestPaymentLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
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
|
||||
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"
|
||||
|
||||
def test_unpaid_invoice_outstanding(self):
|
||||
sinv = create_sales_invoice(
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
|
||||
@@ -46,40 +48,47 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_pos_entries(filters, group_by_field):
|
||||
conditions = get_conditions(filters)
|
||||
order_by = "p.posting_date"
|
||||
select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", ""
|
||||
if group_by_field == "mode_of_payment":
|
||||
select_mop_field = (
|
||||
", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount"
|
||||
p = frappe.qb.DocType("POS Invoice")
|
||||
query = (
|
||||
frappe.qb.from_(p)
|
||||
.select(
|
||||
p.posting_date,
|
||||
p.name.as_("pos_invoice"),
|
||||
p.pos_profile,
|
||||
p.company,
|
||||
p.owner,
|
||||
p.customer,
|
||||
p.is_return,
|
||||
p.base_grand_total.as_("grand_total"),
|
||||
)
|
||||
from_sales_invoice_payment = ", `tabSales Invoice Payment` sip"
|
||||
group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount - IF(sip.type='Cash', p.change_amount, 0), 0) != 0 AND"
|
||||
order_by += ", sip.mode_of_payment"
|
||||
|
||||
elif group_by_field:
|
||||
order_by += f", p.{group_by_field}"
|
||||
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
p.posting_date, p.name as pos_invoice, p.pos_profile, p.company,
|
||||
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
|
||||
FROM
|
||||
`tabPOS Invoice` p {from_sales_invoice_payment}
|
||||
WHERE
|
||||
p.docstatus = 1 and
|
||||
{group_by_mop_condition}
|
||||
{conditions}
|
||||
ORDER BY
|
||||
{order_by}
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
.where(p.docstatus == 1)
|
||||
)
|
||||
|
||||
for condition in get_conditions(filters, p):
|
||||
query = query.where(condition)
|
||||
|
||||
if group_by_field == "mode_of_payment":
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
paid_amount = sip.base_amount - Case().when(sip.type == "Cash", p.change_amount).else_(0)
|
||||
query = (
|
||||
query.inner_join(sip)
|
||||
.on(sip.parent == p.name)
|
||||
.select(sip.mode_of_payment, paid_amount.as_("paid_amount"))
|
||||
.where(IfNull(paid_amount, 0) != 0)
|
||||
.orderby(p.posting_date)
|
||||
.orderby(sip.mode_of_payment)
|
||||
)
|
||||
elif group_by_field:
|
||||
query = (
|
||||
query.select((p.base_paid_amount - p.change_amount).as_("paid_amount"))
|
||||
.orderby(p.posting_date)
|
||||
.orderby(p[group_by_field])
|
||||
)
|
||||
else:
|
||||
query = query.orderby(p.posting_date)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def concat_mode_of_payments(pos_entries):
|
||||
mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries))
|
||||
@@ -127,27 +136,34 @@ def validate_filters(filters):
|
||||
frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method"))
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s"
|
||||
def get_conditions(filters, p):
|
||||
conditions = [
|
||||
p.company == filters.get("company"),
|
||||
p.posting_date >= filters.get("from_date"),
|
||||
p.posting_date <= filters.get("to_date"),
|
||||
]
|
||||
|
||||
if filters.get("pos_profile"):
|
||||
conditions += " AND pos_profile = %(pos_profile)s"
|
||||
conditions.append(p.pos_profile == filters.get("pos_profile"))
|
||||
|
||||
if filters.get("owner"):
|
||||
conditions += " AND owner = %(owner)s"
|
||||
conditions.append(p.owner == filters.get("owner"))
|
||||
|
||||
if filters.get("customer"):
|
||||
conditions += " AND customer = %(customer)s"
|
||||
conditions.append(p.customer == filters.get("customer"))
|
||||
|
||||
if filters.get("is_return"):
|
||||
conditions += " AND is_return = %(is_return)s"
|
||||
conditions.append(p.is_return == filters.get("is_return"))
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """
|
||||
AND EXISTS(
|
||||
SELECT name FROM `tabSales Invoice Payment` sip
|
||||
WHERE parent=p.name AND ifnull(sip.mode_of_payment, '') = %(mode_of_payment)s
|
||||
)"""
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
conditions.append(
|
||||
p.name.isin(
|
||||
frappe.qb.from_(sip)
|
||||
.select(sip.parent)
|
||||
.where(IfNull(sip.mode_of_payment, "") == filters.get("mode_of_payment"))
|
||||
)
|
||||
)
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
20
erpnext/accounts/report/pos_register/test_pos_register.py
Normal file
20
erpnext/accounts/report/pos_register/test_pos_register.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.report.pos_register.pos_register import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPOSRegister(ERPNextTestSuite):
|
||||
def test_report_executes(self):
|
||||
# Smoke-guards the raw-SQL -> query-builder port: the report's POS Invoice query must
|
||||
# compile and run on both MariaDB and postgres (it returns columns + a row list either way).
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
columns, data = execute(
|
||||
frappe._dict({"company": company, "from_date": add_days(today(), -365), "to_date": today()})
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
self.assertIsInstance(data, list)
|
||||
@@ -14,9 +14,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
|
||||
def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika.terms import Bracket, LiteralValue, Order
|
||||
|
||||
@@ -307,14 +309,17 @@ def get_account_columns(invoice_list, include_payments):
|
||||
unrealized_profit_loss_account_columns = []
|
||||
|
||||
if invoice_list:
|
||||
expense_accounts = frappe.db.sql_list(
|
||||
"""select distinct expense_account
|
||||
from `tabPurchase Invoice Item` where docstatus = 1
|
||||
and (expense_account is not null and expense_account != '')
|
||||
and parenttype='Purchase Invoice'
|
||||
and parent in (%s) order by expense_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple([inv.name for inv in invoice_list]),
|
||||
expense_accounts = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"expense_account": ["is", "set"],
|
||||
"parenttype": "Purchase Invoice",
|
||||
"parent": ["in", [inv.name for inv in invoice_list]],
|
||||
},
|
||||
pluck="expense_account",
|
||||
distinct=True,
|
||||
order_by="expense_account",
|
||||
)
|
||||
|
||||
purchase_taxes_query = get_taxes_query(invoice_list, "Purchase Taxes and Charges", "Purchase Invoice")
|
||||
@@ -326,13 +331,16 @@ def get_account_columns(invoice_list, include_payments):
|
||||
advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
|
||||
tax_accounts = set(tax_accounts + advance_tax_accounts)
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list(
|
||||
"""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
unrealized_profit_loss_accounts = frappe.get_all(
|
||||
"Purchase Invoice",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"name": ["in", [inv.name for inv in invoice_list]],
|
||||
"unrealized_profit_loss_account": ["is", "set"],
|
||||
},
|
||||
pluck="unrealized_profit_loss_account",
|
||||
distinct=True,
|
||||
order_by="unrealized_profit_loss_account",
|
||||
)
|
||||
|
||||
for account in expense_accounts:
|
||||
@@ -454,16 +462,11 @@ def get_payments(filters):
|
||||
|
||||
|
||||
def get_invoice_expense_map(invoice_list):
|
||||
expense_details = frappe.db.sql(
|
||||
"""
|
||||
select parent, expense_account, sum(base_net_amount) as amount
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
group by parent, expense_account
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
expense_details = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Purchase Invoice"},
|
||||
fields=["parent", "expense_account", {"SUM": "base_net_amount", "as": "amount"}],
|
||||
group_by="parent, expense_account",
|
||||
)
|
||||
|
||||
invoice_expense_map = {}
|
||||
@@ -475,13 +478,16 @@ def get_invoice_expense_map(invoice_list):
|
||||
|
||||
|
||||
def get_internal_invoice_map(invoice_list):
|
||||
unrealized_amount_details = frappe.db.sql(
|
||||
"""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
|
||||
and is_internal_supplier = 1 and company = represents_company"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
unrealized_amount_details = (
|
||||
frappe.qb.from_(pi)
|
||||
.select(pi.name, pi.unrealized_profit_loss_account, pi.base_net_total.as_("amount"))
|
||||
.where(
|
||||
pi.name.isin([inv.name for inv in invoice_list])
|
||||
& (pi.is_internal_supplier == 1)
|
||||
& (pi.company == pi.represents_company)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
internal_invoice_map = {}
|
||||
@@ -493,18 +499,23 @@ def get_internal_invoice_map(invoice_list):
|
||||
|
||||
|
||||
def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, include_payments=False):
|
||||
tax_details = frappe.db.sql(
|
||||
"""
|
||||
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)
|
||||
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
|
||||
from `tabPurchase Taxes and Charges`
|
||||
where parent in (%s) and category in ('Total', 'Valuation and Total')
|
||||
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
|
||||
group by parent, account_head, add_deduct_tax
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
ptc = frappe.qb.DocType("Purchase Taxes and Charges")
|
||||
tax_amount = (
|
||||
Case()
|
||||
.when(ptc.add_deduct_tax == "Add", Sum(ptc.base_tax_amount_after_discount_amount))
|
||||
.else_(Sum(ptc.base_tax_amount_after_discount_amount) * -1)
|
||||
)
|
||||
tax_details = (
|
||||
frappe.qb.from_(ptc)
|
||||
.select(ptc.parent, ptc.account_head, tax_amount.as_("tax_amount"))
|
||||
.where(
|
||||
ptc.parent.isin([inv.name for inv in invoice_list])
|
||||
& ptc.category.isin(["Total", "Valuation and Total"])
|
||||
& (ptc.base_tax_amount_after_discount_amount != 0)
|
||||
& (ptc.parenttype == "Purchase Invoice")
|
||||
)
|
||||
.groupby(ptc.parent, ptc.account_head, ptc.add_deduct_tax)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
if include_payments:
|
||||
@@ -525,15 +536,10 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
|
||||
|
||||
|
||||
def get_invoice_po_pr_map(invoice_list):
|
||||
pi_items = frappe.db.sql(
|
||||
"""
|
||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
pi_items = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Purchase Invoice"},
|
||||
fields=["parent", "purchase_order", "purchase_receipt", "po_detail", "project"],
|
||||
)
|
||||
|
||||
invoice_po_pr_map = {}
|
||||
@@ -547,10 +553,11 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
if d.purchase_receipt:
|
||||
pr_list = [d.purchase_receipt]
|
||||
elif d.po_detail:
|
||||
pr_list = frappe.db.sql_list(
|
||||
"""select distinct parent from `tabPurchase Receipt Item`
|
||||
where docstatus=1 and purchase_order_item=%s""",
|
||||
d.po_detail,
|
||||
pr_list = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"docstatus": 1, "purchase_order_item": d.po_detail},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
if pr_list:
|
||||
@@ -565,12 +572,8 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
def get_account_details(invoice_list):
|
||||
account_map = {}
|
||||
accounts = list(set([inv.credit_to for inv in invoice_list]))
|
||||
for acc in frappe.db.sql(
|
||||
"""select name, parent_account from tabAccount
|
||||
where name in (%s)"""
|
||||
% ", ".join(["%s"] * len(accounts)),
|
||||
tuple(accounts),
|
||||
as_dict=1,
|
||||
for acc in frappe.get_all(
|
||||
"Account", filters={"name": ["in", accounts]}, fields=["name", "parent_account"]
|
||||
):
|
||||
account_map[acc.name] = acc.parent_account
|
||||
|
||||
|
||||
@@ -346,12 +346,15 @@ def get_account_columns(invoice_list, include_payments):
|
||||
unrealized_profit_loss_account_columns = []
|
||||
|
||||
if invoice_list:
|
||||
income_accounts = frappe.db.sql_list(
|
||||
"""select distinct income_account
|
||||
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
|
||||
order by income_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
# frappe drops ORDER BY for distinct queries on postgres (db_query), so sort in python to keep
|
||||
# the generated account-column order deterministic and identical on both backends.
|
||||
income_accounts = sorted(
|
||||
frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"docstatus": 1, "parent": ["in", [inv.name for inv in invoice_list]]},
|
||||
pluck="income_account",
|
||||
distinct=True,
|
||||
)
|
||||
)
|
||||
|
||||
sales_taxes_query = get_taxes_query(invoice_list, "Sales Taxes and Charges", "Sales Invoice")
|
||||
@@ -363,14 +366,18 @@ def get_account_columns(invoice_list, include_payments):
|
||||
advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
|
||||
tax_accounts = set(tax_accounts + advance_tax_accounts)
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list(
|
||||
"""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabSales Invoice` where docstatus = 1 and name in (%s)
|
||||
and is_internal_customer = 1
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
unrealized_profit_loss_accounts = sorted(
|
||||
frappe.get_all(
|
||||
"Sales Invoice",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"name": ["in", [inv.name for inv in invoice_list]],
|
||||
"is_internal_customer": 1,
|
||||
"unrealized_profit_loss_account": ["is", "set"],
|
||||
},
|
||||
pluck="unrealized_profit_loss_account",
|
||||
distinct=True,
|
||||
)
|
||||
)
|
||||
|
||||
for account in income_accounts:
|
||||
@@ -494,12 +501,11 @@ def get_payments(filters):
|
||||
|
||||
|
||||
def get_invoice_income_map(invoice_list):
|
||||
income_details = frappe.db.sql(
|
||||
"""select parent, income_account, sum(base_net_amount) as amount
|
||||
from `tabSales Invoice Item` where parent in (%s) group by parent, income_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
income_details = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
|
||||
fields=["parent", "income_account", {"SUM": "base_net_amount", "as": "amount"}],
|
||||
group_by="parent, income_account",
|
||||
)
|
||||
|
||||
invoice_income_map = {}
|
||||
@@ -511,13 +517,16 @@ def get_invoice_income_map(invoice_list):
|
||||
|
||||
|
||||
def get_internal_invoice_map(invoice_list):
|
||||
unrealized_amount_details = frappe.db.sql(
|
||||
"""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabSales Invoice` where name in (%s)
|
||||
and is_internal_customer = 1 and company = represents_company"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
unrealized_amount_details = (
|
||||
frappe.qb.from_(si)
|
||||
.select(si.name, si.unrealized_profit_loss_account, si.base_net_total.as_("amount"))
|
||||
.where(
|
||||
si.name.isin([inv.name for inv in invoice_list])
|
||||
& (si.is_internal_customer == 1)
|
||||
& (si.company == si.represents_company)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
internal_invoice_map = {}
|
||||
@@ -529,14 +538,15 @@ def get_internal_invoice_map(invoice_list):
|
||||
|
||||
|
||||
def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, include_payments=False):
|
||||
tax_details = frappe.db.sql(
|
||||
"""select parent, account_head,
|
||||
sum(base_tax_amount_after_discount_amount) as tax_amount
|
||||
from `tabSales Taxes and Charges` where parent in (%s) and parenttype = 'Sales Invoice'
|
||||
group by parent, account_head"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
tax_details = frappe.get_all(
|
||||
"Sales Taxes and Charges",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Sales Invoice"},
|
||||
fields=[
|
||||
"parent",
|
||||
"account_head",
|
||||
{"SUM": "base_tax_amount_after_discount_amount", "as": "tax_amount"},
|
||||
],
|
||||
group_by="parent, account_head",
|
||||
)
|
||||
|
||||
if include_payments:
|
||||
@@ -557,13 +567,11 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, inclu
|
||||
|
||||
|
||||
def get_invoice_so_dn_map(invoice_list):
|
||||
si_items = frappe.db.sql(
|
||||
"""select parent, sales_order, delivery_note, so_detail
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (sales_order != '' or delivery_note != '')"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
si_items = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
|
||||
or_filters=[["sales_order", "!=", ""], ["delivery_note", "!=", ""]],
|
||||
fields=["parent", "sales_order", "delivery_note", "so_detail"],
|
||||
)
|
||||
|
||||
invoice_so_dn_map = {}
|
||||
@@ -577,10 +585,11 @@ def get_invoice_so_dn_map(invoice_list):
|
||||
if d.delivery_note:
|
||||
delivery_note_list = [d.delivery_note]
|
||||
elif d.sales_order:
|
||||
delivery_note_list = frappe.db.sql_list(
|
||||
"""select distinct parent from `tabDelivery Note Item`
|
||||
where docstatus=1 and so_detail=%s""",
|
||||
d.so_detail,
|
||||
delivery_note_list = frappe.get_all(
|
||||
"Delivery Note Item",
|
||||
filters={"docstatus": 1, "so_detail": d.so_detail},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
if delivery_note_list:
|
||||
@@ -592,13 +601,11 @@ def get_invoice_so_dn_map(invoice_list):
|
||||
|
||||
|
||||
def get_invoice_cc_wh_map(invoice_list):
|
||||
si_items = frappe.db.sql(
|
||||
"""select parent, cost_center, warehouse
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (cost_center != '' or warehouse != '')"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
si_items = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
|
||||
or_filters=[["cost_center", "!=", ""], ["warehouse", "!=", ""]],
|
||||
fields=["parent", "cost_center", "warehouse"],
|
||||
)
|
||||
|
||||
invoice_cc_wh_map = {}
|
||||
@@ -619,12 +626,11 @@ def get_invoice_cc_wh_map(invoice_list):
|
||||
def get_mode_of_payments(invoice_list):
|
||||
mode_of_payments = {}
|
||||
if invoice_list:
|
||||
inv_mop = frappe.db.sql(
|
||||
"""select parent, mode_of_payment
|
||||
from `tabSales Invoice Payment` where parent in (%s) group by parent, mode_of_payment"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(invoice_list),
|
||||
as_dict=1,
|
||||
inv_mop = frappe.get_all(
|
||||
"Sales Invoice Payment",
|
||||
filters={"parent": ["in", list(invoice_list)]},
|
||||
fields=["parent", "mode_of_payment"],
|
||||
group_by="parent, mode_of_payment",
|
||||
)
|
||||
|
||||
for d in inv_mop:
|
||||
|
||||
@@ -10,9 +10,13 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.create_child_cost_center()
|
||||
|
||||
def create_child_cost_center(self):
|
||||
|
||||
@@ -62,15 +62,10 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_all_transfers(date, shareholder):
|
||||
condition = " "
|
||||
# if company:
|
||||
# condition = 'AND company = %(company)s '
|
||||
return frappe.db.sql(
|
||||
f"""SELECT * FROM `tabShare Transfer`
|
||||
WHERE ((DATE(date) <= %(date)s AND from_shareholder = %(shareholder)s {condition})
|
||||
OR (DATE(date) <= %(date)s AND to_shareholder = %(shareholder)s {condition}))
|
||||
AND docstatus = 1
|
||||
ORDER BY date""",
|
||||
{"date": date, "shareholder": shareholder},
|
||||
as_dict=1,
|
||||
return frappe.get_all(
|
||||
"Share Transfer",
|
||||
filters={"date": ["<=", date], "docstatus": 1},
|
||||
or_filters=[["from_shareholder", "=", shareholder], ["to_shareholder", "=", shareholder]],
|
||||
fields=["*"],
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
@@ -9,10 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -20,8 +20,7 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
create_records()
|
||||
|
||||
def test_tax_withholding_for_customers(self):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Max, Sum
|
||||
from frappe.utils import add_days, cstr, flt, formatdate, getdate
|
||||
|
||||
import erpnext
|
||||
@@ -82,12 +82,21 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
as_dict=True,
|
||||
accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": filters.company},
|
||||
fields=[
|
||||
"name",
|
||||
"account_number",
|
||||
"parent_account",
|
||||
"account_name",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"is_group",
|
||||
"lft",
|
||||
"rgt",
|
||||
],
|
||||
order_by="lft",
|
||||
)
|
||||
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
|
||||
|
||||
@@ -240,7 +249,8 @@ def get_opening_balance(
|
||||
frappe.qb.from_(closing_balance)
|
||||
.select(
|
||||
closing_balance.account,
|
||||
closing_balance.account_currency,
|
||||
# account_currency is constant per grouped account -> Max() keeps the GROUP BY postgres-valid
|
||||
Max(closing_balance.account_currency).as_("account_currency"),
|
||||
Sum(closing_balance.debit).as_("debit"),
|
||||
Sum(closing_balance.credit).as_("credit"),
|
||||
Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Max, Sum
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -43,7 +43,13 @@ def get_data(filters):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit"))
|
||||
# voucher_type is constant per grouped voucher_no -> Max() keeps the GROUP BY valid on postgres
|
||||
.select(
|
||||
Max(gle.voucher_type).as_("voucher_type"),
|
||||
gle.voucher_no,
|
||||
Sum(gle.debit).as_("debit"),
|
||||
Sum(gle.credit).as_("credit"),
|
||||
)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.groupby(gle.voucher_no)
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.query_builder.functions import Abs, Max, Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
@@ -150,7 +150,7 @@ def calculate_total_advance_from_ledger(doc) -> list:
|
||||
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
||||
return (
|
||||
frappe.qb.from_(adv)
|
||||
.select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency"))
|
||||
.select(Abs(Sum(adv.amount)).as_("amount"), Max(adv.currency).as_("account_currency"))
|
||||
.where(adv.company == doc.company)
|
||||
.where(adv.delinked == 0)
|
||||
.where(adv.against_voucher_type == doc.doctype)
|
||||
|
||||
@@ -141,7 +141,7 @@ def make_exchange_gain_loss_journal(
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (je.name.isin(parents))
|
||||
& (je.voucher_type == "Exchange Gain or Loss")
|
||||
& (je.voucher_type == "Exchange Gain Or Loss")
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
@@ -200,7 +200,6 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_on(
|
||||
account: str | None = None,
|
||||
date: DateTimeLikeObject | None = None,
|
||||
|
||||
@@ -96,7 +96,9 @@ def get_depreciable_assets_data(date):
|
||||
.where(a.status.isin(["Submitted", "Partially Depreciated"]))
|
||||
.where(ds.journal_entry.isnull())
|
||||
.where(ds.schedule_date <= date)
|
||||
.groupby(ads.name)
|
||||
# a.name/a.creation are constant per ads.name; include them so postgres accepts the
|
||||
# SELECT and ORDER BY (one row per Asset Depreciation Schedule either way)
|
||||
.groupby(ads.name, a.name, a.creation)
|
||||
.orderby(a.creation, order=Order.desc)
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import DateDiff, Sum
|
||||
from frappe.utils import getdate
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def get_item_workdays(scorecard):
|
||||
frappe.qb.from_(PO_Item)
|
||||
.join(PO)
|
||||
.on(PO_Item.parent == PO.name)
|
||||
.select(Sum(frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty)))
|
||||
.select(Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty)))
|
||||
.where(PO.supplier == scorecard.supplier)
|
||||
.where(PO_Item.received_qty < PO_Item.qty)
|
||||
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date]) # Équivalent du BETWEEN
|
||||
@@ -153,7 +153,7 @@ def get_total_days_late(scorecard):
|
||||
.on(PR_Item.purchase_order_item == PO_Item.name)
|
||||
.join(PO)
|
||||
.on(PO_Item.parent == PO.name)
|
||||
.select(Sum(frappe.qb.fn.DATEDIFF(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty))
|
||||
.select(Sum(DateDiff(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty))
|
||||
.where(PO.supplier == scorecard.supplier)
|
||||
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date])
|
||||
.where(PO_Item.schedule_date < PR.posting_date)
|
||||
@@ -170,10 +170,7 @@ def get_total_days_late(scorecard):
|
||||
.join(PO)
|
||||
.on(PO_Item.parent == PO.name)
|
||||
.select(
|
||||
Sum(
|
||||
frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date)
|
||||
* (PO_Item.qty - PO_Item.received_qty)
|
||||
)
|
||||
Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty - PO_Item.received_qty))
|
||||
)
|
||||
.where(PO.supplier == scorecard.supplier)
|
||||
.where(PO_Item.received_qty < PO_Item.qty)
|
||||
@@ -530,7 +527,7 @@ def get_rfq_response_days(scorecard):
|
||||
.on(sq_item.request_for_quotation_item == rfq_item.name)
|
||||
.join(sq)
|
||||
.on(sq_item.parent == sq.name)
|
||||
.select(frappe.qb.fn.Sum(frappe.qb.fn.Datediff(sq.transaction_date, rfq.transaction_date)))
|
||||
.select(frappe.qb.fn.Sum(DateDiff(sq.transaction_date, rfq.transaction_date)))
|
||||
.where(rfq_sup.supplier == scorecard.supplier)
|
||||
.where(sq.supplier == scorecard.supplier)
|
||||
.where(rfq.transaction_date[scorecard.start_date : scorecard.end_date])
|
||||
|
||||
@@ -6,7 +6,7 @@ import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.query_builder.functions import Coalesce, Max, Sum
|
||||
from frappe.utils import cint, date_diff, flt, getdate
|
||||
|
||||
|
||||
@@ -44,13 +44,15 @@ def get_data(filters):
|
||||
.on(mr_item.parent == mr.name)
|
||||
.select(
|
||||
mr.name.as_("material_request"),
|
||||
mr.transaction_date.as_("date"),
|
||||
mr_item.schedule_date.as_("required_date"),
|
||||
# non-grouped columns are constant per grouped mr.name / item_code -> Max() keeps the
|
||||
# GROUP BY valid on postgres while returning the same value MySQL picked.
|
||||
Max(mr.transaction_date).as_("date"),
|
||||
Max(mr_item.schedule_date).as_("required_date"),
|
||||
mr_item.item_code.as_("item_code"),
|
||||
Sum(Coalesce(mr_item.qty, 0)).as_("qty"),
|
||||
Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"),
|
||||
Coalesce(mr_item.uom, "").as_("uom"),
|
||||
Coalesce(mr_item.stock_uom, "").as_("stock_uom"),
|
||||
Max(Coalesce(mr_item.uom, "")).as_("uom"),
|
||||
Max(Coalesce(mr_item.stock_uom, "")).as_("stock_uom"),
|
||||
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
|
||||
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
||||
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_(
|
||||
@@ -58,9 +60,9 @@ def get_data(filters):
|
||||
),
|
||||
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
||||
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))).as_("qty_to_order"),
|
||||
mr_item.item_name,
|
||||
mr_item.description,
|
||||
mr.company,
|
||||
Max(mr_item.item_name).as_("item_name"),
|
||||
Max(mr_item.description).as_("description"),
|
||||
Max(mr.company).as_("company"),
|
||||
)
|
||||
.where(
|
||||
(mr.material_request_type == "Purchase")
|
||||
@@ -72,7 +74,7 @@ def get_data(filters):
|
||||
|
||||
query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
|
||||
|
||||
query = query.groupby(mr.name, mr_item.item_code).orderby(mr.transaction_date, mr.schedule_date)
|
||||
query = query.groupby(mr.name, mr_item.item_code).orderby(Max(mr.transaction_date), Max(mr.schedule_date))
|
||||
data = query.run(as_dict=True)
|
||||
return data
|
||||
|
||||
|
||||
@@ -115,6 +115,26 @@ class AccountsController(TransactionBase):
|
||||
|
||||
PaymentScheduleService(self).set_payment_schedule()
|
||||
|
||||
def before_insert(self):
|
||||
self.clear_clearance_date_on_amend()
|
||||
|
||||
def clear_clearance_date_on_amend(self):
|
||||
"""Drop the bank reconciliation clearance date copied over while amending.
|
||||
|
||||
The framework copies `no_copy` fields when amending, so a reconciled
|
||||
voucher would carry a stale clearance date into its amendment even though
|
||||
the linked bank transaction gets unreconciled on cancellation.
|
||||
"""
|
||||
if not self.get("amended_from"):
|
||||
return
|
||||
|
||||
if self.meta.has_field("clearance_date"):
|
||||
self.clearance_date = None
|
||||
|
||||
for payment in self.get("payments") or []:
|
||||
if payment.meta.has_field("clearance_date"):
|
||||
payment.clearance_date = None
|
||||
|
||||
def on_update(self):
|
||||
from erpnext.controllers.taxes_and_totals import process_item_wise_tax_details
|
||||
|
||||
@@ -1563,13 +1583,13 @@ def update_invoice_status():
|
||||
|
||||
total = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(invoice.disable_rounded_total, invoice.grand_total)
|
||||
.when(invoice.disable_rounded_total == 1, invoice.grand_total)
|
||||
.else_(invoice.rounded_total)
|
||||
)
|
||||
|
||||
base_total = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(invoice.disable_rounded_total, invoice.base_grand_total)
|
||||
.when(invoice.disable_rounded_total == 1, invoice.base_grand_total)
|
||||
.else_(invoice.base_rounded_total)
|
||||
)
|
||||
|
||||
@@ -1582,7 +1602,7 @@ def update_invoice_status():
|
||||
& (invoice.outstanding_amount > 0)
|
||||
& (invoice.status.like("Unpaid%") | invoice.status.like("Partly Paid%"))
|
||||
& (
|
||||
((invoice.is_pos & invoice.due_date < today) | is_overdue)
|
||||
(((invoice.is_pos == 1) & (invoice.due_date < today)) | is_overdue)
|
||||
if doctype == "Sales Invoice"
|
||||
else is_overdue
|
||||
)
|
||||
|
||||
@@ -416,20 +416,21 @@ class BuyingController(SubcontractingController):
|
||||
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
|
||||
last_item_idx = 1
|
||||
for d in self.get("items"):
|
||||
if d.item_code and d.item_code in stock_and_asset_items:
|
||||
if d.item_code:
|
||||
stock_and_asset_items_qty += flt(d.qty)
|
||||
stock_and_asset_items_amount += flt(d.base_net_amount)
|
||||
|
||||
last_item_idx = d.idx
|
||||
|
||||
tax_accounts, total_valuation_amount, total_actual_tax_amount = self.get_tax_details()
|
||||
remaining_amount = total_actual_tax_amount
|
||||
|
||||
for i, item in enumerate(self.get("items")):
|
||||
if item.item_code and (item.qty or item.get("rejected_qty")):
|
||||
item_tax_amount, actual_tax_amount = 0.0, 0.0
|
||||
if i == (last_item_idx - 1):
|
||||
item_tax_amount = total_valuation_amount
|
||||
actual_tax_amount = total_actual_tax_amount
|
||||
actual_tax_amount = remaining_amount
|
||||
else:
|
||||
# calculate item tax amount
|
||||
item_tax_amount = self.get_item_tax_amount(item, tax_accounts)
|
||||
@@ -442,7 +443,8 @@ class BuyingController(SubcontractingController):
|
||||
stock_and_asset_items_amount,
|
||||
stock_and_asset_items_qty,
|
||||
)
|
||||
total_actual_tax_amount -= actual_tax_amount
|
||||
|
||||
remaining_amount -= actual_tax_amount
|
||||
|
||||
# This code is required here to calculate the correct valuation for stock items
|
||||
if item.item_code not in stock_and_asset_items:
|
||||
|
||||
@@ -632,7 +632,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
|
||||
|
||||
inspection_fieldname = INSPECTION_FIELDNAME_MAP.get(doctype)
|
||||
if inspection_fieldname is None:
|
||||
return []
|
||||
return items if doctype == "Stock Entry" else []
|
||||
|
||||
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
|
||||
@@ -18,39 +18,9 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
prepare_data_for_internal_transfer,
|
||||
)
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = customer_name
|
||||
customer.customer_type = "Individual"
|
||||
|
||||
if currency:
|
||||
customer.default_currency = currency
|
||||
customer.save()
|
||||
return customer.name
|
||||
else:
|
||||
return customer_name
|
||||
|
||||
|
||||
def make_supplier(supplier_name, currency=None):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = supplier_name
|
||||
supplier.supplier_type = "Individual"
|
||||
supplier.supplier_group = "All Supplier Groups"
|
||||
|
||||
if currency:
|
||||
supplier.default_currency = currency
|
||||
supplier.save()
|
||||
return supplier.name
|
||||
else:
|
||||
return supplier_name
|
||||
|
||||
|
||||
class TestAccountsController(ERPNextTestSuite):
|
||||
"""
|
||||
Test Exchange Gain/Loss booking on various scenarios.
|
||||
@@ -67,79 +37,28 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.company = "_Test Company"
|
||||
self.company_abbr = "_TC"
|
||||
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.debit_usd = "_Test Receivable USD - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.creditors = "Creditors - _TC"
|
||||
self.creditors_usd = "_Test Payable USD - _TC"
|
||||
self.item = "_Test Item"
|
||||
self.customer = "_Test Customer USD"
|
||||
self.supplier = "_Test Supplier USD"
|
||||
self.create_account()
|
||||
self.create_item()
|
||||
self.create_parties()
|
||||
self.clear_old_entries()
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Company"
|
||||
self.company_abbr = abbr = "_TC"
|
||||
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.debit_usd = "Debtors USD - " + abbr
|
||||
self.cash = "Cash - " + abbr
|
||||
self.creditors = "Creditors - " + abbr
|
||||
|
||||
def create_item(self):
|
||||
item = create_item(
|
||||
item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_parties(self):
|
||||
self.create_customer()
|
||||
self.create_supplier()
|
||||
|
||||
def create_customer(self):
|
||||
self.customer = make_customer("_Test MC Customer USD", "USD")
|
||||
|
||||
def create_supplier(self):
|
||||
self.supplier = make_supplier("_Test MC Supplier USD", "USD")
|
||||
|
||||
def create_account(self):
|
||||
# Advance accounts are not in persistent test data — create them on demand.
|
||||
accounts = [
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "debtors_usd",
|
||||
"name": "Debtors USD",
|
||||
"account_type": "Receivable",
|
||||
"account_currency": "USD",
|
||||
"parent_account": "Accounts Receivable - " + self.company_abbr,
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "creditors_usd",
|
||||
"name": "Creditors USD",
|
||||
"account_type": "Payable",
|
||||
"account_currency": "USD",
|
||||
"parent_account": "Accounts Payable - " + self.company_abbr,
|
||||
}
|
||||
),
|
||||
# Advance accounts under Asset and Liability header
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "advance_received_usd",
|
||||
@@ -185,6 +104,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
company.save()
|
||||
|
||||
customer = frappe.get_doc("Customer", self.customer)
|
||||
customer.accounts = []
|
||||
customer.append(
|
||||
"accounts",
|
||||
{
|
||||
@@ -196,6 +116,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
customer.save()
|
||||
|
||||
supplier = frappe.get_doc("Supplier", self.supplier)
|
||||
supplier.accounts = []
|
||||
supplier.append(
|
||||
"accounts",
|
||||
{
|
||||
@@ -321,18 +242,6 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
pinv.submit()
|
||||
return pinv
|
||||
|
||||
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_payment_reconciliation(self):
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
@@ -955,7 +864,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
|
||||
# Create a Sales Invoice
|
||||
sinv = frappe.new_doc("Sales Invoice")
|
||||
sinv.customer = self.customer
|
||||
sinv.customer = "_Test Customer"
|
||||
sinv.company = self.company
|
||||
sinv.currency = "INR"
|
||||
sinv.taxes_and_charges = "_Test Tax - _TC"
|
||||
@@ -971,7 +880,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
def test_19_fetch_taxes_based_on_item_tax_template_template(self):
|
||||
# Create a Sales Invoice
|
||||
sinv = frappe.new_doc("Sales Invoice")
|
||||
sinv.customer = self.customer
|
||||
sinv.customer = "_Test Customer"
|
||||
sinv.company = self.company
|
||||
sinv.currency = "INR"
|
||||
sinv.append(
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.custom import GROUP_CONCAT
|
||||
from frappe.query_builder.functions import Date
|
||||
from frappe.query_builder.functions import Date, GroupConcat
|
||||
|
||||
Opportunity = DocType("Opportunity")
|
||||
OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail")
|
||||
@@ -72,6 +71,9 @@ def get_columns():
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
# db-aware GROUP_CONCAT (MariaDB) / STRING_AGG (postgres) with a ", " separator
|
||||
lost_reasons = GroupConcat(OpportunityLostReasonDetail.lost_reason, ", ", alias="lost_reason")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Opportunity)
|
||||
.left_join(OpportunityLostReasonDetail)
|
||||
@@ -85,7 +87,7 @@ def get_data(filters):
|
||||
Opportunity.party_name,
|
||||
Opportunity.customer_name,
|
||||
Opportunity.opportunity_type,
|
||||
GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "),
|
||||
lost_reasons,
|
||||
Opportunity.sales_stage,
|
||||
Opportunity.territory,
|
||||
)
|
||||
|
||||
22
erpnext/crm/report/lost_opportunity/test_lost_opportunity.py
Normal file
22
erpnext/crm/report/lost_opportunity/test_lost_opportunity.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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.crm.report.lost_opportunity.lost_opportunity import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestLostOpportunity(ERPNextTestSuite):
|
||||
def test_report_aggregates_lost_reasons(self):
|
||||
# Exercises the db-aware GROUP_CONCAT (MariaDB) / STRING_AGG (postgres) aggregation of the
|
||||
# child "Opportunity Lost Reason Detail" rows. The MySQL-only GROUP_CONCAT term would fail to
|
||||
# compile on postgres, so simply running the report query guards the portability fix on both
|
||||
# databases.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
columns, data = execute(
|
||||
frappe._dict({"company": company, "from_date": add_days(today(), -365), "to_date": today()})
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
self.assertIsInstance(data, list)
|
||||
@@ -86,10 +86,12 @@ class SalesPipelineAnalytics:
|
||||
|
||||
if self.filters.get("range") == "Monthly":
|
||||
self.group_by_period = Month(opp.expected_closing)
|
||||
self.duration = MonthName(opp.expected_closing).as_("month")
|
||||
self.duration_expr = MonthName(opp.expected_closing)
|
||||
self.duration = self.duration_expr.as_("month")
|
||||
else:
|
||||
self.group_by_period = Quarter(opp.expected_closing)
|
||||
self.duration = Quarter(opp.expected_closing).as_("quarter")
|
||||
self.duration_expr = Quarter(opp.expected_closing)
|
||||
self.duration = self.duration_expr.as_("quarter")
|
||||
|
||||
self.pipeline_by = {"Owner": "opportunity_owner", "Sales Stage": "sales_stage"}[
|
||||
self.filters.get("pipeline_by")
|
||||
@@ -101,27 +103,35 @@ class SalesPipelineAnalytics:
|
||||
self.get_fields()
|
||||
|
||||
opp = frappe.qb.DocType("Opportunity")
|
||||
query = frappe.qb.get_query(
|
||||
"Opportunity",
|
||||
filters=self.get_conditions(),
|
||||
ignore_permissions=True,
|
||||
)
|
||||
|
||||
pipeline_field = opp._assign if self.group_by_based_on == "_assign" else opp.sales_stage
|
||||
|
||||
if self.filters.get("based_on") == "Number":
|
||||
# Ask get_query for exactly the grouped columns via `fields`, instead of taking its
|
||||
# default un-grouped "name" select and stripping it. Group by the displayed period
|
||||
# expression too, so postgres accepts MonthName alongside the numeric Month used for
|
||||
# chronological ordering (for Quarterly they're the same expression).
|
||||
self.query_result = (
|
||||
query.select(
|
||||
pipeline_field.as_(self.pipeline_by),
|
||||
frappe.query_builder.functions.Count("*").as_("count"),
|
||||
self.duration,
|
||||
frappe.qb.get_query(
|
||||
"Opportunity",
|
||||
filters=self.get_conditions(),
|
||||
fields=[
|
||||
pipeline_field.as_(self.pipeline_by),
|
||||
frappe.query_builder.functions.Count("*").as_("count"),
|
||||
self.duration,
|
||||
],
|
||||
ignore_permissions=True,
|
||||
)
|
||||
.groupby(pipeline_field, self.group_by_period)
|
||||
.groupby(pipeline_field, self.group_by_period, self.duration_expr)
|
||||
.orderby(self.group_by_period)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if self.filters.get("based_on") == "Amount":
|
||||
query = frappe.qb.get_query(
|
||||
"Opportunity",
|
||||
filters=self.get_conditions(),
|
||||
ignore_permissions=True,
|
||||
)
|
||||
self.query_result = query.select(
|
||||
pipeline_field.as_(self.pipeline_by),
|
||||
opp.opportunity_amount.as_("amount"),
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-06-14 10:35+0000\n"
|
||||
"PO-Revision-Date: 2026-06-14 17:01\n"
|
||||
"PO-Revision-Date: 2026-06-18 18:25\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Bosnian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -720,8 +720,8 @@ msgid "<h3>About Product Bundle</h3>\n\n"
|
||||
"<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n"
|
||||
"<h4>Example:</h4>\n"
|
||||
"<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>"
|
||||
msgstr "<h3>O Paketu Proizvoda</h3>\n\n"
|
||||
"<p>Spoji grupu <b>artikala</b> u drugi <b>artikal</b>. Ovo je korisno ako spajate određene <b>Artikle</b> u paket i održavate zalihe upakiranih <b>artikala</b>, a ne zbirn <b>artikal</b>.</p>\n"
|
||||
msgstr "<h3>O Paketu Artikala</h3>\n\n"
|
||||
"<p>Spoji grupu <b>artikala</b> u drugi <b>artikal</b>. Ovo je korisno ako spajate određene <b>Artikle</b> u paket i održavate zalihe upakiranih <b>artikala</b>, a ne zbirni <b>artikal</b>.</p>\n"
|
||||
"<p>Paketni <b>Artikal</b> će imati <code>artikle na zalihi</code> kao <b>Ne</b> i <code>Prodajni Artikal</code> kao <b>Da </b>.</p>\n"
|
||||
"<h4>Primjer:</h4>\n"
|
||||
"<p>Ako prodajete prijenosna računala i ruksake odvojeno i imate posebnu cijenu ako Klijent kupi oboje, tada će prijenosno računalo + ruksak biti novi artikal paketa proizvoda.</p>"
|
||||
@@ -1102,7 +1102,7 @@ msgstr "Klijent mora imati primarni kontakt e-poštu."
|
||||
#. Description of the 'Disabled' (Check) field in DocType 'Product Bundle'
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.json
|
||||
msgid "A disabled Product Bundle cannot be selected in transactions."
|
||||
msgstr ""
|
||||
msgstr "Onemogućeni Paket Artikal ne može se odabrati u transakcijama."
|
||||
|
||||
#: erpnext/stock/doctype/delivery_trip/delivery_trip.py:59
|
||||
msgid "A driver must be set to submit."
|
||||
@@ -4487,7 +4487,7 @@ msgstr "Dozvoli Uređivanje Količine Jedinice Zaliha za Dokumente Prodaje"
|
||||
#. DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Allow to edit stock UOM qty for Stock Entry"
|
||||
msgstr ""
|
||||
msgstr "Omogući uređivanje količine jedinice zaliha za Unos Zaliha"
|
||||
|
||||
#. Label of the allow_to_make_quality_inspection_after_purchase_or_delivery
|
||||
#. (Check) field in DocType 'Stock Settings'
|
||||
@@ -4597,7 +4597,7 @@ msgstr "Alternativni Artikal"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:427
|
||||
msgid "Alternative For Item"
|
||||
msgstr ""
|
||||
msgstr "Artikal Alternativa"
|
||||
|
||||
#. Label of the alternative_item_code (Link) field in DocType 'Item
|
||||
#. Alternative'
|
||||
@@ -6918,7 +6918,7 @@ msgstr "Alat Poređenja Sastavnica"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:178
|
||||
msgid "BOM Component"
|
||||
msgstr ""
|
||||
msgstr "Komponenta Sastavnice"
|
||||
|
||||
#. Label of the bom_conf_tab (Tab Break) field in DocType 'BOM'
|
||||
#: erpnext/manufacturing/doctype/bom/bom.json
|
||||
@@ -6949,7 +6949,7 @@ msgstr "Artikal Sastavnice Konstruktora"
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:392
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:535
|
||||
msgid "BOM Creator Item with name {0} does not exist"
|
||||
msgstr ""
|
||||
msgstr "Artikal Sastavnice s nazivom {0} ne postoji"
|
||||
|
||||
#. Label of the bom_detail_no (Data) field in DocType 'Purchase Receipt Item
|
||||
#. Supplied'
|
||||
@@ -7049,7 +7049,7 @@ msgstr "Operativno Vrijeme Sastavnice"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:248
|
||||
msgid "BOM Output"
|
||||
msgstr ""
|
||||
msgstr "Sastavnica"
|
||||
|
||||
#: erpnext/stock/report/item_prices/item_prices.py:60
|
||||
msgid "BOM Rate"
|
||||
@@ -8222,13 +8222,13 @@ msgstr "Datum Fakture"
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Bill Even If Previous Invoice Unpaid"
|
||||
msgstr ""
|
||||
msgstr "Fakturiši čak i ako prethodna faktura nije plaćena"
|
||||
|
||||
#. Option for the 'Generate Invoice At' (Select) field in DocType
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Bill N days before period start"
|
||||
msgstr ""
|
||||
msgstr "Fakturiši N dana prije početka perioda"
|
||||
|
||||
#. Label of the bill_no (Data) field in DocType 'Journal Entry'
|
||||
#. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt'
|
||||
@@ -8410,13 +8410,13 @@ msgstr "e-pošta Fakture"
|
||||
#. Label of the billing_heatmap (HTML) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing Heatmap"
|
||||
msgstr ""
|
||||
msgstr "Toplinska mapa Fakturisanja"
|
||||
|
||||
#. Label of the billing_history_section (Section Break) field in DocType
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing History"
|
||||
msgstr ""
|
||||
msgstr "Historija Fakturisanja"
|
||||
|
||||
#. Label of the billing_hours (Float) field in DocType 'Sales Invoice
|
||||
#. Timesheet'
|
||||
@@ -8450,7 +8450,7 @@ msgstr "Faktura Interval u Planu pretplate mora biti Mjesec koji prati kalendars
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing Period"
|
||||
msgstr ""
|
||||
msgstr "Period Fakturisanja"
|
||||
|
||||
#. Label of the billing_rate (Currency) field in DocType 'Activity Cost'
|
||||
#. Label of the billing_rate (Currency) field in DocType 'Timesheet Detail'
|
||||
@@ -9278,7 +9278,7 @@ msgstr "Izračunaj procijenjeno vrijeme dolaska"
|
||||
#. Settings'
|
||||
#: erpnext/selling/doctype/selling_settings/selling_settings.json
|
||||
msgid "Calculate Product Bundle price based on child Item's rates"
|
||||
msgstr "Obračunaj Cijenu Paketa Proizvoda na osnovu cijena Podređenih Artikala"
|
||||
msgstr "Obračunaj Cijenu Paketa Artikala na osnovu cijena Podređenih Artikala"
|
||||
|
||||
#. Description of the 'Hidden Line (Internal Use Only)' (Check) field in
|
||||
#. DocType 'Financial Report Row'
|
||||
@@ -9547,7 +9547,7 @@ msgstr "Otkaži Pretplatu nakon perioda odgode"
|
||||
#. Label of the cancel_at_period_end (Check) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Cancel When Period Ends"
|
||||
msgstr ""
|
||||
msgstr "Otkaži kada se završi period"
|
||||
|
||||
#. Label of the cancelation_date (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
@@ -9556,7 +9556,7 @@ msgstr "Datum Otkazivanja"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:1567
|
||||
msgid "Cancelled Job Card cannot be processed."
|
||||
msgstr ""
|
||||
msgstr "Otkazani Radni Nalog ne može se obraditi."
|
||||
|
||||
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:76
|
||||
msgid "Cannot Assign Cashier"
|
||||
@@ -9691,7 +9691,7 @@ msgstr "Nije moguće pretvoriti u Grupu jer je odabran Tip Računa."
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/mapper.py:372
|
||||
msgid "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. Please check the existing linked {2}s."
|
||||
msgstr ""
|
||||
msgstr "Nije moguće kreirati {0} između poduzeća. Svi početni artikli {1} su već u potpunosti fakturisani. Provjeri postojeće povezane {2}."
|
||||
|
||||
#: erpnext/stock/doctype/purchase_receipt/services/reservation.py:49
|
||||
msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts."
|
||||
@@ -9770,7 +9770,7 @@ msgstr "Nije moguće omogućiti račun zaliha po artiklima, jer postoje postoje
|
||||
|
||||
#: erpnext/crm/doctype/crm_settings/crm_settings.py:37
|
||||
msgid "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
|
||||
msgstr ""
|
||||
msgstr "Nije moguće omogućiti kreiranje prilike iz kontakta jer je kontakt obrazac onemogućen."
|
||||
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.py:624
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.py:647
|
||||
@@ -9878,7 +9878,7 @@ msgstr "Nije moguće započeti brisanje. Drugo brisanje {0} je već u redu čeka
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:922
|
||||
msgid "Cannot submit Job Card {0} while it is On Hold. Please resume and complete the job before submission."
|
||||
msgstr ""
|
||||
msgstr "Nije moguće podnijeti Radni Nalog {0} dok je na čekanju. Nastavi i završi posao prije podnošenja."
|
||||
|
||||
#: erpnext/accounts/services/child_item_update.py:283
|
||||
msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation"
|
||||
@@ -13410,7 +13410,7 @@ msgstr "Kreiraj novi trag"
|
||||
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.js:16
|
||||
msgid "Create New Version"
|
||||
msgstr ""
|
||||
msgstr "Kreiraj novu verziju"
|
||||
|
||||
#: banking/src/components/common/LinkFieldCombobox.tsx:284
|
||||
msgid "Create New {0}"
|
||||
@@ -14291,12 +14291,12 @@ msgstr "Trenutni Valuta kurs"
|
||||
#. Label of the current_invoice_end (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Current Invoice End"
|
||||
msgstr ""
|
||||
msgstr "Trenutni Završni Datum Fakture"
|
||||
|
||||
#. Label of the current_invoice_start (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Current Invoice Start"
|
||||
msgstr ""
|
||||
msgstr "Trenutni Početni Datum Fakture"
|
||||
|
||||
#. Label of the current_level (Int) field in DocType 'BOM Update Log'
|
||||
#: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
|
||||
@@ -17243,7 +17243,7 @@ msgstr "Onemogućeni Bankovni Račun"
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:216
|
||||
msgid "Disabled Product Bundle"
|
||||
msgstr ""
|
||||
msgstr "Onemogući Paket Artikala"
|
||||
|
||||
#: erpnext/stock/utils.py:434
|
||||
msgid "Disabled Warehouse {0} cannot be used for this transaction."
|
||||
@@ -18939,7 +18939,7 @@ msgstr "Omogući Program Bodova Lojalnosti"
|
||||
#. DocType 'CRM Settings'
|
||||
#: erpnext/crm/doctype/crm_settings/crm_settings.json
|
||||
msgid "Enable Opportunity Creation from Contact Us"
|
||||
msgstr ""
|
||||
msgstr "Omogući Kreiranje Prilika iz Kontaktiraj Nas obrasca"
|
||||
|
||||
#. Label of the enable_parallel_reposting (Check) field in DocType 'Stock
|
||||
#. Reposting Settings'
|
||||
@@ -19151,9 +19151,9 @@ msgid "Enabling this will do the following:\n"
|
||||
msgstr "Omogućavanje ovoga će učiniti sljedeće:\n"
|
||||
"<ul style=\"padding-left:16px\">\n"
|
||||
"<li>Omogućiti uređivanje kolone cjene u svim tabelama Pakiranih/Paketnih artikala.</li>\n"
|
||||
"<li>Izračunati cijene svih <a href=\"/desk/product-bundle\" rel=\"noopener noreferrer\">paketa proizvoda</a> u tabeli artikala na osnovu cijena njihovih podređenih artikala navedenih u tabeli pakiranih/paketiranih artikala. </li>\n"
|
||||
"<li>Izračunati cijene svih <a href=\"/desk/product-bundle\" rel=\"noopener noreferrer\">paketa artikala</a> u tabeli artikala na osnovu cijena njihovih podređenih artikala navedenih u tabeli pakiranih/paketiranih artikala. </li>\n"
|
||||
"</ul>\n"
|
||||
"Napomena: Ako je ovo omogućeno, ažuriranje cjene proizvoda u paketu u tabeli artikala neće promijeniti njegovu cijenu. Cijena će se vratiti na cijenu zasnovanu na podređenim artiklima prilikom spremanja dokumenta."
|
||||
"Napomena: Ako je ovo omogućeno, ažuriranje cjene artikala u paketu u tabeli artikala neće promijeniti njegovu cijenu. Cijena će se vratiti na cijenu zasnovanu na podređenim artiklima prilikom spremanja dokumenta."
|
||||
|
||||
#. Label of the encashment_date (Date) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
@@ -19541,7 +19541,7 @@ msgstr "Uloga Odobravatelja Izuzetka Proračuna"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:53
|
||||
msgid "Excess Disassembly"
|
||||
msgstr "Prekomjerna Demontaža"
|
||||
msgstr "Prekomjerno Rastavljanje"
|
||||
|
||||
#: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js:55
|
||||
msgid "Excess Materials Consumed"
|
||||
@@ -19977,7 +19977,7 @@ msgstr "Račun troškova je obavezan za artikal {0}"
|
||||
#. Description of the 'Enable Deferred Revenue' (Check) field in DocType 'Item'
|
||||
#: erpnext/stock/doctype/item/item.json
|
||||
msgid "Expense for this item will be recognized over a period of months. Eg: prepaid insurance or annual software license"
|
||||
msgstr "Trošak za ovaj artikal bit će priznat tokom nekoliko mjeseci. Npr: unaprijed plaćeno osiguranje ili godišnja licenca za softver"
|
||||
msgstr "Trošak za ovaj artikal bit će priznat tokom nekoliko mjeseci. Npr: unaprijed plaćeno osiguranje ili godišnja licenca za program"
|
||||
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:85
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:145
|
||||
@@ -20225,7 +20225,7 @@ msgstr "Ažuriranje prioriteta pravila nije uspjelo"
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:521
|
||||
msgid "Failed to update subscription status for {0} {1}"
|
||||
msgstr ""
|
||||
msgstr "Nije uspjelo ažuriranje statusa pretplate za {0} {1}"
|
||||
|
||||
#. Label of the failure_date (Datetime) field in DocType 'Asset Repair'
|
||||
#: erpnext/assets/doctype/asset_repair/asset_repair.json
|
||||
@@ -20765,7 +20765,7 @@ msgstr "Gotov Proizvod {0} ne odgovara Radnom Nalogu {1}"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:71
|
||||
msgid "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the finished good row."
|
||||
msgstr ""
|
||||
msgstr "Količina gotovog proizvoda koja se troši ({0} u jedinici zaliha) mora biti jednaka količini za rastavljanje ({1}). Ne mijenjaj jedinicu, faktor konverzije ili količinu u redu gotovog proizvoda."
|
||||
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.js:615
|
||||
msgid "First Delivery Date"
|
||||
@@ -23435,13 +23435,13 @@ msgstr "Ako je odbrano, ovaj artikal se tretira kao direktna dostava u Prodajnim
|
||||
#. Description of the 'Update Stock' (Check) field in DocType 'Sales Invoice'
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
|
||||
msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Delivery Note is created separately."
|
||||
msgstr ""
|
||||
msgstr "Ako je oodabrano, ažurira inventar; zalihe i knjigovodstveni unosi se kreiraju zajedno. Ostavi neodabrano ako se Dostavnica kreira zasebno."
|
||||
|
||||
#. Description of the 'Update Stock' (Check) field in DocType 'Purchase
|
||||
#. Invoice'
|
||||
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
|
||||
msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Purchase Receipt is created separately."
|
||||
msgstr ""
|
||||
msgstr "Ako je odabrano, ažurira se inventar; unosi zaliha i knjigoovodstva se kreiraju zajedno. Ostavi neodabrano ako Kupovni Račun kreira zasebno."
|
||||
|
||||
#: erpnext/public/js/setup_wizard.js:56
|
||||
msgid "If checked, we will create demo data for you to explore the system. This demo data can be erased later."
|
||||
@@ -25255,7 +25255,7 @@ msgstr "Nevažeće poduzeće za transakcije među poduzećima."
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:972
|
||||
msgid "Invalid Configuration"
|
||||
msgstr ""
|
||||
msgstr "Nevažeća Konfiguracija"
|
||||
|
||||
#: erpnext/accounts/services/taxes.py:294
|
||||
#: erpnext/assets/doctype/asset/asset.py:361
|
||||
@@ -25273,12 +25273,12 @@ msgstr "Nevažeći Datum Dostave"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:110
|
||||
msgid "Invalid Disassembly Item"
|
||||
msgstr ""
|
||||
msgstr "Nevažeći Artikala za Rastavljanje"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:76
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:125
|
||||
msgid "Invalid Disassembly Quantity"
|
||||
msgstr ""
|
||||
msgstr "Nevažeća Količina za Rastavljanje"
|
||||
|
||||
#: erpnext/selling/page/point_of_sale/pos_item_cart.js:414
|
||||
msgid "Invalid Discount"
|
||||
@@ -26144,7 +26144,7 @@ msgstr "Je Fantomski Artikal"
|
||||
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
|
||||
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
|
||||
msgid "Is Product Bundle"
|
||||
msgstr ""
|
||||
msgstr "Je Paket Artikala"
|
||||
|
||||
#. Label of the po_required (Select) field in DocType 'Buying Settings'
|
||||
#: erpnext/buying/doctype/buying_settings/buying_settings.json
|
||||
@@ -27652,7 +27652,7 @@ msgstr "Detalji Težine Artikla"
|
||||
#. Name of a report
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.json
|
||||
msgid "Item Where Used"
|
||||
msgstr ""
|
||||
msgstr "Gdje se koristi Artikal"
|
||||
|
||||
#. Label of a Link in the Buying Workspace
|
||||
#. Name of a report
|
||||
@@ -27775,7 +27775,7 @@ msgstr "Artikal {0} dodan je više puta pod isti nadređeni artikal {1} u redovi
|
||||
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.js:54
|
||||
msgid "Item {0} already has an active Product Bundle ({1}). Submitting this will create a new version and deactivate {1}."
|
||||
msgstr ""
|
||||
msgstr "Artikal {0} već ima aktivni Paket Artikala ({1}). Podnošenjem ovoga kreiraće te novu verziju i deaktivirati {1}."
|
||||
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:119
|
||||
msgid "Item {0} cannot be added as a sub-assembly of itself"
|
||||
@@ -28106,7 +28106,7 @@ msgstr "Stavka Radne Kartice"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:925
|
||||
msgid "Job Card On Hold"
|
||||
msgstr ""
|
||||
msgstr "Radni Nalog je na čekanju"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
|
||||
@@ -30309,7 +30309,7 @@ msgstr "Usklađeno"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:57
|
||||
msgid "Matched Field"
|
||||
msgstr ""
|
||||
msgstr "Usklađeno polje"
|
||||
|
||||
#. Label of the matched_transaction_rule (Link) field in DocType 'Bank
|
||||
#. Transaction'
|
||||
@@ -30928,7 +30928,7 @@ msgstr "Metar/Sekunda"
|
||||
|
||||
#: erpnext/manufacturing/doctype/workstation/workstation.py:546
|
||||
msgid "Method {0} is not allowed to be run on a Job Card."
|
||||
msgstr ""
|
||||
msgstr "Metoda {0} se ne smije izvršavati na Radnom Nalogu."
|
||||
|
||||
#. Name of a UOM
|
||||
#: erpnext/setup/setup_wizard/data/uom_data.json
|
||||
@@ -32258,13 +32258,13 @@ msgstr "Newton"
|
||||
#. Label of the next_billing_period_end (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Next Billing Period End"
|
||||
msgstr ""
|
||||
msgstr "Sljedeći Perioda Fakturisanja Završava"
|
||||
|
||||
#. Label of the next_billing_period_start (Date) field in DocType
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Next Billing Period Start"
|
||||
msgstr ""
|
||||
msgstr "Sljedeći Perioda Fakturisanja Počinje"
|
||||
|
||||
#. Label of the next_depreciation_date (Date) field in DocType 'Asset'
|
||||
#: erpnext/assets/doctype/asset/asset.json
|
||||
@@ -33476,7 +33476,7 @@ msgstr "Samo jedna operacija može imati odabranu opciju 'Je li Gotov Proizvod'
|
||||
#. Description of the 'Is Active' (Check) field in DocType 'Product Bundle'
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.json
|
||||
msgid "Only one version of a Product Bundle can be active at a time for a given Parent Item. Activating a version deactivates the previously active one."
|
||||
msgstr ""
|
||||
msgstr "Samo jedna verzija Paketa Artikala može biti aktivna u datom trenutku za dati Nadređeni Artikal. Aktiviranje verzije deaktivira prethodno aktivnu verziju."
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:719
|
||||
msgid "Only one {0} entry can be created against the Work Order {1}"
|
||||
@@ -39083,7 +39083,7 @@ msgstr "Vremenska oznaka knjiženja mora biti nakon {0}"
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Postpaid (bill at period end)"
|
||||
msgstr ""
|
||||
msgstr "Naknadno Plaćeno (faktura na završetku perioda)"
|
||||
|
||||
#. Description of a DocType
|
||||
#: erpnext/crm/doctype/opportunity/opportunity.json
|
||||
@@ -39186,7 +39186,7 @@ msgstr "Preferirana e-pošta"
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Prepaid (bill at period start)"
|
||||
msgstr ""
|
||||
msgstr "Unaprijed Plaćeno (faktura na početku perioda)"
|
||||
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:34
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:51
|
||||
@@ -40174,7 +40174,7 @@ msgstr "Stanje Paketa Proizvoda"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:278
|
||||
msgid "Product Bundle Component"
|
||||
msgstr ""
|
||||
msgstr "Komponenta Paketa Artikala"
|
||||
|
||||
#. Label of the product_bundle_help (HTML) field in DocType 'POS Invoice'
|
||||
#. Label of the product_bundle_help (HTML) field in DocType 'Sales Invoice'
|
||||
@@ -40195,11 +40195,11 @@ msgstr "Pomoć Paketa Proizvoda"
|
||||
#: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json
|
||||
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
|
||||
msgid "Product Bundle Item"
|
||||
msgstr "Artikal Paketa Proizvoda"
|
||||
msgstr "Artikal Paketa Artikala"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:305
|
||||
msgid "Product Bundle Parent"
|
||||
msgstr ""
|
||||
msgstr "Nadređeni Paket Artikala"
|
||||
|
||||
#. Description of the 'Product Bundle' (Link) field in DocType 'Purchase
|
||||
#. Invoice Item'
|
||||
@@ -40213,15 +40213,15 @@ msgstr ""
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.json
|
||||
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
|
||||
msgid "Product Bundle version this row was packed from"
|
||||
msgstr ""
|
||||
msgstr "Verzija Paketa Artikala iz koje je ovaj red preuzet iz"
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:453
|
||||
msgid "Product Bundle {0} is disabled and cannot be used in transactions."
|
||||
msgstr ""
|
||||
msgstr "Paket Artikala {0} je onemogućen i ne može se koristiti u transakcijama."
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:450
|
||||
msgid "Product Bundle {0} is not submitted"
|
||||
msgstr ""
|
||||
msgstr "Paket Artikala {0} nije podnešen"
|
||||
|
||||
#. Label of the product_discount_scheme_section (Section Break) field in
|
||||
#. DocType 'Pricing Rule'
|
||||
@@ -43881,7 +43881,7 @@ msgstr "Osvježite Plaid Link"
|
||||
#. Option for the 'Status' (Select) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Refunded"
|
||||
msgstr ""
|
||||
msgstr "Povraćeno"
|
||||
|
||||
#: erpnext/stock/reorder_item.py:390
|
||||
msgid "Regards,"
|
||||
@@ -43992,7 +43992,7 @@ msgstr "Povezano"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:50
|
||||
msgid "Related Item"
|
||||
msgstr ""
|
||||
msgstr "Povezani Artikal"
|
||||
|
||||
#. Label of the relation (Data) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
@@ -46064,7 +46064,7 @@ msgstr "Red #{0}: Gotov Proizvod artikla nije navedena zaservisni artikal {1}"
|
||||
|
||||
#: erpnext/manufacturing/doctype/bom/bom.py:371
|
||||
msgid "Row #{0}: Finished Good Item {1} cannot be added in the Secondary Items table."
|
||||
msgstr ""
|
||||
msgstr "Red #{0}: Artikal Gotovog Proizvoda {1} ne može se dodati u tabelu Sekundarnih Artikala."
|
||||
|
||||
#: erpnext/buying/doctype/purchase_order/services/subcontracting.py:28
|
||||
#: erpnext/selling/doctype/sales_order/services/subcontracting.py:27
|
||||
@@ -46155,7 +46155,7 @@ msgstr "Red #{0}: Artikal {1} nije artikal na zalihama"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:106
|
||||
msgid "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be added to this disassembly."
|
||||
msgstr ""
|
||||
msgstr "Red #{0}: Artikal {1} nije dio unosa izvornog proizvođača i ne može se dodati ovom rastavljanju."
|
||||
|
||||
#: erpnext/controllers/subcontracting_inward_controller.py:79
|
||||
msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead."
|
||||
@@ -46167,7 +46167,7 @@ msgstr "Red #{0}: Artikla {1} se ne slaže. Promjena koda artikla nije dozvoljen
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:115
|
||||
msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows."
|
||||
msgstr ""
|
||||
msgstr "Red #{0}: Količina artikla {1} ({2} u jedinici zaliha) ne odgovara količini izvedenoj iz izvora ({3}). Ne mijenjaj jedinicu, faktor konverzije ili količinu redova za rastavljanje."
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:780
|
||||
msgid "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher"
|
||||
@@ -46233,7 +46233,7 @@ msgstr "Red #{0}: Procentualni Gubitka Procesa treba da bude manji od 100% za {1
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:213
|
||||
msgid "Row #{0}: Product Bundle {1} is disabled and cannot be used in transactions."
|
||||
msgstr ""
|
||||
msgstr "Red #{0}: Paket Artikal {1} je onemogućen i ne može se koristiti u transakcijama."
|
||||
|
||||
#: erpnext/public/js/utils/barcode_scanner.js:425
|
||||
msgid "Row #{0}: Qty increased by {1}"
|
||||
@@ -49490,7 +49490,7 @@ msgstr "Serijski i Šaržni Paket {0} nije podnešen"
|
||||
|
||||
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2251
|
||||
msgid "Serial and Batch Bundle {0} is submitted and its entries cannot be modified."
|
||||
msgstr ""
|
||||
msgstr "Serijski i Šaržni Paket {0} je podnešen i njegovi unosi se ne mogu mijenjati."
|
||||
|
||||
#. Label of the section_break_45 (Section Break) field in DocType
|
||||
#. 'Subcontracting Receipt Item'
|
||||
@@ -52668,7 +52668,7 @@ msgstr "Podizvođačka Dostava"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:362
|
||||
msgid "Subcontracting Finished Good"
|
||||
msgstr ""
|
||||
msgstr "Podizvođački Gotov Proizvod"
|
||||
|
||||
#. Label of the subcontracting_inward_tab (Tab Break) field in DocType 'Selling
|
||||
#. Settings'
|
||||
@@ -52852,7 +52852,7 @@ msgstr "Podizvođački Prodajni Nalog"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:336
|
||||
msgid "Subcontracting Service Item"
|
||||
msgstr ""
|
||||
msgstr "Podizvođački Uslužni Artikal"
|
||||
|
||||
#. Label of the subcontract (Tab Break) field in DocType 'Buying Settings'
|
||||
#: erpnext/buying/doctype/buying_settings/buying_settings.json
|
||||
@@ -52900,7 +52900,7 @@ msgstr "Podnesi Ponudu"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:1570
|
||||
msgid "Submitted Job Card cannot be processed."
|
||||
msgstr ""
|
||||
msgstr "Podnešeni Radni Nalog ne može biti obrađen."
|
||||
|
||||
#. Label of the subscription_section (Section Break) field in DocType 'Payment
|
||||
#. Request'
|
||||
@@ -55486,7 +55486,7 @@ msgstr "Otpremljena datoteka nije u važećem MT940 formatu."
|
||||
|
||||
#: erpnext/edi/doctype/code_list/code_list_import.py:40
|
||||
msgid "The uploaded file does not match the selected Code List."
|
||||
msgstr "Učitani fajl ne odgovara odabranoj Listi Kodova."
|
||||
msgstr "Učitana datoteka ne odgovara odabranoj Listi Kodova."
|
||||
|
||||
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js:10
|
||||
msgid "The user cannot submit the Serial and Batch Bundle manually"
|
||||
@@ -61720,7 +61720,7 @@ msgstr "Ne možete podnijeti nalog bez plaćanja."
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:968
|
||||
msgid "You cannot update stock for a Debit Note. A Debit Note is a financial document that should not affect inventory. Please disable 'Update Stock'."
|
||||
msgstr ""
|
||||
msgstr "Ne možete ažurirati zalihe za debitnu notu. Debitna nota je finansijski dokument koji ne bi trebao utjecati na zalihe. Molimo vas da onemogućite opciju 'Ažuriraj Zalihe'."
|
||||
|
||||
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:106
|
||||
msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}"
|
||||
@@ -62086,7 +62086,7 @@ msgstr "izvodi bilo koje dolje:"
|
||||
#. Item'
|
||||
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
|
||||
msgid "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle"
|
||||
msgstr "naziv reda artikla paketa proizvoda u prodajnom nalogu. Također označava da odabrani artikal treba koristiti za paket proizvoda"
|
||||
msgstr "naziv reda artikla paketa artikala u prodajnom nalogu. Također označava da odabrani artikal treba koristiti za paket artikala"
|
||||
|
||||
#. Option for the 'Plaid Environment' (Select) field in DocType 'Plaid
|
||||
#. Settings'
|
||||
@@ -62834,7 +62834,7 @@ msgstr "{0}, završi operaciju {1} prije operacije {2}."
|
||||
|
||||
#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:61
|
||||
msgid "{0}, {1} or {2} are the only allowed options."
|
||||
msgstr ""
|
||||
msgstr "{0}, {1} ili {2} su jedine dozvoljene opcije."
|
||||
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:525
|
||||
msgid "{0}: Child table (auto-deleted with parent)"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-06-14 10:35+0000\n"
|
||||
"PO-Revision-Date: 2026-06-14 17:01\n"
|
||||
"PO-Revision-Date: 2026-06-17 17:52\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Persian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -4507,7 +4507,7 @@ msgstr "آیتم جایگزین"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:427
|
||||
msgid "Alternative For Item"
|
||||
msgstr ""
|
||||
msgstr "جایگزین برای آیتم"
|
||||
|
||||
#. Label of the alternative_item_code (Link) field in DocType 'Item
|
||||
#. Alternative'
|
||||
@@ -6828,7 +6828,7 @@ msgstr "ابزار مقایسه BOM"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:178
|
||||
msgid "BOM Component"
|
||||
msgstr ""
|
||||
msgstr "مولفه BOM"
|
||||
|
||||
#. Label of the bom_conf_tab (Tab Break) field in DocType 'BOM'
|
||||
#: erpnext/manufacturing/doctype/bom/bom.json
|
||||
@@ -6859,7 +6859,7 @@ msgstr "آیتم ایجاد کننده BOM"
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:392
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:535
|
||||
msgid "BOM Creator Item with name {0} does not exist"
|
||||
msgstr ""
|
||||
msgstr "آیتم سازنده BOM با نام {0} وجود ندارد"
|
||||
|
||||
#. Label of the bom_detail_no (Data) field in DocType 'Purchase Receipt Item
|
||||
#. Supplied'
|
||||
@@ -6959,7 +6959,7 @@ msgstr "زمان عملیات BOM"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:248
|
||||
msgid "BOM Output"
|
||||
msgstr ""
|
||||
msgstr "خروجی BOM"
|
||||
|
||||
#: erpnext/stock/report/item_prices/item_prices.py:60
|
||||
msgid "BOM Rate"
|
||||
@@ -7630,7 +7630,7 @@ msgstr "تراکنش بانکی {0} به روز شد"
|
||||
|
||||
#: banking/src/pages/BankReconciliation.tsx:118
|
||||
msgid "Bank Transactions"
|
||||
msgstr ""
|
||||
msgstr "تراکنشهای بانکی"
|
||||
|
||||
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:584
|
||||
msgid "Bank account cannot be named as {0}"
|
||||
@@ -8132,13 +8132,13 @@ msgstr "تاریخ صورتحساب"
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Bill Even If Previous Invoice Unpaid"
|
||||
msgstr ""
|
||||
msgstr "صدور صورتحساب حتی اگر فاکتور قبلی پرداخت نشده باشد"
|
||||
|
||||
#. Option for the 'Generate Invoice At' (Select) field in DocType
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Bill N days before period start"
|
||||
msgstr ""
|
||||
msgstr "صورتحساب N روز قبل از شروع دوره"
|
||||
|
||||
#. Label of the bill_no (Data) field in DocType 'Journal Entry'
|
||||
#. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt'
|
||||
@@ -8320,13 +8320,13 @@ msgstr "ایمیل صورتحساب"
|
||||
#. Label of the billing_heatmap (HTML) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing Heatmap"
|
||||
msgstr ""
|
||||
msgstr "نقشه حرارتی صورتحساب"
|
||||
|
||||
#. Label of the billing_history_section (Section Break) field in DocType
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing History"
|
||||
msgstr ""
|
||||
msgstr "تاریخچه صورتحساب"
|
||||
|
||||
#. Label of the billing_hours (Float) field in DocType 'Sales Invoice
|
||||
#. Timesheet'
|
||||
@@ -8360,7 +8360,7 @@ msgstr "بازه صورتحساب در طرح اشتراک باید ماه با
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing Period"
|
||||
msgstr ""
|
||||
msgstr "دوره صورتحساب"
|
||||
|
||||
#. Label of the billing_rate (Currency) field in DocType 'Activity Cost'
|
||||
#. Label of the billing_rate (Currency) field in DocType 'Timesheet Detail'
|
||||
@@ -8905,11 +8905,11 @@ msgstr "ساختمان ها"
|
||||
|
||||
#: banking/src/components/features/ActionLog/ActionLogDialogBody.tsx:88
|
||||
msgid "Bulk Bank Entry"
|
||||
msgstr ""
|
||||
msgstr "ثبت بانک انبوه"
|
||||
|
||||
#: banking/src/components/features/ActionLog/ActionLogDialogBody.tsx:76
|
||||
msgid "Bulk Payment"
|
||||
msgstr ""
|
||||
msgstr "پرداخت انبوه"
|
||||
|
||||
#: erpnext/utilities/doctype/rename_tool/rename_tool.js:71
|
||||
msgid "Bulk Rename Jobs"
|
||||
@@ -9457,7 +9457,7 @@ msgstr "لغو اشتراک پس از دوره مهلت"
|
||||
#. Label of the cancel_at_period_end (Check) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Cancel When Period Ends"
|
||||
msgstr ""
|
||||
msgstr "لغو هنگام پایان دوره"
|
||||
|
||||
#. Label of the cancelation_date (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
@@ -13320,7 +13320,7 @@ msgstr "ایجاد سرنخ جدید"
|
||||
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.js:16
|
||||
msgid "Create New Version"
|
||||
msgstr ""
|
||||
msgstr "ایجاد نسخه جدید"
|
||||
|
||||
#: banking/src/components/common/LinkFieldCombobox.tsx:284
|
||||
msgid "Create New {0}"
|
||||
@@ -14201,12 +14201,12 @@ msgstr "نرخ ارز فعلی"
|
||||
#. Label of the current_invoice_end (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Current Invoice End"
|
||||
msgstr ""
|
||||
msgstr "پایان فاکتور فعلی"
|
||||
|
||||
#. Label of the current_invoice_start (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Current Invoice Start"
|
||||
msgstr ""
|
||||
msgstr "شروع فاکتور فعلی"
|
||||
|
||||
#. Label of the current_level (Int) field in DocType 'BOM Update Log'
|
||||
#: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
|
||||
@@ -17153,7 +17153,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:216
|
||||
msgid "Disabled Product Bundle"
|
||||
msgstr ""
|
||||
msgstr "بسته محصول غیرفعال"
|
||||
|
||||
#: erpnext/stock/utils.py:434
|
||||
msgid "Disabled Warehouse {0} cannot be used for this transaction."
|
||||
@@ -17162,7 +17162,7 @@ msgstr "از انبار غیرفعال شده {0} نمیتوان برای ا
|
||||
#. Description of the 'Disabled' (Check) field in DocType 'Item'
|
||||
#: erpnext/stock/doctype/item/item.json
|
||||
msgid "Disabled items cannot be selected in any transaction."
|
||||
msgstr ""
|
||||
msgstr "اقلام غیرفعال را نمیتوان در هیچ تراکنشی انتخاب کرد."
|
||||
|
||||
#: erpnext/accounts/services/internal_transfer.py:118
|
||||
msgid "Disabled pricing rules since this {} is an internal transfer"
|
||||
@@ -25029,7 +25029,7 @@ msgstr "مرجع فروش داخلی وجود ندارد"
|
||||
#. 'Supplier'
|
||||
#: erpnext/buying/doctype/supplier/supplier.json
|
||||
msgid "Internal Supplier Details"
|
||||
msgstr ""
|
||||
msgstr "جزئیات تأمینکننده داخلی"
|
||||
|
||||
#: erpnext/buying/doctype/supplier/supplier.py:180
|
||||
msgid "Internal Supplier for company {0} already exists"
|
||||
@@ -25060,7 +25060,7 @@ msgstr "مرجع انتقال داخلی وجود ندارد"
|
||||
#. DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Internal Transfer Rules"
|
||||
msgstr ""
|
||||
msgstr "قوانین انتقال داخلی"
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py:37
|
||||
msgid "Internal Transfers"
|
||||
@@ -25196,7 +25196,7 @@ msgstr "نوع سند نامعتبر است"
|
||||
|
||||
#: erpnext/selling/report/sales_analytics/sales_analytics.py:529
|
||||
msgid "Invalid Document Type {0}"
|
||||
msgstr ""
|
||||
msgstr "نوع سند نامعتبر {0}"
|
||||
|
||||
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:207
|
||||
msgid "Invalid File Type"
|
||||
@@ -25313,7 +25313,7 @@ msgstr "انبار منبع و هدف نامعتبر"
|
||||
|
||||
#: erpnext/selling/report/sales_analytics/sales_analytics.py:507
|
||||
msgid "Invalid Tree Type {0}"
|
||||
msgstr ""
|
||||
msgstr "نوع درخت نامعتبر {0}"
|
||||
|
||||
#: erpnext/edi/doctype/code_list/code_list_import.py:37
|
||||
msgid "Invalid Upload"
|
||||
@@ -25374,11 +25374,11 @@ msgstr "پرسمان جستجوی نامعتبر"
|
||||
|
||||
#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:99
|
||||
msgid "Invalid value {0} for 'Based On'"
|
||||
msgstr ""
|
||||
msgstr "مقدار نامعتبر {0} برای 'Based On'"
|
||||
|
||||
#: erpnext/selling/report/inactive_customers/inactive_customers.py:20
|
||||
msgid "Invalid value {0} for 'Doctype'"
|
||||
msgstr ""
|
||||
msgstr "مقدار نامعتبر {0} برای 'Doctype'"
|
||||
|
||||
#: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py:109
|
||||
#: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py:119
|
||||
@@ -25411,7 +25411,7 @@ msgstr "فهرست موجودی"
|
||||
#. Default'
|
||||
#: erpnext/stock/doctype/item_default/item_default.json
|
||||
msgid "Inventory Account"
|
||||
msgstr ""
|
||||
msgstr "حساب موجودی"
|
||||
|
||||
#. Label of the inventory_account_currency (Link) field in DocType 'Item
|
||||
#. Default'
|
||||
@@ -26993,7 +26993,7 @@ msgstr "نام گروه آیتم"
|
||||
|
||||
#: erpnext/setup/doctype/item_group/item_group.js:119
|
||||
msgid "Item Group Override"
|
||||
msgstr ""
|
||||
msgstr "بازتعریف گروه آیتم"
|
||||
|
||||
#: erpnext/setup/doctype/item_group/item_group.js:82
|
||||
msgid "Item Group Tree"
|
||||
@@ -27267,7 +27267,7 @@ msgstr "آیتم موجود نیست"
|
||||
#. Default'
|
||||
#: erpnext/stock/doctype/item_default/item_default.json
|
||||
msgid "Item Override"
|
||||
msgstr ""
|
||||
msgstr "بازتعریف آیتم"
|
||||
|
||||
#. Label of a Link in the Buying Workspace
|
||||
#. Label of a Link in the Selling Workspace
|
||||
@@ -29370,7 +29370,7 @@ msgstr "نگهداری موجودی"
|
||||
#. DocType 'Accounts Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Maintain same rate throughout internal Transaction"
|
||||
msgstr ""
|
||||
msgstr "حفظ نرخ یکسان در کل تراکنشهای داخلی"
|
||||
|
||||
#. Label of the maintain_same_sales_rate (Check) field in DocType 'Selling
|
||||
#. Settings'
|
||||
@@ -30760,7 +30760,7 @@ msgstr "ادغام پیشرفت"
|
||||
#. Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Merge similar Account Heads"
|
||||
msgstr ""
|
||||
msgstr "ادغام سر فصلهای حساب مشابه"
|
||||
|
||||
#: erpnext/public/js/utils.js:1090
|
||||
msgid "Merge taxes from multiple documents"
|
||||
@@ -31140,7 +31140,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:929
|
||||
msgid "Missing Dependency"
|
||||
msgstr ""
|
||||
msgstr "وابستگی گمشده"
|
||||
|
||||
#: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:44
|
||||
msgid "Missing Filters"
|
||||
@@ -31627,7 +31627,7 @@ msgstr "مقدار منفی مجاز نیست"
|
||||
#. Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Negative Stock"
|
||||
msgstr ""
|
||||
msgstr "موجودی منفی"
|
||||
|
||||
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1608
|
||||
#: erpnext/stock/serial_batch_bundle.py:1549
|
||||
@@ -32295,7 +32295,7 @@ msgstr "هیچ تامین کننده ای برای Inter Company Transactions ی
|
||||
|
||||
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:976
|
||||
msgid "No Tables Detected"
|
||||
msgstr ""
|
||||
msgstr "هیچ جدولی شناسایی نشد"
|
||||
|
||||
#: erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py:100
|
||||
msgid "No Tax Withholding data found for the current posting date."
|
||||
@@ -32341,7 +32341,7 @@ msgstr "هیچ BOM فعالی برای آیتم {0} یافت نشد. تحویل
|
||||
|
||||
#: erpnext/stock/doctype/item/item_prices.html:135
|
||||
msgid "No active item prices found."
|
||||
msgstr ""
|
||||
msgstr "هیچ قیمت آیتم فعالی یافت نشد."
|
||||
|
||||
#: erpnext/stock/doctype/item_variant_settings/item_variant_settings.js:46
|
||||
msgid "No additional fields available"
|
||||
@@ -43778,7 +43778,7 @@ msgstr "پیوند شطرنجی را تازه کنید"
|
||||
#. Option for the 'Status' (Select) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Refunded"
|
||||
msgstr ""
|
||||
msgstr "استرداد وجه شده"
|
||||
|
||||
#: erpnext/stock/reorder_item.py:390
|
||||
msgid "Regards,"
|
||||
@@ -43889,7 +43889,7 @@ msgstr "مربوط"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:50
|
||||
msgid "Related Item"
|
||||
msgstr ""
|
||||
msgstr "آیتم مرتبط"
|
||||
|
||||
#. Label of the relation (Data) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
@@ -48858,7 +48858,7 @@ msgstr "مبلغ فروش"
|
||||
#. Default'
|
||||
#: erpnext/stock/doctype/item_default/item_default.json
|
||||
msgid "Selling Cost Center"
|
||||
msgstr ""
|
||||
msgstr "مرکز هزینه فروش"
|
||||
|
||||
#: erpnext/stock/report/item_price_stock/item_price_stock.py:48
|
||||
msgid "Selling Price List"
|
||||
@@ -49026,7 +49026,7 @@ msgstr "شماره های سریال / دسته ای"
|
||||
#. Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Serial Item settings"
|
||||
msgstr ""
|
||||
msgstr "تنظیمات آیتم سریال"
|
||||
|
||||
#. Label of the serial_no (Text) field in DocType 'POS Invoice Item'
|
||||
#. Label of the serial_no (Text) field in DocType 'Purchase Invoice Item'
|
||||
@@ -50640,7 +50640,7 @@ msgstr "نمایش جزئیات پرداخت"
|
||||
#. 'Accounts Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Show Payment Schedule in print"
|
||||
msgstr ""
|
||||
msgstr "نمایش زمانبندی پرداخت در چاپ"
|
||||
|
||||
#. Label of the show_remarks (Check) field in DocType 'Process Statement Of
|
||||
#. Accounts'
|
||||
@@ -50684,12 +50684,12 @@ msgstr ""
|
||||
#. Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Show balances in Chart of Accounts"
|
||||
msgstr ""
|
||||
msgstr "نمایش ترازها در نمودار حسابها"
|
||||
|
||||
#. Label of the show_barcode_field (Check) field in DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Show barcode field in stock transactions"
|
||||
msgstr ""
|
||||
msgstr "نمایش فیلد بارکد در تراکنشهای موجودی"
|
||||
|
||||
#: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js:88
|
||||
msgid "Show in Bucket View"
|
||||
@@ -50704,7 +50704,7 @@ msgstr "نمایش در وب سایت"
|
||||
#. Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Show inclusive tax in print"
|
||||
msgstr ""
|
||||
msgstr "نمایش مالیات فراگیر در چاپ"
|
||||
|
||||
#. Description of the 'Reverse Sign' (Check) field in DocType 'Financial Report
|
||||
#. Row'
|
||||
@@ -50738,7 +50738,7 @@ msgstr "نمایش ثبتهای در انتظار"
|
||||
#. Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Show taxes as table in print"
|
||||
msgstr ""
|
||||
msgstr "نمایش مالیاتها به صورت جدول در چاپ"
|
||||
|
||||
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js:80
|
||||
#: erpnext/accounts/report/trial_balance/trial_balance.js:100
|
||||
@@ -50839,7 +50839,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:503
|
||||
msgid "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table."
|
||||
msgstr ""
|
||||
msgstr "از آنجایی که برای کالای نهایی {1}، اتلاف فرآیند {0} واحد وجود دارد، شما باید مقدار {0} واحد برای کالای نهایی {1} در جدول آیتمها را کاهش دهید."
|
||||
|
||||
#: erpnext/manufacturing/doctype/bom/bom.py:355
|
||||
msgid "Since you have enabled 'Track Semi Finished Goods', at least one operation must have 'Is Final Finished Good' checked. For that set the FG / Semi FG Item as {0} against an operation."
|
||||
@@ -51444,7 +51444,7 @@ msgstr ""
|
||||
#. Label of the statement_password (Password) field in DocType 'Bank Account'
|
||||
#: erpnext/accounts/doctype/bank_account/bank_account.json
|
||||
msgid "Statement PDF Password"
|
||||
msgstr ""
|
||||
msgstr "گذرواژه PDF صورتحساب"
|
||||
|
||||
#: erpnext/accounts/report/general_ledger/general_ledger.html:145
|
||||
msgid "Statement Period"
|
||||
@@ -52288,7 +52288,7 @@ msgstr ""
|
||||
#. Label of the stock_frozen_upto (Date) field in DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Stock frozen up to"
|
||||
msgstr ""
|
||||
msgstr "موجودی منجمد تا"
|
||||
|
||||
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1140
|
||||
msgid "Stock has been unreserved for work order {0}."
|
||||
@@ -52556,7 +52556,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:362
|
||||
msgid "Subcontracting Finished Good"
|
||||
msgstr ""
|
||||
msgstr "کالای نهایی پیمانکاری فرعی"
|
||||
|
||||
#. Label of the subcontracting_inward_tab (Tab Break) field in DocType 'Selling
|
||||
#. Settings'
|
||||
@@ -52740,7 +52740,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:336
|
||||
msgid "Subcontracting Service Item"
|
||||
msgstr ""
|
||||
msgstr "آیتم خدمات پیمانکاری فرعی"
|
||||
|
||||
#. Label of the subcontract (Tab Break) field in DocType 'Buying Settings'
|
||||
#: erpnext/buying/doctype/buying_settings/buying_settings.json
|
||||
@@ -52776,7 +52776,7 @@ msgstr "فاکتورهای تولید شده را ارسال کنید"
|
||||
#. Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Submit Journal entries"
|
||||
msgstr ""
|
||||
msgstr "ارسال ثبتهای دفتر روزنامه"
|
||||
|
||||
#: erpnext/manufacturing/doctype/work_order/work_order.js:185
|
||||
msgid "Submit this Work Order for further processing."
|
||||
@@ -52788,7 +52788,7 @@ msgstr "پیشفاکتور خود را ارسال کنید"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:1570
|
||||
msgid "Submitted Job Card cannot be processed."
|
||||
msgstr ""
|
||||
msgstr "کارت شغلی ارسالشده قابل پردازش نیست."
|
||||
|
||||
#. Label of the subscription_section (Section Break) field in DocType 'Payment
|
||||
#. Request'
|
||||
@@ -53733,7 +53733,7 @@ msgstr "جدول برای آیتم که در وب سایت نشان داده خ
|
||||
#: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:312
|
||||
#: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:329
|
||||
msgid "Table {0}"
|
||||
msgstr ""
|
||||
msgstr "جدول {0}"
|
||||
|
||||
#. Name of a UOM
|
||||
#: erpnext/setup/setup_wizard/data/uom_data.json
|
||||
@@ -54133,7 +54133,7 @@ msgstr "شناسه مالیاتی: {0}"
|
||||
#. Label of the taxation_section (Section Break) field in DocType 'Supplier'
|
||||
#: erpnext/buying/doctype/supplier/supplier.json
|
||||
msgid "Tax Identification"
|
||||
msgstr ""
|
||||
msgstr "شناسایی مالیات"
|
||||
|
||||
#. Label of a Card Break in the Invoicing Workspace
|
||||
#: erpnext/accounts/workspace/invoicing/invoicing.json
|
||||
@@ -55545,7 +55545,7 @@ msgstr "هنگام انجام اقدام خطایی رخ داد."
|
||||
|
||||
#: banking/src/components/ui/error-banner.tsx:21
|
||||
msgid "There was an error."
|
||||
msgstr ""
|
||||
msgstr "خطایی رخ داده است."
|
||||
|
||||
#: erpnext/accounts/doctype/bank/bank.js:112
|
||||
#: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:119
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-06-14 10:35+0000\n"
|
||||
"PO-Revision-Date: 2026-06-14 17:01\n"
|
||||
"PO-Revision-Date: 2026-06-18 18:25\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Croatian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -1102,7 +1102,7 @@ msgstr "Klijent mora imati primarni kontakt e-poštu."
|
||||
#. Description of the 'Disabled' (Check) field in DocType 'Product Bundle'
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.json
|
||||
msgid "A disabled Product Bundle cannot be selected in transactions."
|
||||
msgstr ""
|
||||
msgstr "Onemogućeni Paket Artikal ne može se odabrati u transakcijama."
|
||||
|
||||
#: erpnext/stock/doctype/delivery_trip/delivery_trip.py:59
|
||||
msgid "A driver must be set to submit."
|
||||
@@ -4487,7 +4487,7 @@ msgstr "Dopusti Uređivanje Količine Jedinice Zaliha za Dokumente Prodaje"
|
||||
#. DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Allow to edit stock UOM qty for Stock Entry"
|
||||
msgstr ""
|
||||
msgstr "Omogući uređivanje količine jedinice zaliha za Unos Zaliha"
|
||||
|
||||
#. Label of the allow_to_make_quality_inspection_after_purchase_or_delivery
|
||||
#. (Check) field in DocType 'Stock Settings'
|
||||
@@ -4597,7 +4597,7 @@ msgstr "Alternativni Artikal"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:427
|
||||
msgid "Alternative For Item"
|
||||
msgstr ""
|
||||
msgstr "Artikal Alternativa"
|
||||
|
||||
#. Label of the alternative_item_code (Link) field in DocType 'Item
|
||||
#. Alternative'
|
||||
@@ -6918,7 +6918,7 @@ msgstr "Alat Poređenja Sastavnica"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:178
|
||||
msgid "BOM Component"
|
||||
msgstr ""
|
||||
msgstr "Komponenta Sastavnice"
|
||||
|
||||
#. Label of the bom_conf_tab (Tab Break) field in DocType 'BOM'
|
||||
#: erpnext/manufacturing/doctype/bom/bom.json
|
||||
@@ -6949,7 +6949,7 @@ msgstr "Artikal Sastavnice Konstruktora"
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:392
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:535
|
||||
msgid "BOM Creator Item with name {0} does not exist"
|
||||
msgstr ""
|
||||
msgstr "Artikal Sastavnice s nazivom {0} ne postoji"
|
||||
|
||||
#. Label of the bom_detail_no (Data) field in DocType 'Purchase Receipt Item
|
||||
#. Supplied'
|
||||
@@ -7049,7 +7049,7 @@ msgstr "Operativno Vrijeme Sastavnice"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:248
|
||||
msgid "BOM Output"
|
||||
msgstr ""
|
||||
msgstr "Sastavnica"
|
||||
|
||||
#: erpnext/stock/report/item_prices/item_prices.py:60
|
||||
msgid "BOM Rate"
|
||||
@@ -8222,13 +8222,13 @@ msgstr "Datum Fakture"
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Bill Even If Previous Invoice Unpaid"
|
||||
msgstr ""
|
||||
msgstr "Fakturiraj čak i ako prethodna faktura nije plaćena"
|
||||
|
||||
#. Option for the 'Generate Invoice At' (Select) field in DocType
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Bill N days before period start"
|
||||
msgstr ""
|
||||
msgstr "Fakturiraj N dana prije početka perioda"
|
||||
|
||||
#. Label of the bill_no (Data) field in DocType 'Journal Entry'
|
||||
#. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt'
|
||||
@@ -8410,13 +8410,13 @@ msgstr "e-pošta Fakture"
|
||||
#. Label of the billing_heatmap (HTML) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing Heatmap"
|
||||
msgstr ""
|
||||
msgstr "Toplinska mapa Fakturisanja"
|
||||
|
||||
#. Label of the billing_history_section (Section Break) field in DocType
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing History"
|
||||
msgstr ""
|
||||
msgstr "Povijest Fakturiranja"
|
||||
|
||||
#. Label of the billing_hours (Float) field in DocType 'Sales Invoice
|
||||
#. Timesheet'
|
||||
@@ -8450,7 +8450,7 @@ msgstr "Faktura Interval u Planu pretplate mora biti Mjesec koji prati kalendars
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Billing Period"
|
||||
msgstr ""
|
||||
msgstr "Razdoblje Fakturiranja"
|
||||
|
||||
#. Label of the billing_rate (Currency) field in DocType 'Activity Cost'
|
||||
#. Label of the billing_rate (Currency) field in DocType 'Timesheet Detail'
|
||||
@@ -9547,7 +9547,7 @@ msgstr "Otkaži Pretplatu nakon perioda odgode"
|
||||
#. Label of the cancel_at_period_end (Check) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Cancel When Period Ends"
|
||||
msgstr ""
|
||||
msgstr "Otkaži po završetku razdoblja"
|
||||
|
||||
#. Label of the cancelation_date (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
@@ -9556,7 +9556,7 @@ msgstr "Datum Otkazivanja"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:1567
|
||||
msgid "Cancelled Job Card cannot be processed."
|
||||
msgstr ""
|
||||
msgstr "Otkazani Radni Nalog ne može se obraditi."
|
||||
|
||||
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:76
|
||||
msgid "Cannot Assign Cashier"
|
||||
@@ -9691,7 +9691,7 @@ msgstr "Nije moguće pretvoriti u Grupu jer je odabran Tip Računa."
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/mapper.py:372
|
||||
msgid "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. Please check the existing linked {2}s."
|
||||
msgstr ""
|
||||
msgstr "Nije moguće stvoriti međutvrtku {0}. Svi artikli u izvoru {1} već su u potpunosti fakturirani. Provjeri postojeće povezane {2}."
|
||||
|
||||
#: erpnext/stock/doctype/purchase_receipt/services/reservation.py:49
|
||||
msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts."
|
||||
@@ -9770,7 +9770,7 @@ msgstr "Nije moguće omogućiti račun zaliha po stavkama jer postoje postojeći
|
||||
|
||||
#: erpnext/crm/doctype/crm_settings/crm_settings.py:37
|
||||
msgid "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
|
||||
msgstr ""
|
||||
msgstr "Nije moguće omogućiti stvaranje prilike iz Kontaktirajte Nas jer je obrazac Kontaktirajte Nas onemogućen."
|
||||
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.py:624
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.py:647
|
||||
@@ -9878,7 +9878,7 @@ msgstr "Brisanje nije moguće. Drugo brisanje {0} je već u redu čekanja/pokre
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:922
|
||||
msgid "Cannot submit Job Card {0} while it is On Hold. Please resume and complete the job before submission."
|
||||
msgstr ""
|
||||
msgstr "Nije moguće podnijeti Radni Nalog {0} dok je na čekanju. Nastavi i završi posao prije podnošenja."
|
||||
|
||||
#: erpnext/accounts/services/child_item_update.py:283
|
||||
msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation"
|
||||
@@ -13410,7 +13410,7 @@ msgstr "Kreiraj novi trag"
|
||||
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.js:16
|
||||
msgid "Create New Version"
|
||||
msgstr ""
|
||||
msgstr "Stvori novu verziju"
|
||||
|
||||
#: banking/src/components/common/LinkFieldCombobox.tsx:284
|
||||
msgid "Create New {0}"
|
||||
@@ -14291,12 +14291,12 @@ msgstr "Trenutni Valuta kurs"
|
||||
#. Label of the current_invoice_end (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Current Invoice End"
|
||||
msgstr ""
|
||||
msgstr "Trenutni Završni Datum Fakture"
|
||||
|
||||
#. Label of the current_invoice_start (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Current Invoice Start"
|
||||
msgstr ""
|
||||
msgstr "Trenutni Početni Datum Fakture"
|
||||
|
||||
#. Label of the current_level (Int) field in DocType 'BOM Update Log'
|
||||
#: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
|
||||
@@ -17243,7 +17243,7 @@ msgstr "Onemogućeni Bankovni Račun"
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:216
|
||||
msgid "Disabled Product Bundle"
|
||||
msgstr ""
|
||||
msgstr "Onemogući Paket Artikala"
|
||||
|
||||
#: erpnext/stock/utils.py:434
|
||||
msgid "Disabled Warehouse {0} cannot be used for this transaction."
|
||||
@@ -18939,7 +18939,7 @@ msgstr "Omogući Program Bodova Lojalnosti"
|
||||
#. DocType 'CRM Settings'
|
||||
#: erpnext/crm/doctype/crm_settings/crm_settings.json
|
||||
msgid "Enable Opportunity Creation from Contact Us"
|
||||
msgstr ""
|
||||
msgstr "Omogući stvaranje Prilika iz Kontaktiraj Nas obrasca"
|
||||
|
||||
#. Label of the enable_parallel_reposting (Check) field in DocType 'Stock
|
||||
#. Reposting Settings'
|
||||
@@ -20225,7 +20225,7 @@ msgstr "Nije uspjelo ažuriranje prioriteta pravila"
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:521
|
||||
msgid "Failed to update subscription status for {0} {1}"
|
||||
msgstr ""
|
||||
msgstr "Nije uspjelo ažuriranje statusa pretplate za {0} {1}"
|
||||
|
||||
#. Label of the failure_date (Datetime) field in DocType 'Asset Repair'
|
||||
#: erpnext/assets/doctype/asset_repair/asset_repair.json
|
||||
@@ -20765,7 +20765,7 @@ msgstr "Gotov Proizvod {0} ne odgovara Radnom Nalogu {1}"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:71
|
||||
msgid "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the finished good row."
|
||||
msgstr ""
|
||||
msgstr "Količina gotovog proizvoda koja se troši ({0} u jedinici zaliha) mora biti jednaka količini za rastavljanje ({1}). Ne mijenjaj jedinicu, faktor konverzije ili količinu u redu gotovog proizvoda."
|
||||
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.js:615
|
||||
msgid "First Delivery Date"
|
||||
@@ -23435,13 +23435,13 @@ msgstr "Ako je odbrano, ovaj artikal se tretira kao direktna dostava u Prodajnim
|
||||
#. Description of the 'Update Stock' (Check) field in DocType 'Sales Invoice'
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
|
||||
msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Delivery Note is created separately."
|
||||
msgstr ""
|
||||
msgstr "Ako je oodabrano, ažurira inventar; zalihe i knjigovodstveni unosi se kreiraju zajedno. Ostavi neodabrano ako se Dostavnica kreira zasebno."
|
||||
|
||||
#. Description of the 'Update Stock' (Check) field in DocType 'Purchase
|
||||
#. Invoice'
|
||||
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
|
||||
msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Purchase Receipt is created separately."
|
||||
msgstr ""
|
||||
msgstr "Ako je odabrano, ažurira se inventar; unosi zaliha i knjigoovodstva se kreiraju zajedno. Ostavi neodabrano ako Kupovni Račun kreira zasebno."
|
||||
|
||||
#: erpnext/public/js/setup_wizard.js:56
|
||||
msgid "If checked, we will create demo data for you to explore the system. This demo data can be erased later."
|
||||
@@ -25255,7 +25255,7 @@ msgstr "Nevažeća Tvrtka za transakcije između tvrtki."
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:972
|
||||
msgid "Invalid Configuration"
|
||||
msgstr ""
|
||||
msgstr "Nevažeća Konfiguracija"
|
||||
|
||||
#: erpnext/accounts/services/taxes.py:294
|
||||
#: erpnext/assets/doctype/asset/asset.py:361
|
||||
@@ -25273,12 +25273,12 @@ msgstr "Nevažeći Datum Dostave"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:110
|
||||
msgid "Invalid Disassembly Item"
|
||||
msgstr ""
|
||||
msgstr "Nevažeći Artikala za Rastavljanje"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:76
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:125
|
||||
msgid "Invalid Disassembly Quantity"
|
||||
msgstr ""
|
||||
msgstr "Nevažeća Količina za Rastavljanje"
|
||||
|
||||
#: erpnext/selling/page/point_of_sale/pos_item_cart.js:414
|
||||
msgid "Invalid Discount"
|
||||
@@ -26144,7 +26144,7 @@ msgstr "Je Fantomska Stavka"
|
||||
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
|
||||
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
|
||||
msgid "Is Product Bundle"
|
||||
msgstr ""
|
||||
msgstr "Je Paket Artikala"
|
||||
|
||||
#. Label of the po_required (Select) field in DocType 'Buying Settings'
|
||||
#: erpnext/buying/doctype/buying_settings/buying_settings.json
|
||||
@@ -27652,7 +27652,7 @@ msgstr "Detalji Težine Artikla"
|
||||
#. Name of a report
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.json
|
||||
msgid "Item Where Used"
|
||||
msgstr ""
|
||||
msgstr "Gdje se koristi Artikal"
|
||||
|
||||
#. Label of a Link in the Buying Workspace
|
||||
#. Name of a report
|
||||
@@ -27775,7 +27775,7 @@ msgstr "Artikal {0} dodan je više puta pod isti nadređeni artikal {1} u redovi
|
||||
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.js:54
|
||||
msgid "Item {0} already has an active Product Bundle ({1}). Submitting this will create a new version and deactivate {1}."
|
||||
msgstr ""
|
||||
msgstr "{0} već ima aktivan Paket Artikala ({1}). Podnošenjem ovog stvorit će se nova verzija i deaktivirati {1}."
|
||||
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:119
|
||||
msgid "Item {0} cannot be added as a sub-assembly of itself"
|
||||
@@ -28106,7 +28106,7 @@ msgstr "Artikal Radne Kartice"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:925
|
||||
msgid "Job Card On Hold"
|
||||
msgstr ""
|
||||
msgstr "Radni Nalog je na čekanju"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
|
||||
@@ -30309,7 +30309,7 @@ msgstr "Usklađeno"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:57
|
||||
msgid "Matched Field"
|
||||
msgstr ""
|
||||
msgstr "Usklađeno polje"
|
||||
|
||||
#. Label of the matched_transaction_rule (Link) field in DocType 'Bank
|
||||
#. Transaction'
|
||||
@@ -30928,7 +30928,7 @@ msgstr "Metar/Sekunda"
|
||||
|
||||
#: erpnext/manufacturing/doctype/workstation/workstation.py:546
|
||||
msgid "Method {0} is not allowed to be run on a Job Card."
|
||||
msgstr ""
|
||||
msgstr "Metodu {0} nije dopušteno pokretati na Radnom Nalogu."
|
||||
|
||||
#. Name of a UOM
|
||||
#: erpnext/setup/setup_wizard/data/uom_data.json
|
||||
@@ -32258,13 +32258,13 @@ msgstr "Newton"
|
||||
#. Label of the next_billing_period_end (Date) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Next Billing Period End"
|
||||
msgstr ""
|
||||
msgstr "Sljedeći Perioda Fakturiranja Završava"
|
||||
|
||||
#. Label of the next_billing_period_start (Date) field in DocType
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Next Billing Period Start"
|
||||
msgstr ""
|
||||
msgstr "Sljedeći Perioda Fakturiranja Počinje"
|
||||
|
||||
#. Label of the next_depreciation_date (Date) field in DocType 'Asset'
|
||||
#: erpnext/assets/doctype/asset/asset.json
|
||||
@@ -33476,7 +33476,7 @@ msgstr "Samo jedna operacija može imati odabranu opciju 'Je li Gotov Proizvod'
|
||||
#. Description of the 'Is Active' (Check) field in DocType 'Product Bundle'
|
||||
#: erpnext/selling/doctype/product_bundle/product_bundle.json
|
||||
msgid "Only one version of a Product Bundle can be active at a time for a given Parent Item. Activating a version deactivates the previously active one."
|
||||
msgstr ""
|
||||
msgstr "Samo jedna verzija Paketa Artikala može biti aktivna u datom trenutku za dati Nadređeni Artikal. Aktiviranje verzije deaktivira prethodno aktivnu verziju."
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:719
|
||||
msgid "Only one {0} entry can be created against the Work Order {1}"
|
||||
@@ -39083,7 +39083,7 @@ msgstr "Vremenska oznaka knjiženja mora biti nakon {0}"
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Postpaid (bill at period end)"
|
||||
msgstr ""
|
||||
msgstr "Naknadno Plaćanje (faktura na kraju razdoblja)"
|
||||
|
||||
#. Description of a DocType
|
||||
#: erpnext/crm/doctype/opportunity/opportunity.json
|
||||
@@ -39186,7 +39186,7 @@ msgstr "Preferirana e-pošta"
|
||||
#. 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Prepaid (bill at period start)"
|
||||
msgstr ""
|
||||
msgstr "Unaprijed Plaćeno (faktura na početku razdoblja)"
|
||||
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:34
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:51
|
||||
@@ -40174,7 +40174,7 @@ msgstr "Stanje Paketa Proizvoda"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:278
|
||||
msgid "Product Bundle Component"
|
||||
msgstr ""
|
||||
msgstr "Komponenta Paketa Artikala"
|
||||
|
||||
#. Label of the product_bundle_help (HTML) field in DocType 'POS Invoice'
|
||||
#. Label of the product_bundle_help (HTML) field in DocType 'Sales Invoice'
|
||||
@@ -40199,7 +40199,7 @@ msgstr "Artikal Paketa Proizvoda"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:305
|
||||
msgid "Product Bundle Parent"
|
||||
msgstr ""
|
||||
msgstr "Nadređeni Paket Artikala"
|
||||
|
||||
#. Description of the 'Product Bundle' (Link) field in DocType 'Purchase
|
||||
#. Invoice Item'
|
||||
@@ -40213,15 +40213,15 @@ msgstr ""
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.json
|
||||
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
|
||||
msgid "Product Bundle version this row was packed from"
|
||||
msgstr ""
|
||||
msgstr "Verzija Paketa Artikala iz koje je ovaj red preuzet iz"
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:453
|
||||
msgid "Product Bundle {0} is disabled and cannot be used in transactions."
|
||||
msgstr ""
|
||||
msgstr "Paket Artikala {0} je onemogućen i ne može se koristiti u transakcijama."
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:450
|
||||
msgid "Product Bundle {0} is not submitted"
|
||||
msgstr ""
|
||||
msgstr "Paket Artikala {0} nije podnešen"
|
||||
|
||||
#. Label of the product_discount_scheme_section (Section Break) field in
|
||||
#. DocType 'Pricing Rule'
|
||||
@@ -43881,7 +43881,7 @@ msgstr "Osvježite Plaid Link"
|
||||
#. Option for the 'Status' (Select) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Refunded"
|
||||
msgstr ""
|
||||
msgstr "Povraćeno"
|
||||
|
||||
#: erpnext/stock/reorder_item.py:390
|
||||
msgid "Regards,"
|
||||
@@ -43992,7 +43992,7 @@ msgstr "Povezano"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:50
|
||||
msgid "Related Item"
|
||||
msgstr ""
|
||||
msgstr "Povezani Artikal"
|
||||
|
||||
#. Label of the relation (Data) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
@@ -46064,7 +46064,7 @@ msgstr "Red #{0}: Gotov Proizvod artikla nije navedena zaservisni artikal {1}"
|
||||
|
||||
#: erpnext/manufacturing/doctype/bom/bom.py:371
|
||||
msgid "Row #{0}: Finished Good Item {1} cannot be added in the Secondary Items table."
|
||||
msgstr ""
|
||||
msgstr "Red #{0}: Artikal Gotovog Proizvoda {1} ne može se dodati u tablicu Sekundarnih Artikala."
|
||||
|
||||
#: erpnext/buying/doctype/purchase_order/services/subcontracting.py:28
|
||||
#: erpnext/selling/doctype/sales_order/services/subcontracting.py:27
|
||||
@@ -46155,7 +46155,7 @@ msgstr "Red #{0}: Artikal {1} nije artikal na zalihama"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:106
|
||||
msgid "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be added to this disassembly."
|
||||
msgstr ""
|
||||
msgstr "Red #{0}: Artikal {1} nije dio unosa izvornog proizvođača i ne može se dodati ovom rastavljanju."
|
||||
|
||||
#: erpnext/controllers/subcontracting_inward_controller.py:79
|
||||
msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead."
|
||||
@@ -46167,7 +46167,7 @@ msgstr "Red #{0}: Artikla {1} se ne slaže. Promjena koda artikla nije dozvoljen
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:115
|
||||
msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows."
|
||||
msgstr ""
|
||||
msgstr "Red #{0}: Količina artikla {1} ({2} u jedinici zaliha) ne odgovara količini izvedeno iz izvora ({3}). Ne mijenjaj jedinicu, faktor konverzije ili količinu redova za rastavljanje."
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:780
|
||||
msgid "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher"
|
||||
@@ -46233,7 +46233,7 @@ msgstr "Red #{0}: Postotnii Gubitka Procesa treba da bude manji od 100% za {1} a
|
||||
|
||||
#: erpnext/stock/doctype/packed_item/packed_item.py:213
|
||||
msgid "Row #{0}: Product Bundle {1} is disabled and cannot be used in transactions."
|
||||
msgstr ""
|
||||
msgstr "Red #{0}: Paket Artikal {1} je onemogućen i ne može se koristiti u transakcijama."
|
||||
|
||||
#: erpnext/public/js/utils/barcode_scanner.js:425
|
||||
msgid "Row #{0}: Qty increased by {1}"
|
||||
@@ -49490,7 +49490,7 @@ msgstr "Serijski i Šaržni Paket {0} nije podnešen"
|
||||
|
||||
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2251
|
||||
msgid "Serial and Batch Bundle {0} is submitted and its entries cannot be modified."
|
||||
msgstr ""
|
||||
msgstr "Serijski i Šaržni Paket {0} je podnešen i njegovi unosi se ne mogu mijenjati."
|
||||
|
||||
#. Label of the section_break_45 (Section Break) field in DocType
|
||||
#. 'Subcontracting Receipt Item'
|
||||
@@ -52668,7 +52668,7 @@ msgstr "Podizvođačka Dostava"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:362
|
||||
msgid "Subcontracting Finished Good"
|
||||
msgstr ""
|
||||
msgstr "Podizvođački Gotov Proizvod"
|
||||
|
||||
#. Label of the subcontracting_inward_tab (Tab Break) field in DocType 'Selling
|
||||
#. Settings'
|
||||
@@ -52852,7 +52852,7 @@ msgstr "Podizvođački Prodajni Nalog"
|
||||
|
||||
#: erpnext/stock/report/item_where_used/item_where_used.py:336
|
||||
msgid "Subcontracting Service Item"
|
||||
msgstr ""
|
||||
msgstr "Podizvođački Uslužni Artikal"
|
||||
|
||||
#. Label of the subcontract (Tab Break) field in DocType 'Buying Settings'
|
||||
#: erpnext/buying/doctype/buying_settings/buying_settings.json
|
||||
@@ -52900,7 +52900,7 @@ msgstr "Podnesi Ponudu"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:1570
|
||||
msgid "Submitted Job Card cannot be processed."
|
||||
msgstr ""
|
||||
msgstr "Podnešeni Radni Nalog ne može biti obrađen."
|
||||
|
||||
#. Label of the subscription_section (Section Break) field in DocType 'Payment
|
||||
#. Request'
|
||||
@@ -61720,7 +61720,7 @@ msgstr "Ne možete podnijeti nalog bez plaćanja."
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:968
|
||||
msgid "You cannot update stock for a Debit Note. A Debit Note is a financial document that should not affect inventory. Please disable 'Update Stock'."
|
||||
msgstr ""
|
||||
msgstr "Ne možete ažurirati zalihe za Terećenje. Terećenje je financijski dokument koji ne bi trebao utjecati na zalihe. Onemogući opciju 'Ažuriraj Zalihe'."
|
||||
|
||||
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:106
|
||||
msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}"
|
||||
@@ -62834,7 +62834,7 @@ msgstr "{0}, završi operaciju {1} prije operacije {2}."
|
||||
|
||||
#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:61
|
||||
msgid "{0}, {1} or {2} are the only allowed options."
|
||||
msgstr ""
|
||||
msgstr "{0}, {1} ili {2} su jedine dopuštene opcije."
|
||||
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:525
|
||||
msgid "{0}: Child table (auto-deleted with parent)"
|
||||
|
||||
@@ -373,13 +373,6 @@ class BOM(WebsiteGenerator):
|
||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||
)
|
||||
|
||||
if not item.qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Quantity should be greater than 0 for {1} Item {2}").format(
|
||||
item.idx, item.secondary_item_type, get_link_to_form("Item", item.item_code)
|
||||
)
|
||||
)
|
||||
|
||||
if item.process_loss_per >= 100:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Process Loss Percentage should be less than 100% for {1} Item {2}").format(
|
||||
|
||||
@@ -138,8 +138,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -218,7 +217,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-03 16:10:18.474377",
|
||||
"modified": "2026-06-16 16:51:40.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Secondary Item",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"""BOM explosion helpers for Production Plan material planning."""
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.query_builder.functions import IfNull, Max, Min, Sum
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.services.planning_queries import get_uom_conversion_factor
|
||||
|
||||
@@ -38,22 +38,25 @@ def _exploded_items_query(company, bom_no, include_non_stock_items, planned_qty)
|
||||
|
||||
|
||||
def _exploded_item_columns(bei, bom, item, item_default, item_uom, planned_qty):
|
||||
# only item_code/stock_uom are grouped; the rest are functionally dependent on the grouped item
|
||||
# or arbitrary per BOM Item on MySQL -> Max() keeps the GROUP BY valid on postgres with the same
|
||||
# value MySQL picked.
|
||||
return [
|
||||
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
|
||||
item.item_name,
|
||||
item.name.as_("item_code"),
|
||||
bei.description,
|
||||
Max(item.item_name).as_("item_name"),
|
||||
Max(item.name).as_("item_code"),
|
||||
Max(bei.description).as_("description"),
|
||||
bei.stock_uom,
|
||||
item.min_order_qty,
|
||||
bei.source_warehouse,
|
||||
item.default_material_request_type,
|
||||
item.min_order_qty,
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
bom.name.as_("main_bom"),
|
||||
Max(item.min_order_qty).as_("min_order_qty"),
|
||||
Max(bei.source_warehouse).as_("source_warehouse"),
|
||||
Max(item.default_material_request_type).as_("default_material_request_type"),
|
||||
Max(item.min_order_qty).as_("min_order_qty"),
|
||||
Max(item_default.default_warehouse).as_("default_warehouse"),
|
||||
Max(item.purchase_uom).as_("purchase_uom"),
|
||||
Max(item_uom.conversion_factor).as_("conversion_factor"),
|
||||
Max(item.safety_stock).as_("safety_stock"),
|
||||
Max(bom.item).as_("main_bom_item"),
|
||||
Max(bom.name).as_("main_bom"),
|
||||
]
|
||||
|
||||
|
||||
@@ -106,30 +109,34 @@ def _subitems_query(company, bom_no, include_non_stock_items, parent_qty, planne
|
||||
.select(*_subitem_columns(bom_item, bom, item, item_default, item_uom, parent_qty, planned_qty))
|
||||
.where(_subitem_filter(bom_item, bom, item, bom_no, include_non_stock_items))
|
||||
.groupby(bom_item.item_code)
|
||||
.orderby(bom_item.idx)
|
||||
# idx is not grouped; Min() preserves the original ordering and is valid on postgres
|
||||
.orderby(Min(bom_item.idx))
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def _subitem_columns(bom_item, bom, item, item_default, item_uom, parent_qty, planned_qty):
|
||||
qty = IfNull(parent_qty * Sum(bom_item.stock_qty / IfNull(bom.quantity, 1)) * planned_qty, 0).as_("qty")
|
||||
# only item_code is grouped; the rest are functionally dependent on the grouped item (item
|
||||
# attributes) or arbitrary per BOM Item on MySQL -> Max() keeps the GROUP BY valid on postgres
|
||||
# while returning the same value MySQL picked.
|
||||
return [
|
||||
bom_item.item_code,
|
||||
item.default_material_request_type,
|
||||
item.item_name,
|
||||
Max(item.default_material_request_type).as_("default_material_request_type"),
|
||||
Max(item.item_name).as_("item_name"),
|
||||
qty,
|
||||
item.is_sub_contracted_item.as_("is_sub_contracted"),
|
||||
bom_item.source_warehouse,
|
||||
item.default_bom.as_("default_bom"),
|
||||
bom_item.description.as_("description"),
|
||||
bom_item.stock_uom.as_("stock_uom"),
|
||||
item.min_order_qty.as_("min_order_qty"),
|
||||
item.safety_stock.as_("safety_stock"),
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
bom.item.as_("main_bom_item"),
|
||||
bom.name.as_("main_bom"),
|
||||
bom_item.is_phantom_item,
|
||||
Max(item.is_sub_contracted_item).as_("is_sub_contracted"),
|
||||
Max(bom_item.source_warehouse).as_("source_warehouse"),
|
||||
Max(item.default_bom).as_("default_bom"),
|
||||
Max(bom_item.description).as_("description"),
|
||||
Max(bom_item.stock_uom).as_("stock_uom"),
|
||||
Max(item.min_order_qty).as_("min_order_qty"),
|
||||
Max(item.safety_stock).as_("safety_stock"),
|
||||
Max(item_default.default_warehouse).as_("default_warehouse"),
|
||||
Max(item.purchase_uom).as_("purchase_uom"),
|
||||
Max(item_uom.conversion_factor).as_("conversion_factor"),
|
||||
Max(bom.item).as_("main_bom_item"),
|
||||
Max(bom.name).as_("main_bom"),
|
||||
Max(bom_item.is_phantom_item).as_("is_phantom_item"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"""Sub-assembly resolution helpers for Production Plan."""
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.query_builder.functions import IfNull, Max, Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children
|
||||
@@ -184,24 +184,27 @@ def _sub_assembly_rm_query(company, bom_no, include_non_stock_items, planned_qty
|
||||
|
||||
|
||||
def _sub_assembly_rm_columns(bei, bom, item, item_default, item_uom, planned_qty):
|
||||
# only item_code/stock_uom are grouped; every other column is functionally dependent on the
|
||||
# grouped item (item attributes) or arbitrary per BOM Item on MySQL -> Max() keeps the GROUP BY
|
||||
# valid on postgres while returning the same value MySQL picked.
|
||||
return [
|
||||
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
|
||||
item.item_name,
|
||||
item.name.as_("item_code"),
|
||||
bei.description,
|
||||
Max(item.item_name).as_("item_name"),
|
||||
Max(item.name).as_("item_code"),
|
||||
Max(bei.description).as_("description"),
|
||||
bei.stock_uom,
|
||||
bei.is_phantom_item,
|
||||
bei.bom_no,
|
||||
item.min_order_qty,
|
||||
bei.source_warehouse,
|
||||
item.default_material_request_type,
|
||||
item.min_order_qty,
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
bom.name.as_("main_bom"),
|
||||
Max(bei.is_phantom_item).as_("is_phantom_item"),
|
||||
Max(bei.bom_no).as_("bom_no"),
|
||||
Max(item.min_order_qty).as_("min_order_qty"),
|
||||
Max(bei.source_warehouse).as_("source_warehouse"),
|
||||
Max(item.default_material_request_type).as_("default_material_request_type"),
|
||||
Max(item.min_order_qty).as_("min_order_qty"),
|
||||
Max(item_default.default_warehouse).as_("default_warehouse"),
|
||||
Max(item.purchase_uom).as_("purchase_uom"),
|
||||
Max(item_uom.conversion_factor).as_("conversion_factor"),
|
||||
Max(item.safety_stock).as_("safety_stock"),
|
||||
Max(bom.item).as_("main_bom_item"),
|
||||
Max(bom.name).as_("main_bom"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -56,11 +56,12 @@ def _item_master_details(item):
|
||||
|
||||
|
||||
def _item_is_alive(item_table):
|
||||
return (
|
||||
item_table.end_of_life.isnull()
|
||||
| (item_table.end_of_life == "0000-00-00")
|
||||
| (item_table.end_of_life > nowdate())
|
||||
)
|
||||
# "not set" end_of_life is NULL on postgres (the MariaDB zero-date '0000-00-00' is an invalid
|
||||
# date constant there), so only add the zero-date term on MariaDB.
|
||||
is_alive = item_table.end_of_life.isnull() | (item_table.end_of_life > nowdate())
|
||||
if frappe.db.db_type != "postgres":
|
||||
is_alive |= item_table.end_of_life == "0000-00-00"
|
||||
return is_alive
|
||||
|
||||
|
||||
def _default_bom_for_item(item, project):
|
||||
|
||||
@@ -11,6 +11,7 @@ are called from other modules.
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import CombineDatetime
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
@@ -268,13 +269,17 @@ class OperationsService:
|
||||
self.doc.actual_end_date = max(end_dates)
|
||||
|
||||
def _set_dates_from_stock_entries(self):
|
||||
data = frappe.get_all(
|
||||
"Stock Entry",
|
||||
fields=[{"TIMESTAMP": ["posting_date", "posting_time"], "as": "posting_datetime"}],
|
||||
filters={
|
||||
"work_order": self.doc.name,
|
||||
"purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]),
|
||||
},
|
||||
# {"TIMESTAMP": [...]} renders MySQL's TIMESTAMP(date, time), invalid on postgres; use the
|
||||
# portable CombineDatetime via query builder instead.
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
data = (
|
||||
frappe.qb.from_(se)
|
||||
.select(CombineDatetime(se.posting_date, se.posting_time).as_("posting_datetime"))
|
||||
.where(
|
||||
(se.work_order == self.doc.name)
|
||||
& (se.purpose.isin(["Material Transfer for Manufacture", "Manufacture"]))
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if not data:
|
||||
return
|
||||
|
||||
@@ -19,6 +19,7 @@ from erpnext.manufacturing.doctype.work_order.services.reservation import (
|
||||
get_consumed_qty,
|
||||
get_row_wise_serial_batch,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.services.status import StatusService
|
||||
from erpnext.stock.utils import get_bin, get_latest_stock_qty
|
||||
|
||||
|
||||
@@ -146,6 +147,38 @@ class RequiredItemsService:
|
||||
row, transferred_qty, row_wise_serial_batch
|
||||
)
|
||||
|
||||
self.recompute_material_transferred_for_manufacturing(transferred_items)
|
||||
|
||||
def recompute_material_transferred_for_manufacturing(self, transferred_items):
|
||||
"""Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty."""
|
||||
# When fg_completed_qty > 0 (direct stock entries, excess transfer), preserve the
|
||||
# SUM(fg_completed_qty) approach so excess-transfer tracking works correctly.
|
||||
sum_fg_completed_qty = StatusService(self.doc).get_transferred_or_manufactured_qty(
|
||||
"Material Transfer for Manufacture", "material_transferred_for_manufacturing"
|
||||
)
|
||||
if sum_fg_completed_qty:
|
||||
self.doc.db_set("material_transferred_for_manufacturing", sum_fg_completed_qty)
|
||||
return
|
||||
|
||||
# Pick list flow sets fg_completed_qty=0; use min-fraction of actual item transfers
|
||||
# so partial availability does not prematurely mark the work order as fully transferred.
|
||||
required_by_item = {}
|
||||
for row in self.doc.required_items:
|
||||
if not row.include_item_in_manufacturing or flt(row.required_qty) <= 0:
|
||||
continue
|
||||
required_by_item[row.item_code] = required_by_item.get(row.item_code, 0.0) + flt(row.required_qty)
|
||||
|
||||
if not required_by_item:
|
||||
return
|
||||
|
||||
min_fraction = min(
|
||||
flt(transferred_items.get(item_code) or 0) / required_qty
|
||||
for item_code, required_qty in required_by_item.items()
|
||||
)
|
||||
min_fraction = min(min_fraction, 1.0)
|
||||
material_transferred = min_fraction * flt(self.doc.qty)
|
||||
self.doc.db_set("material_transferred_for_manufacturing", material_transferred)
|
||||
|
||||
def update_returned_qty(self):
|
||||
returned_dict = self._material_transfer_qty_by_item(is_return=1)
|
||||
for row in self.doc.required_items:
|
||||
@@ -158,7 +191,13 @@ class RequiredItemsService:
|
||||
frappe.qb.from_(ste)
|
||||
.inner_join(ste_child)
|
||||
.on(ste_child.parent == ste.name)
|
||||
.select(ste_child.item_code, ste_child.original_item, fn.Sum(ste_child.transfer_qty).as_("qty"))
|
||||
# original_item is arbitrary per grouped item_code on MySQL -> Max() keeps the GROUP BY valid
|
||||
# on postgres while returning the same value (it is only used as a dict key fallback below)
|
||||
.select(
|
||||
ste_child.item_code,
|
||||
fn.Max(ste_child.original_item).as_("original_item"),
|
||||
fn.Sum(ste_child.transfer_qty).as_("qty"),
|
||||
)
|
||||
.where(self._material_transfer_filter(ste, is_return))
|
||||
.groupby(ste_child.item_code)
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user