mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-29 15:38:18 +00:00
Compare commits
462 Commits
ledger_rep
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
282a30144a | ||
|
|
c6c7d7832a | ||
|
|
edb254e43e | ||
|
|
61efb2bb39 | ||
|
|
856ec08484 | ||
|
|
31005c5984 | ||
|
|
14ce2337df | ||
|
|
e4d3235b9c | ||
|
|
5ccf4a1783 | ||
|
|
1ef9f7f8fd | ||
|
|
108a91d788 | ||
|
|
1f1c01d88e | ||
|
|
90339511aa | ||
|
|
a9b761f862 | ||
|
|
4ec824d71e | ||
|
|
bb9dd7b8cc | ||
|
|
b291835ccd | ||
|
|
2e930eb97b | ||
|
|
00dadc1a89 | ||
|
|
b4ceda6f2c | ||
|
|
7ea73d8265 | ||
|
|
9e760e54a5 | ||
|
|
81d8f257aa | ||
|
|
dbb572eec1 | ||
|
|
2b453219fc | ||
|
|
ca44a31420 | ||
|
|
4620025dcd | ||
|
|
5df9a8ab99 | ||
|
|
731822efac | ||
|
|
a24d7e8ecd | ||
|
|
729ce1dc50 | ||
|
|
31dd32dcdf | ||
|
|
6850019649 | ||
|
|
2788739c1e | ||
|
|
9f77793f16 | ||
|
|
a0f17f8e73 | ||
|
|
dc5bff9008 | ||
|
|
3bc74f219a | ||
|
|
886281f81a | ||
|
|
9eede907f8 | ||
|
|
6f5fea6b52 | ||
|
|
3ca3707603 | ||
|
|
d7bf73cffa | ||
|
|
8de0fe78ea | ||
|
|
87f1f6e15c | ||
|
|
7afe3cccd6 | ||
|
|
30d68a31e0 | ||
|
|
d79e561248 | ||
|
|
03abde6993 | ||
|
|
c7370e214f | ||
|
|
3125bc8a16 | ||
|
|
7bbd70a7e2 | ||
|
|
acb5eeb281 | ||
|
|
b8838bd9b9 | ||
|
|
d42ee40b17 | ||
|
|
19f1e089bd | ||
|
|
bddffbb04f | ||
|
|
2b88c13002 | ||
|
|
988b680244 | ||
|
|
4d74724a34 | ||
|
|
b6fe43b63a | ||
|
|
d39fa07620 | ||
|
|
34a4efbc59 | ||
|
|
d34ebcfe9a | ||
|
|
7b5f026b8b | ||
|
|
630009e28f | ||
|
|
9727bcc281 | ||
|
|
3c96ccf990 | ||
|
|
74791fd619 | ||
|
|
ee3f3fc799 | ||
|
|
7bcd5f5bcb | ||
|
|
af027cf980 | ||
|
|
b3e2e8e123 | ||
|
|
43408c1b6d | ||
|
|
609f789807 | ||
|
|
cacc90c224 | ||
|
|
a76c3b5f3f | ||
|
|
ae4e55bf3a | ||
|
|
07ba0281f7 | ||
|
|
d5babf4237 | ||
|
|
9e86a02a91 | ||
|
|
e28382afc1 | ||
|
|
1f2d7da426 | ||
|
|
f996f71d16 | ||
|
|
f51c9f578c | ||
|
|
ba90a6a4bf | ||
|
|
947ab72441 | ||
|
|
5245c54de9 | ||
|
|
e39638bdcd | ||
|
|
839b79ffd0 | ||
|
|
2f80c4dee5 | ||
|
|
4f690affc9 | ||
|
|
413fef332a | ||
|
|
63d547fb4a | ||
|
|
3221c89218 | ||
|
|
b634ba7f54 | ||
|
|
49885f8eae | ||
|
|
71d1205f53 | ||
|
|
15d488b9aa | ||
|
|
bdece96510 | ||
|
|
b1de82ddad | ||
|
|
5dacfd5cda | ||
|
|
e609a6a038 | ||
|
|
1f26feca2e | ||
|
|
51354c894a | ||
|
|
53d8e32961 | ||
|
|
dc5f2d35ac | ||
|
|
d2b0e0fa2d | ||
|
|
28ea3ddd51 | ||
|
|
81a6e42620 | ||
|
|
015fd4a05b | ||
|
|
5e977cf5a7 | ||
|
|
e92af10f14 | ||
|
|
d1d01482df | ||
|
|
8aa3157307 | ||
|
|
b84c8ff960 | ||
|
|
7db9bcaeac | ||
|
|
a9cc23f242 | ||
|
|
575fb43f9c | ||
|
|
2cd915fb4d | ||
|
|
b60bd17d1d | ||
|
|
c6634d03ad | ||
|
|
fa3c882656 | ||
|
|
e58d73525b | ||
|
|
ebc0ed8a31 | ||
|
|
2beb485d77 | ||
|
|
4b6cae156e | ||
|
|
65dc3505c4 | ||
|
|
079ec864de | ||
|
|
7c4aecf834 | ||
|
|
d6980a9493 | ||
|
|
d1ae0d784e | ||
|
|
2cbab9b875 | ||
|
|
22d38c2af4 | ||
|
|
7a5d958aba | ||
|
|
27195c7c96 | ||
|
|
54cb99eae3 | ||
|
|
7926bf066a | ||
|
|
a984aaae36 | ||
|
|
bb0cf3bf78 | ||
|
|
b92f8bc514 | ||
|
|
1a7b09e576 | ||
|
|
ee418ffefa | ||
|
|
98cbb7e900 | ||
|
|
867aa9dd86 | ||
|
|
4f398d8edc | ||
|
|
93b70b98bb | ||
|
|
5a284df51d | ||
|
|
784a62eae8 | ||
|
|
f3be246df3 | ||
|
|
9fdeb5f826 | ||
|
|
e216547084 | ||
|
|
92b1f314ef | ||
|
|
6e0dd271f5 | ||
|
|
50c92034ba | ||
|
|
9661c1d081 | ||
|
|
6f8d6c81d5 | ||
|
|
70b1077286 | ||
|
|
bcae2810b9 | ||
|
|
303c52f134 | ||
|
|
187c74ae09 | ||
|
|
0589fa7f3e | ||
|
|
cb197fd01f | ||
|
|
cfcc24a341 | ||
|
|
4f29908aa9 | ||
|
|
e85142f996 | ||
|
|
78b0b3047c | ||
|
|
751a0a93ef | ||
|
|
c9088f4955 | ||
|
|
ab1cca0c40 | ||
|
|
8f82219986 | ||
|
|
f5e53c9661 | ||
|
|
b998933ef0 | ||
|
|
ac26622d6e | ||
|
|
976e35d547 | ||
|
|
1319ce4bc1 | ||
|
|
614a8f106d | ||
|
|
888da9c1ab | ||
|
|
9f8448bb15 | ||
|
|
0adfebee85 | ||
|
|
9368485594 | ||
|
|
8bcfd2429d | ||
|
|
42c8ce68ad | ||
|
|
0b113815c9 | ||
|
|
7378eff0ca | ||
|
|
f0a34aa587 | ||
|
|
e1fc239f3d | ||
|
|
8abbece7c4 | ||
|
|
f8b923edfe | ||
|
|
de54c0b41f | ||
|
|
021d077808 | ||
|
|
a10a15b2c3 | ||
|
|
c6a2d86ba6 | ||
|
|
c59d3bda7e | ||
|
|
dc5cd93bf0 | ||
|
|
d4b21f532a | ||
|
|
60694838f3 | ||
|
|
079b86044e | ||
|
|
0b1c3208a0 | ||
|
|
2f7e6230a6 | ||
|
|
1f4e1811de | ||
|
|
948556def5 | ||
|
|
c9b143b509 | ||
|
|
7d41805d0e | ||
|
|
54d7b742ab | ||
|
|
2aff3ced52 | ||
|
|
7497f0ca52 | ||
|
|
42f2b965d1 | ||
|
|
70405d9026 | ||
|
|
4c5e9c005c | ||
|
|
789469f784 | ||
|
|
643163cc24 | ||
|
|
e98881cc56 | ||
|
|
ab36dfe5ef | ||
|
|
3892f2a1fa | ||
|
|
abf624b994 | ||
|
|
05ceb25abc | ||
|
|
5cb86b1367 | ||
|
|
a515a399cf | ||
|
|
a87e7fde03 | ||
|
|
a7078e5702 | ||
|
|
92b8768ae2 | ||
|
|
a2c2b8b5ad | ||
|
|
2dfd7472a4 | ||
|
|
ab91abb144 | ||
|
|
6f00a87a9c | ||
|
|
ab01b8fc63 | ||
|
|
10b7c3e0ac | ||
|
|
fc57fb22a5 | ||
|
|
fad00fedaa | ||
|
|
b7699012b2 | ||
|
|
cf57cb73f0 | ||
|
|
a2a642a5b1 | ||
|
|
cc3f4bb0b0 | ||
|
|
5567a41a2a | ||
|
|
bcb7617630 | ||
|
|
fe0036e707 | ||
|
|
b1c9716fec | ||
|
|
c100b68ad1 | ||
|
|
085ef5df8f | ||
|
|
ba28f6bf73 | ||
|
|
919abd2c03 | ||
|
|
31803a2f53 | ||
|
|
a4c34ad759 | ||
|
|
3be633f6f1 | ||
|
|
1eb8b0ceef | ||
|
|
bf4d696f17 | ||
|
|
7cd6bfc8a4 | ||
|
|
56f561cdaa | ||
|
|
3b36ce560c | ||
|
|
09776e9a5a | ||
|
|
913001e44c | ||
|
|
5057a4e1b2 | ||
|
|
afea92c01d | ||
|
|
a764d16674 | ||
|
|
854e37c05c | ||
|
|
2d58e845e6 | ||
|
|
6db1b3fef8 | ||
|
|
c14a2d73bf | ||
|
|
ff1d040a6e | ||
|
|
225e56cbca | ||
|
|
82306c5a85 | ||
|
|
a7b5e2565b | ||
|
|
e3a7f2f2e7 | ||
|
|
b9e96f62ef | ||
|
|
51137cf001 | ||
|
|
cb9c12d495 | ||
|
|
3662a6a41d | ||
|
|
93ec962aae | ||
|
|
90baa38f64 | ||
|
|
2ce07865d3 | ||
|
|
6b847cdb62 | ||
|
|
df13a4cc2f | ||
|
|
700dc05b55 | ||
|
|
ce9f10b387 | ||
|
|
3eba6bf3dd | ||
|
|
779dd2d798 | ||
|
|
6f99e9959d | ||
|
|
1fd7ba7c88 | ||
|
|
febdf4c61e | ||
|
|
c798a68e5d | ||
|
|
663372a23e | ||
|
|
7fed467354 | ||
|
|
9a43acb65c | ||
|
|
54eedaeeff | ||
|
|
b970eb8b15 | ||
|
|
352b82bc0b | ||
|
|
878f76455b | ||
|
|
5c9e8fb4da | ||
|
|
5ea131c763 | ||
|
|
8d6e79a16f | ||
|
|
7ac7a40aec | ||
|
|
3342fa1440 | ||
|
|
fe4eb8e6db | ||
|
|
a819b1feaa | ||
|
|
8205000195 | ||
|
|
4387e09ee8 | ||
|
|
3be6390a12 | ||
|
|
c33b9922a5 | ||
|
|
c25ef357a5 | ||
|
|
09e64594db | ||
|
|
1663c7983e | ||
|
|
1f5d7072e7 | ||
|
|
37fb6d12da | ||
|
|
94c64235d4 | ||
|
|
8b89cc69c1 | ||
|
|
8a5f7ec4d7 | ||
|
|
5401cf9647 | ||
|
|
494ac04f8e | ||
|
|
2a6be127ef | ||
|
|
4de180feee | ||
|
|
852596dbe6 | ||
|
|
84dff3601a | ||
|
|
b565b67cc7 | ||
|
|
12634d0bd6 | ||
|
|
fda57420a5 | ||
|
|
bf0cfc4704 | ||
|
|
4af0365719 | ||
|
|
30d2a2bce5 | ||
|
|
e82f993d94 | ||
|
|
9e8304c16f | ||
|
|
efa00fd9c6 | ||
|
|
e90d2c2cfe | ||
|
|
30735b339d | ||
|
|
7eb7a7fa36 | ||
|
|
0e68c433db | ||
|
|
95da0913f6 | ||
|
|
5f539619bc | ||
|
|
5d16e4aefe | ||
|
|
ac4ee1b005 | ||
|
|
9ebdd4d79a | ||
|
|
dc02cb8119 | ||
|
|
eb1e36ca22 | ||
|
|
f8cf1983a4 | ||
|
|
8338d1d5b4 | ||
|
|
62c553edeb | ||
|
|
37fc2068b5 | ||
|
|
15d2ca0c06 | ||
|
|
4d34eefde4 | ||
|
|
73112fa3c9 | ||
|
|
6119d4384a | ||
|
|
4409dadfe2 | ||
|
|
3a4d8a7bbd | ||
|
|
578ca230fe | ||
|
|
d07f9d746a | ||
|
|
48b49cdea4 | ||
|
|
168baae118 | ||
|
|
693b073088 | ||
|
|
2c5b5d4117 | ||
|
|
f704ade417 | ||
|
|
d8f55923c9 | ||
|
|
a626372d66 | ||
|
|
325b20491a | ||
|
|
d84aeef0bb | ||
|
|
faa6a3dec0 | ||
|
|
d2cab83003 | ||
|
|
91c7e3d5f3 | ||
|
|
cd693b5fa4 | ||
|
|
63123471b3 | ||
|
|
ecc213b06f | ||
|
|
b584212edb | ||
|
|
4385349e36 | ||
|
|
9ef9ff3de8 | ||
|
|
4d12134552 | ||
|
|
7199b6c094 | ||
|
|
15c7d26378 | ||
|
|
033075c023 | ||
|
|
4c68fa46e7 | ||
|
|
a793017628 | ||
|
|
56a0a0db18 | ||
|
|
3595783202 | ||
|
|
67ac3a5173 | ||
|
|
3f4832631f | ||
|
|
542d0140f2 | ||
|
|
d9408eae44 | ||
|
|
644c48ba46 | ||
|
|
687894584f | ||
|
|
f03a0d8015 | ||
|
|
3a2b805a55 | ||
|
|
ae89a81ecb | ||
|
|
79fd881ad5 | ||
|
|
eac3ae5627 | ||
|
|
ee9a2952d6 | ||
|
|
11644241fa | ||
|
|
a7d964212e | ||
|
|
4d5241486f | ||
|
|
1467610109 | ||
|
|
267d9606f8 | ||
|
|
22c1608745 | ||
|
|
c97e058bc6 | ||
|
|
8a710f85e2 | ||
|
|
6ea9c0c48d | ||
|
|
67b28a7864 | ||
|
|
b21d5934e6 | ||
|
|
a06a6ccaa6 | ||
|
|
5b4987e160 | ||
|
|
364126d2e4 | ||
|
|
d68f30769a | ||
|
|
d0153065b0 | ||
|
|
af580c9977 | ||
|
|
88e6b572a8 | ||
|
|
600d92100c | ||
|
|
ce1ee98a12 | ||
|
|
f2783fbb55 | ||
|
|
968762cb3e | ||
|
|
da09316d4c | ||
|
|
b839663c48 | ||
|
|
01d1c67869 | ||
|
|
94d7e5964b | ||
|
|
f5c038cd1b | ||
|
|
616bb383c5 | ||
|
|
7614f166d8 | ||
|
|
6e02e29e4e | ||
|
|
d847f75ade | ||
|
|
314c7b8d2a | ||
|
|
2195529c26 | ||
|
|
f9f8ef0e17 | ||
|
|
9ad79625e0 | ||
|
|
603ca0963a | ||
|
|
0c18fb575e | ||
|
|
a08a66df92 | ||
|
|
af3743f09e | ||
|
|
a6aaaa5525 | ||
|
|
f0270f69f0 | ||
|
|
426fe96e5f | ||
|
|
42d238da14 | ||
|
|
8806d17ef1 | ||
|
|
d871e21a40 | ||
|
|
14a57795d5 | ||
|
|
93e9517f5d | ||
|
|
dda272220b | ||
|
|
125a352bc2 | ||
|
|
b7a3c6b6ca | ||
|
|
72256565bb | ||
|
|
d5a208cf69 | ||
|
|
cc931d20c2 | ||
|
|
0e38e9417d | ||
|
|
c116815c14 | ||
|
|
53d183ae03 | ||
|
|
93c75901cf | ||
|
|
e8e13674ef | ||
|
|
b9f9fc0afe | ||
|
|
8a554a5538 | ||
|
|
7d244051c8 | ||
|
|
84b54f549a | ||
|
|
c26114b66a | ||
|
|
98da9b56e9 | ||
|
|
9610a33d23 | ||
|
|
1ac292285e | ||
|
|
30c3892f9d | ||
|
|
1571dff3ef | ||
|
|
ffd6a8424b | ||
|
|
6ff4704345 | ||
|
|
9b09116576 | ||
|
|
342a398bec | ||
|
|
901bcd5c43 | ||
|
|
143acf2330 | ||
|
|
d1dc7ec7bf | ||
|
|
329d14957b | ||
|
|
8bce382834 | ||
|
|
0c83f48f78 | ||
|
|
ac0cb9db5a |
1
.github/helper/documentation.py
vendored
1
.github/helper/documentation.py
vendored
@@ -10,6 +10,7 @@ WEBSITE_REPOS = [
|
||||
|
||||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"docs.frappe.io",
|
||||
"frappeframework.com",
|
||||
]
|
||||
|
||||
|
||||
12
.github/helper/install.sh
vendored
12
.github/helper/install.sh
vendored
@@ -6,7 +6,7 @@ cd ~ || exit
|
||||
|
||||
sudo apt update
|
||||
sudo apt remove mysql-server mysql-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client-10.6
|
||||
sudo apt install libcups2-dev redis-server mariadb-client
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
@@ -51,13 +51,9 @@ fi
|
||||
|
||||
|
||||
install_whktml() {
|
||||
if [ "$(lsb_release -rs)" = "22.04" ]; then
|
||||
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
|
||||
sudo apt install /tmp/wkhtmltox.deb
|
||||
else
|
||||
echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
|
||||
exit 1
|
||||
fi
|
||||
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
|
||||
sudo apt install /tmp/wkhtmltox.deb
|
||||
|
||||
}
|
||||
install_whktml &
|
||||
wkpid=$!
|
||||
|
||||
164
README.md
164
README.md
@@ -1,57 +1,100 @@
|
||||
<div align="center">
|
||||
<a href="https://erpnext.com">
|
||||
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
|
||||
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
||||
</a>
|
||||
<h2>ERPNext</h2>
|
||||
<p align="center">
|
||||
<p>ERP made simple</p>
|
||||
<p>Powerful, Intuitive and Open-Source ERP</p>
|
||||
</p>
|
||||
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
|
||||
[](https://www.codetriage.com/frappe/erpnext)
|
||||
[](https://codecov.io/gh/frappe/erpnext)
|
||||
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
||||
|
||||
[https://erpnext.com](https://erpnext.com)
|
||||
|
||||
</div>
|
||||
|
||||
ERPNext as a monolith includes the following areas for managing businesses:
|
||||
|
||||
1. [Accounting](https://erpnext.com/open-source-accounting)
|
||||
1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system)
|
||||
1. [CRM](https://erpnext.com/open-source-crm)
|
||||
1. [Sales](https://erpnext.com/open-source-sales-purchase)
|
||||
1. [Purchase](https://erpnext.com/open-source-sales-purchase)
|
||||
1. [HRMS](https://erpnext.com/open-source-hrms)
|
||||
1. [Project Management](https://erpnext.com/open-source-projects)
|
||||
1. [Support](https://erpnext.com/open-source-help-desk-software)
|
||||
1. [Asset Management](https://erpnext.com/open-source-asset-management-software)
|
||||
1. [Quality Management](https://erpnext.com/docs/user/manual/en/quality-management)
|
||||
1. [Manufacturing](https://erpnext.com/open-source-manufacturing-erp-software)
|
||||
1. [Website Management](https://erpnext.com/open-source-website-builder-software)
|
||||
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
|
||||
1. [And More](https://erpnext.com/docs/user/manual/en/)
|
||||
|
||||
ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
|
||||
|
||||
## Installation
|
||||
|
||||
<div align="center" style="max-height: 40px;">
|
||||
<a href="https://frappecloud.com/erpnext/signup">
|
||||
<img src=".github/try-on-f-cloud-button.svg" height="40">
|
||||
</a>
|
||||
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
|
||||
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD" height="37"/>
|
||||
</a>
|
||||
<div align="center">
|
||||
<img src="./erpnext/public/images/v16/hero_image.png"/>
|
||||
</div>
|
||||
|
||||
> Login for the PWD site: (username: Administrator, password: admin)
|
||||
<div align="center">
|
||||
<a href="https://erpnext-demo.frappe.cloud/app/home">Live Demo</a>
|
||||
-
|
||||
<a href="https://erpnext.com">Website</a>
|
||||
-
|
||||
<a href="https://docs.erpnext.com">Documentation</a>
|
||||
</div>
|
||||
|
||||
### Containerized Installation
|
||||
## ERPNext
|
||||
|
||||
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
|
||||
100% Open-Source ERP system to help you run your business.
|
||||
|
||||
### Motivation
|
||||
|
||||
Running a business is a complex task - handling invoices, tracking stock, managing personnel and even more ad-hoc activities. In a market where software is sold separately to manage each of these tasks, ERPNext does all of the above and more, for free.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Accounting**: All the tools you need to manage cash flow in one place, right from recording transactions to summarizing and analyzing financial reports.
|
||||
- **Order Management**: Track inventory levels, replenish stock, and manage sales orders, customers, suppliers, shipments, deliverables, and order fulfillment.
|
||||
- **Manufacturing**: Simplifies the production cycle, helps track material consumption, exhibits capacity planning, handles subcontracting, and more!
|
||||
- **Asset Management**: From purchase to perishment, IT infrastructure to equipment. Cover every branch of your organization, all in one centralized system.
|
||||
- **Projects**: Delivery both internal and external Projects on time, budget and Profitability. Track tasks, timesheets, and issues by project.
|
||||
|
||||
<details open>
|
||||
|
||||
<summary>More</summary>
|
||||
<img src="https://erpnext.com/files/v16_bom.png"/>
|
||||
<img src="https://erpnext.com/files/v16_stock_summary.png"/>
|
||||
<img src="https://erpnext.com/files/v16_job_card.png"/>
|
||||
<img src="https://erpnext.com/files/v16_tasks.png"/>
|
||||
</details>
|
||||
|
||||
### Under the Hood
|
||||
|
||||
- [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework written in Python and Javascript. The framework provides a robust foundation for building web applications, including a database abstraction layer, user authentication, and a REST API.
|
||||
|
||||
- [**Frappe UI**](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface. The Frappe UI library provides a variety of components that can be used to build single-page applications on top of the Frappe Framework.
|
||||
|
||||
## Production Setup
|
||||
|
||||
### Managed Hosting
|
||||
|
||||
You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications with peace of mind.
|
||||
|
||||
It takes care of installation, setup, upgrades, monitoring, maintenance and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments.
|
||||
|
||||
<div>
|
||||
<a href="https://erpnext-demo.frappe.cloud/app/home" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png">
|
||||
<img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
### Self-Hosted
|
||||
#### Docker
|
||||
|
||||
Prerequisites: docker, docker-compose, git. Refer [Docker Documentation](https://docs.docker.com) for more details on Docker setup.
|
||||
|
||||
Run following commands:
|
||||
|
||||
```
|
||||
git clone https://github.com/frappe/frappe_docker
|
||||
cd frappe_docker
|
||||
docker compose -f pwd.yml up -d
|
||||
```
|
||||
|
||||
After a couple of minutes, site should be accessible on your localhost port: 8080. Use below default login credentials to access the site.
|
||||
- Username: Administrator
|
||||
- Password: admin
|
||||
|
||||
See [Frappe Docker](https://github.com/frappe/frappe_docker?tab=readme-ov-file#to-run-on-arm64-architecture-follow-this-instructions) for ARM based docker setup.
|
||||
|
||||
|
||||
## Development Setup
|
||||
### Manual Install
|
||||
|
||||
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.
|
||||
@@ -59,6 +102,35 @@ The Easy Way: our install script for bench will install all dependencies (e.g. M
|
||||
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
|
||||
|
||||
|
||||
### Local
|
||||
|
||||
To setup the repository locally follow the steps mentioned below:
|
||||
|
||||
1. Setup bench by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation) and start the server
|
||||
```
|
||||
bench start
|
||||
```
|
||||
|
||||
2. In a separate terminal window, run the following commands:
|
||||
```
|
||||
# Create a new site
|
||||
bench new-site erpnext.dev
|
||||
|
||||
# Map your site to localhost
|
||||
bench --site erpnext.dev add-to-hosts
|
||||
```
|
||||
|
||||
3. Get the ERPNext app and install it
|
||||
```
|
||||
# Get the ERPNext app
|
||||
bench get-app https://github.com/frappe/erpnext
|
||||
|
||||
# Install the app
|
||||
bench --site erpnext.dev install-app erpnext
|
||||
```
|
||||
|
||||
4. Open the URL `http://erpnext.dev:8000/app` in your browser, you should see the app running
|
||||
|
||||
## Learning and community
|
||||
|
||||
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||
@@ -73,14 +145,18 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
|
||||
1. [Report Security Vulnerabilities](https://erpnext.com/security)
|
||||
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
|
||||
## License
|
||||
|
||||
GNU/General Public License (see [license.txt](license.txt))
|
||||
|
||||
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
|
||||
|
||||
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
|
||||
|
||||
## Logo and Trademark Policy
|
||||
|
||||
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<div align="center" style="padding-top: 0.75rem;">
|
||||
<a href="https://frappe.io" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
|
||||
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"description": "Rate at which this tax is applied",
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"oldfieldname": "tax_rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ frappe.treeview_settings["Account"] = {
|
||||
fieldtype: "Select",
|
||||
options: erpnext.utils.get_tree_options("company"),
|
||||
label: __("Company"),
|
||||
render_on_toolbar: true,
|
||||
default: erpnext.utils.get_tree_default("company"),
|
||||
on_change: function () {
|
||||
var me = frappe.treeview_settings["Account"].treeview;
|
||||
@@ -182,7 +183,9 @@ frappe.treeview_settings["Account"] = {
|
||||
function () {
|
||||
frappe.set_route("Tree", "Cost Center", { company: get_company() });
|
||||
},
|
||||
__("View")
|
||||
__("View"),
|
||||
"default",
|
||||
true
|
||||
);
|
||||
|
||||
treeview.page.add_inner_button(
|
||||
@@ -190,31 +193,12 @@ frappe.treeview_settings["Account"] = {
|
||||
function () {
|
||||
frappe.set_route("Form", "Opening Invoice Creation Tool", { company: get_company() });
|
||||
},
|
||||
__("View")
|
||||
__("View"),
|
||||
"default",
|
||||
true
|
||||
);
|
||||
|
||||
treeview.page.add_inner_button(
|
||||
__("Period Closing Voucher"),
|
||||
function () {
|
||||
frappe.set_route("List", "Period Closing Voucher", { company: get_company() });
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
|
||||
treeview.page.add_inner_button(
|
||||
__("Journal Entry"),
|
||||
function () {
|
||||
frappe.new_doc("Journal Entry", { company: get_company() });
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
treeview.page.add_inner_button(
|
||||
__("Company"),
|
||||
function () {
|
||||
frappe.new_doc("Company");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
treeview.page.add_divider_to_button_group(__("View"));
|
||||
|
||||
// financial statements
|
||||
for (let report of [
|
||||
@@ -231,25 +215,28 @@ frappe.treeview_settings["Account"] = {
|
||||
function () {
|
||||
frappe.set_route("query-report", report, { company: get_company() });
|
||||
},
|
||||
__("Financial Statements")
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
},
|
||||
post_render: function (treeview) {
|
||||
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
|
||||
treeview.page.set_primary_action(
|
||||
__("New"),
|
||||
function () {
|
||||
let root_company = treeview.page.fields_dict.root_company.get_value();
|
||||
|
||||
if (root_company) {
|
||||
frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]);
|
||||
} else {
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
"add"
|
||||
);
|
||||
if (treeview.can_create) {
|
||||
treeview.page.set_primary_action(
|
||||
__("New"),
|
||||
function () {
|
||||
let root_company = treeview.page.fields_dict.root_company.get_value();
|
||||
if (root_company) {
|
||||
frappe.throw(__("Please add the account to root level Company - {0}"), [
|
||||
root_company,
|
||||
]);
|
||||
} else {
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
"add"
|
||||
);
|
||||
}
|
||||
},
|
||||
toolbar: [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -104,7 +104,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"oldfieldname": "rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ class BankAccount(Document):
|
||||
self.name = self.account_name + " - " + self.bank
|
||||
|
||||
def on_trash(self):
|
||||
delete_contact_and_address("BankAccount", self.name)
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
|
||||
@@ -117,9 +117,9 @@ class BankClearance(Document):
|
||||
)
|
||||
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||
)
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
args: {
|
||||
bank_account: frm.doc.bank_account,
|
||||
till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1),
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: (response) => {
|
||||
frm.set_value("account_opening_balance", response.message);
|
||||
@@ -135,6 +136,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
args: {
|
||||
bank_account: frm.doc.bank_account,
|
||||
till_date: frm.doc.bank_statement_to_date,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: (response) => {
|
||||
frm.cleared_balance = response.message;
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||
get_amounts_not_reflected_in_system,
|
||||
get_entries,
|
||||
@@ -78,10 +79,17 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_balance(bank_account, till_date):
|
||||
def get_account_balance(bank_account, till_date, company):
|
||||
# returns account balance till the specified date
|
||||
account = frappe.db.get_value("Bank Account", bank_account, "account")
|
||||
filters = frappe._dict({"account": account, "report_date": till_date, "include_pos_transactions": 1})
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"account": account,
|
||||
"report_date": till_date,
|
||||
"include_pos_transactions": 1,
|
||||
"company": company,
|
||||
}
|
||||
)
|
||||
data = get_entries(filters)
|
||||
|
||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||
@@ -93,11 +101,7 @@ def get_account_balance(bank_account, till_date):
|
||||
|
||||
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
|
||||
|
||||
bank_bal = (
|
||||
flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
|
||||
)
|
||||
|
||||
return bank_bal
|
||||
return flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -304,54 +308,56 @@ def create_payment_entry_bts(
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
paid_amount = bank_transaction.unallocated_amount
|
||||
|
||||
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
|
||||
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
payment_entry_dict = {
|
||||
"company": company,
|
||||
"payment_type": payment_type,
|
||||
"reference_no": reference_number,
|
||||
"reference_date": reference_date,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"posting_date": posting_date,
|
||||
"paid_amount": paid_amount,
|
||||
"received_amount": paid_amount,
|
||||
}
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_cached_value("Account", bank_account, "company")
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
|
||||
payment_entry.update(payment_entry_dict)
|
||||
bank_currency = bank_transaction.currency
|
||||
party_currency = frappe.get_cached_value("Account", party_account, "account_currency")
|
||||
|
||||
if mode_of_payment:
|
||||
payment_entry.mode_of_payment = mode_of_payment
|
||||
if project:
|
||||
payment_entry.project = project
|
||||
if cost_center:
|
||||
payment_entry.cost_center = cost_center
|
||||
if payment_type == "Receive":
|
||||
payment_entry.paid_to = company_account
|
||||
else:
|
||||
payment_entry.paid_from = company_account
|
||||
exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date)
|
||||
|
||||
payment_entry.validate()
|
||||
amt_in_bank_acc_currency = bank_transaction.unallocated_amount
|
||||
amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = payment_type
|
||||
pe.company = company
|
||||
pe.reference_no = reference_number
|
||||
pe.reference_date = reference_date
|
||||
pe.party_type = party_type
|
||||
pe.party = party
|
||||
pe.posting_date = posting_date
|
||||
pe.paid_from = party_account if payment_type == "Receive" else bank_account
|
||||
pe.paid_to = party_account if payment_type == "Pay" else bank_account
|
||||
pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency
|
||||
pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency
|
||||
pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency
|
||||
pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency
|
||||
pe.mode_of_payment = mode_of_payment
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
return payment_entry
|
||||
return pe
|
||||
|
||||
payment_entry.insert()
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
payment_entry.submit()
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment_entry.name,
|
||||
"amount": paid_amount,
|
||||
"payment_name": pe.name,
|
||||
"amount": amt_in_bank_acc_currency,
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -480,8 +486,12 @@ def get_linked_payments(
|
||||
def subtract_allocations(gl_account, vouchers):
|
||||
"Look up & subtract any existing Bank Transaction allocations"
|
||||
copied = []
|
||||
|
||||
voucher_docs = [(voucher.get("doctype"), voucher.get("name")) for voucher in vouchers]
|
||||
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
|
||||
|
||||
for voucher in vouchers:
|
||||
rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
|
||||
rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or []
|
||||
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
|
||||
|
||||
if amount := None if not filtered_row else filtered_row[0]["total"]:
|
||||
@@ -719,7 +729,7 @@ def get_pe_matching_query(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.base_paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
|
||||
@@ -154,10 +154,16 @@ class BankTransaction(Document):
|
||||
"""
|
||||
remaining_amount = self.unallocated_amount
|
||||
to_remove = []
|
||||
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
|
||||
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
self, payment_entry
|
||||
self,
|
||||
payment_entry,
|
||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
|
||||
or [],
|
||||
)
|
||||
|
||||
if 0.0 == unallocated_amount:
|
||||
@@ -208,13 +214,17 @@ class BankTransaction(Document):
|
||||
if self.party_type and self.party:
|
||||
return
|
||||
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
result = None
|
||||
try:
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error in party matching for Bank Transaction {0}").format(self.name))
|
||||
|
||||
if not result:
|
||||
return
|
||||
@@ -228,7 +238,7 @@ def get_doctypes_for_bank_reconciliation():
|
||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||
|
||||
|
||||
def get_clearance_details(transaction, payment_entry):
|
||||
def get_clearance_details(transaction, payment_entry, bt_allocations):
|
||||
"""
|
||||
There should only be one bank gle for a voucher.
|
||||
Could be none for a Bank Transaction.
|
||||
@@ -237,7 +247,6 @@ def get_clearance_details(transaction, payment_entry):
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
@@ -290,44 +299,52 @@ def get_related_bank_gl_entries(doctype, docname):
|
||||
)
|
||||
|
||||
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
def get_total_allocated_amount(docs):
|
||||
"""
|
||||
Gets the sum of allocations for a voucher on each bank GL account
|
||||
along with the latest bank transaction name & date
|
||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||
"""
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT total, latest_name, latest_date, gl_account FROM (
|
||||
SELECT total, latest_name, 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) AS total,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
||||
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||
ba.account AS gl_account
|
||||
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 = %(doctype)s
|
||||
AND btp.payment_entry = %(docname)s
|
||||
(btp.payment_document, btp.payment_entry) IN %(docs)s
|
||||
AND bt.docstatus = 1
|
||||
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
|
||||
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
|
||||
) temp
|
||||
WHERE
|
||||
rownum = 1
|
||||
""",
|
||||
dict(doctype=doctype, docname=docname),
|
||||
dict(docs=docs),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
payment_allocation_details = {}
|
||||
for row in result:
|
||||
# Why is this *sometimes* a byte string?
|
||||
if isinstance(row["latest_name"], bytes):
|
||||
row["latest_name"] = row["latest_name"].decode()
|
||||
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||
return result
|
||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
|
||||
|
||||
return payment_allocation_details
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
|
||||
@@ -490,13 +490,20 @@ def get_actual_expense(args):
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
for d in frappe.db.sql(
|
||||
"""select mdp.month, mdp.percentage_allocation
|
||||
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
|
||||
where mdp.parent=md.name and md.fiscal_year=%s""",
|
||||
fiscal_year,
|
||||
as_dict=1,
|
||||
):
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(mdp)
|
||||
.join(md)
|
||||
.on(mdp.parent == md.name)
|
||||
.select(mdp.month, mdp.percentage_allocation)
|
||||
.where(md.fiscal_year == fiscal_year)
|
||||
.where(md.name == monthly_distribution)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for d in res:
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
|
||||
@@ -13,7 +13,11 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
|
||||
from erpnext.accounts.party import (
|
||||
validate_account_party_type,
|
||||
validate_party_frozen_disabled,
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
@@ -268,8 +272,12 @@ class GLEntry(Document):
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
if self.is_cancelled:
|
||||
return
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
account_currency = get_account_currency(self.account)
|
||||
|
||||
|
||||
@@ -78,3 +78,48 @@ class TestGLEntry(IntegrationTestCase):
|
||||
"SELECT current from tabSeries where name = %s", naming_series
|
||||
)[0][0]
|
||||
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
|
||||
|
||||
def test_validate_account_party_type(self):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Supplier"
|
||||
break
|
||||
|
||||
jv.save()
|
||||
try:
|
||||
jv.submit()
|
||||
except Exception as e:
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Customer"
|
||||
break
|
||||
|
||||
jv1.save()
|
||||
try:
|
||||
jv1.submit()
|
||||
except Exception as e:
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
@@ -27,6 +27,18 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
// project excluded in setup_dimension_filters
|
||||
frm.set_query("project", function (doc) {
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (doc.party_type == "Customer") filters.customer = doc.party;
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
}
|
||||
@@ -1328,6 +1340,24 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (r.message) {
|
||||
if (!frm.doc.mode_of_payment) {
|
||||
frm.set_value(field, r.message.account);
|
||||
} else {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: "Mode of Payment Account",
|
||||
filters: {
|
||||
parent: frm.doc.mode_of_payment,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
fieldname: "default_account",
|
||||
parent: "Mode of Payment",
|
||||
},
|
||||
callback: function (res) {
|
||||
if (!res.message.default_account) {
|
||||
frm.set_value(field, r.message.account);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
frm.set_value("bank", r.message.bank);
|
||||
frm.set_value("bank_account_no", r.message.bank_account_no);
|
||||
|
||||
@@ -1519,7 +1519,7 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Paid",
|
||||
company=company,
|
||||
account_type="Liability",
|
||||
account_type="Payable",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -153,10 +153,7 @@ class PaymentReconciliation(Document):
|
||||
self.add_payment_entries(non_reconciled_payments)
|
||||
|
||||
def get_payment_entries(self):
|
||||
if self.default_advance_account:
|
||||
party_account = [self.receivable_payable_account, self.default_advance_account]
|
||||
else:
|
||||
party_account = [self.receivable_payable_account]
|
||||
party_account = [self.receivable_payable_account]
|
||||
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = frappe._dict(
|
||||
@@ -187,6 +184,7 @@ class PaymentReconciliation(Document):
|
||||
self.party,
|
||||
party_account,
|
||||
order_doctype,
|
||||
default_advance_account=self.default_advance_account,
|
||||
against_all_orders=True,
|
||||
limit=self.payment_limit,
|
||||
condition=condition,
|
||||
|
||||
@@ -60,7 +60,9 @@
|
||||
"payment_order",
|
||||
"amended_from",
|
||||
"column_break_pnyv",
|
||||
"payment_url"
|
||||
"payment_url",
|
||||
"column_break_iiuv",
|
||||
"phone_number"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -378,6 +380,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel==\"Phone\"",
|
||||
"fetch_from": "payment_gateway_account.payment_channel",
|
||||
"fieldname": "payment_channel",
|
||||
"fieldtype": "Select",
|
||||
@@ -444,13 +447,22 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iiuv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Number"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-10-23 12:23:40.117336",
|
||||
"modified": "2025-01-04 05:39:32.448857",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
||||
@@ -81,6 +81,7 @@ class PaymentRequest(Document):
|
||||
payment_order: DF.Link | None
|
||||
payment_request_type: DF.Literal["Outward", "Inward"]
|
||||
payment_url: DF.Data | None
|
||||
phone_number: DF.Data | None
|
||||
print_format: DF.Literal[None]
|
||||
project: DF.Link | None
|
||||
reference_doctype: DF.Link | None
|
||||
@@ -221,6 +222,7 @@ class PaymentRequest(Document):
|
||||
sender=self.email_to,
|
||||
currency=self.currency,
|
||||
payment_gateway=self.payment_gateway,
|
||||
phone_number=self.phone_number,
|
||||
)
|
||||
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
@@ -643,6 +645,7 @@ def make_payment_request(**args):
|
||||
or args.order_type == "Shopping Cart" # compat for webshop app
|
||||
or gateway_account.get("payment_channel", "Email") != "Email"
|
||||
),
|
||||
"phone_number": args.get("phone_number") if args.get("phone_number") else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const INDICATORS = {
|
||||
"Partially Paid": "orange",
|
||||
Cancelled: "red",
|
||||
Draft: "gray",
|
||||
Draft: "red",
|
||||
Failed: "red",
|
||||
Initiated: "green",
|
||||
Paid: "blue",
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Grand Total') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Grand Total") }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Net Total') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Net Total") }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Total Quantity') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Total Quantity") }}</td>
|
||||
<td class='text-right'>{{ data.total_quantity or '' }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<tbody>
|
||||
{% for d in data.payment_reconciliation %}
|
||||
<tr>
|
||||
<td class="text-left">{{ d.mode_of_payment }}</td>
|
||||
<td class="text-left">{{ _(d.mode_of_payment) }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -63,7 +63,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ _("Account") }}</th>
|
||||
<th class="text-left">{{ _("Rate") }}</th>
|
||||
<th class="text-left">{{ _("Tax Rate") }}</th>
|
||||
<th class="text-right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -118,11 +118,13 @@ class POSInvoiceMergeLog(Document):
|
||||
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
|
||||
|
||||
sales_invoice, credit_note = "", ""
|
||||
if returns:
|
||||
credit_note = self.process_merging_into_credit_note(returns)
|
||||
|
||||
sales_invoice_doc = None
|
||||
if sales:
|
||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||
sales_invoice_doc = self.process_merging_into_sales_invoice(sales)
|
||||
sales_invoice = sales_invoice_doc.name
|
||||
|
||||
if returns:
|
||||
credit_note = self.process_merging_into_credit_note(returns, sales_invoice_doc)
|
||||
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
@@ -133,6 +135,7 @@ class POSInvoiceMergeLog(Document):
|
||||
self.update_pos_invoices(pos_invoice_docs)
|
||||
self.serial_and_batch_bundle_reference_for_pos_invoice()
|
||||
self.cancel_linked_invoices()
|
||||
self.delink_serial_and_batch_bundle()
|
||||
|
||||
def process_merging_into_sales_invoice(self, data):
|
||||
sales_invoice = self.get_new_sales_invoice()
|
||||
@@ -140,20 +143,35 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
sales_invoice.is_consolidated = 1
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.posting_time = get_time(self.posting_time)
|
||||
|
||||
if not sales_invoice.posting_date:
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
|
||||
if not sales_invoice.posting_time:
|
||||
sales_invoice.posting_time = get_time(self.posting_time)
|
||||
|
||||
sales_invoice.save()
|
||||
sales_invoice.submit()
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice.name
|
||||
return sales_invoice
|
||||
|
||||
def process_merging_into_credit_note(self, data):
|
||||
def process_merging_into_credit_note(self, data, sales_invoice_doc=None):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||
referenes = {}
|
||||
|
||||
if sales_invoice_doc:
|
||||
credit_note.return_against = sales_invoice_doc.name
|
||||
|
||||
for d in sales_invoice_doc.items:
|
||||
referenes[d.item_code] = d.name
|
||||
|
||||
for d in credit_note.items:
|
||||
d.sales_invoice_item = referenes.get(d.item_code)
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
@@ -181,6 +199,10 @@ class POSInvoiceMergeLog(Document):
|
||||
for doc in data:
|
||||
map_doc(doc, invoice, table_map={"doctype": invoice.doctype})
|
||||
|
||||
if doc.get("posting_date"):
|
||||
invoice.posting_date = getdate(doc.posting_date)
|
||||
invoice.posting_time = get_time(doc.posting_time)
|
||||
|
||||
if doc.redeem_loyalty_points:
|
||||
invoice.loyalty_redemption_account = doc.loyalty_redemption_account
|
||||
invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
|
||||
@@ -298,6 +320,8 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice = frappe.new_doc("Sales Invoice")
|
||||
sales_invoice.customer = self.customer
|
||||
sales_invoice.is_pos = 1
|
||||
sales_invoice.posting_date = None
|
||||
sales_invoice.posting_time = None
|
||||
|
||||
return sales_invoice
|
||||
|
||||
@@ -320,8 +344,45 @@ class POSInvoiceMergeLog(Document):
|
||||
for table_name in ["items", "packed_items"]:
|
||||
pos_invoice.set_serial_and_batch_bundle(table_name)
|
||||
|
||||
def delink_serial_and_batch_bundle(self):
|
||||
bundles = self.get_serial_and_batch_bundles()
|
||||
if not bundles:
|
||||
return
|
||||
|
||||
sle_table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.update(sle_table)
|
||||
.set(sle_table.serial_and_batch_bundle, None)
|
||||
.where(sle_table.serial_and_batch_bundle.isin(bundles) & sle_table.is_cancelled == 1)
|
||||
)
|
||||
|
||||
query.run()
|
||||
|
||||
def get_serial_and_batch_bundles(self):
|
||||
pos_invoices = []
|
||||
for d in self.pos_invoices:
|
||||
pos_invoices.append(d.pos_invoice)
|
||||
|
||||
if pos_invoices:
|
||||
return frappe.get_all(
|
||||
"POS Invoice Item",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"parent": ["in", pos_invoices],
|
||||
"serial_and_batch_bundle": ["is", "set"],
|
||||
},
|
||||
pluck="serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
def cancel_linked_invoices(self):
|
||||
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
|
||||
invoices = [self.consolidated_invoice, self.consolidated_credit_note]
|
||||
if not invoices:
|
||||
return
|
||||
|
||||
invoices.reverse()
|
||||
for si_name in invoices:
|
||||
if not si_name:
|
||||
continue
|
||||
si = frappe.get_doc("Sales Invoice", si_name)
|
||||
@@ -503,6 +564,9 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
|
||||
try:
|
||||
for log in merge_logs:
|
||||
merge_log = frappe.get_doc("POS Invoice Merge Log", log)
|
||||
if merge_log.docstatus == 2:
|
||||
continue
|
||||
|
||||
merge_log.flags.ignore_permissions = True
|
||||
merge_log.cancel()
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"hide_unavailable_items",
|
||||
"auto_add_item_to_cart",
|
||||
"validate_stock_on_save",
|
||||
"print_receipt_on_order_complete",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
@@ -374,24 +375,30 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_campaign",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Campaign",
|
||||
"options": "UTM Campaign"
|
||||
"options": "UTM Campaign",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_source",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Source",
|
||||
"options": "UTM Source"
|
||||
"options": "UTM Source",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Medium",
|
||||
"options": "UTM Campaign"
|
||||
"options": "UTM Campaign",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_receipt_on_order_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Receipt on Order Complete"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -419,7 +426,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2024-06-28 10:51:48.543766",
|
||||
"modified": "2025-01-01 11:07:03.161950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
@@ -448,4 +455,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ class POSProfile(Document):
|
||||
letter_head: DF.Link | None
|
||||
payments: DF.Table[POSPaymentMethod]
|
||||
print_format: DF.Link | None
|
||||
print_receipt_on_order_complete: DF.Check
|
||||
select_print_heading: DF.Link | None
|
||||
selling_price_list: DF.Link | None
|
||||
tax_category: DF.Link | None
|
||||
|
||||
@@ -415,6 +415,8 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
"parent": args.parent,
|
||||
"parenttype": args.parenttype,
|
||||
"child_docname": args.get("child_docname"),
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -651,8 +651,17 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
|
||||
qty = pricing_rule.free_qty or 1
|
||||
if pricing_rule.is_recursive:
|
||||
transaction_qty = (args.get("qty") if args else doc.total_qty) - pricing_rule.apply_recursion_over
|
||||
if transaction_qty:
|
||||
transaction_qty = sum(
|
||||
[
|
||||
row.qty
|
||||
for row in doc.items
|
||||
if not row.is_free_item
|
||||
and row.item_code == args.item_code
|
||||
and row.pricing_rules == args.pricing_rules
|
||||
]
|
||||
)
|
||||
transaction_qty = transaction_qty - pricing_rule.apply_recursion_over
|
||||
if transaction_qty and transaction_qty > 0:
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:ACC-PPR-{#####}",
|
||||
"beta": 1,
|
||||
"creation": "2023-03-30 21:28:39.793927",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -158,7 +157,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-27 14:48:56.715320",
|
||||
"modified": "2025-01-08 08:22:14.798085",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
@@ -192,4 +191,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company"
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ def trigger_reconciliation_for_queued_docs():
|
||||
unique_filters = set()
|
||||
queue_size = 5
|
||||
|
||||
fields = ["company", "party_type", "party", "receivable_payable_account"]
|
||||
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
|
||||
|
||||
def get_filters_as_tuple(fields, doc):
|
||||
filters = ()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:PPR-LOG-{##}",
|
||||
"beta": 1,
|
||||
"creation": "2023-03-13 15:00:09.149681",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -110,7 +109,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:18.769659",
|
||||
"modified": "2025-01-08 08:22:19.104975",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log",
|
||||
|
||||
@@ -244,9 +244,9 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "cc_to",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "CC To",
|
||||
"options": "User"
|
||||
"options": "Process Statement Of Accounts CC"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -400,7 +400,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-10-18 17:51:39.108481",
|
||||
"modified": "2024-12-11 12:11:13.543134",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -31,6 +31,9 @@ class ProcessStatementOfAccounts(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.process_statement_of_accounts_cc.process_statement_of_accounts_cc import (
|
||||
ProcessStatementOfAccountsCC,
|
||||
)
|
||||
from erpnext.accounts.doctype.process_statement_of_accounts_customer.process_statement_of_accounts_customer import (
|
||||
ProcessStatementOfAccountsCustomer,
|
||||
)
|
||||
@@ -41,7 +44,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
ageing_based_on: DF.Literal["Due Date", "Posting Date"]
|
||||
based_on_payment_terms: DF.Check
|
||||
body: DF.TextEditor | None
|
||||
cc_to: DF.Link | None
|
||||
cc_to: DF.TableMultiSelect[ProcessStatementOfAccountsCC]
|
||||
collection_name: DF.DynamicLink | None
|
||||
company: DF.Link
|
||||
cost_center: DF.TableMultiSelect[PSOACostCenter]
|
||||
@@ -324,7 +327,7 @@ def get_recipients_and_cc(customer, doc):
|
||||
cc = []
|
||||
if doc.cc_to != "":
|
||||
try:
|
||||
cc = [frappe.get_value("User", doc.cc_to, "email")]
|
||||
cc = [frappe.get_value("User", user.cc, "email") for user in doc.cc_to]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-12-11 12:10:04.654593",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"cc"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "cc",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "CC",
|
||||
"options": "User"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-11 12:10:39.772598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts CC",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProcessStatementOfAccountsCC(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
cc: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -403,6 +403,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
hide_fields(this.frm.doc);
|
||||
if (cint(this.frm.doc.is_paid)) {
|
||||
this.frm.set_value("allocate_advances_automatically", 0);
|
||||
this.frm.set_value("payment_terms_template", "");
|
||||
this.frm.set_value("payment_schedule", []);
|
||||
if (!this.frm.doc.company) {
|
||||
this.frm.set_value("is_paid", 0);
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
|
||||
@@ -1683,7 +1683,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if pi:
|
||||
pi = pi[0][0]
|
||||
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
|
||||
|
||||
frappe.throw(
|
||||
_("Supplier Invoice No exists in Purchase Invoice {0}").format(
|
||||
get_link_to_form("Purchase Invoice", pi)
|
||||
)
|
||||
)
|
||||
|
||||
def update_billing_status_in_pr(self, update_modified=True):
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
|
||||
|
||||
@@ -1845,6 +1845,52 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_adjust_incoming_rate_for_rejected_item(self):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rejected_qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "warehouse": pi.items[0].warehouse},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": pr.name,
|
||||
"warehouse": pi.items[0].rejected_warehouse,
|
||||
},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertFalse(stock_value_difference)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_item_less_defaults(self):
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
pi.supplier = "_Test Supplier"
|
||||
@@ -2465,6 +2511,34 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 0)
|
||||
|
||||
def test_invoice_against_returned_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_purchase_invoice_from_pr,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_return_against_rejected_warehouse,
|
||||
)
|
||||
|
||||
item = make_item("_Test Item For Invoice Against Returned PR", properties={"is_stock_item": 1}).name
|
||||
|
||||
original_value = frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
|
||||
|
||||
pr = make_purchase_receipt(item_code=item, qty=5, rejected_qty=5, rate=100)
|
||||
pr_return = make_purchase_return_against_rejected_warehouse(pr.name)
|
||||
pr_return.submit()
|
||||
|
||||
pi = make_purchase_invoice_from_pr(pr.name)
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].qty, 5.0)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", original_value
|
||||
)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "180px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -40,7 +41,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -111,13 +112,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:24.072896",
|
||||
"modified": "2024-12-20 12:04:46.729972",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
|
||||
@@ -16,6 +16,7 @@ class PurchaseInvoiceAdvance(Document):
|
||||
|
||||
advance_amount: DF.Currency
|
||||
allocated_amount: DF.Currency
|
||||
difference_posting_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -181,7 +181,7 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
else:
|
||||
elif doc.doctype in frappe.get_hooks("repost_allowed_doctypes"):
|
||||
if hasattr(doc, "make_gl_entries") and callable(doc.make_gl_entries):
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
if "cancel" in inspect.getfullargspec(doc.make_gl_entries):
|
||||
|
||||
@@ -993,47 +993,51 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
|
||||
frm.add_custom_button(__("Fetch Timesheet"), function () {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldname: "from_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
frm.add_custom_button(
|
||||
__("Timesheet"),
|
||||
function () {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldname: "from_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
label: __("To"),
|
||||
fieldname: "to_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Project"),
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
default: frm.doc.project,
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const data = d.get_values();
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
label: __("To"),
|
||||
fieldname: "to_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Project"),
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
default: frm.doc.project,
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const data = d.get_values();
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __("Get Timesheets"),
|
||||
});
|
||||
d.show();
|
||||
});
|
||||
primary_action_label: __("Get Timesheets"),
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.is_debit_note) {
|
||||
|
||||
@@ -323,9 +323,7 @@ class SalesInvoice(SellingController):
|
||||
self.set_against_income_account()
|
||||
self.validate_time_sheets_are_submitted()
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
|
||||
if not self.is_return:
|
||||
self.validate_serial_numbers()
|
||||
else:
|
||||
if self.is_return:
|
||||
self.timesheets = []
|
||||
self.update_packing_list()
|
||||
self.set_billing_hours_and_amount()
|
||||
@@ -364,7 +362,7 @@ class SalesInvoice(SellingController):
|
||||
if self.update_stock:
|
||||
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
|
||||
|
||||
elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or (
|
||||
elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or (
|
||||
asset.status == "Sold" and not self.is_return
|
||||
):
|
||||
frappe.throw(
|
||||
@@ -506,7 +504,7 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
def validate_pos_paid_amount(self):
|
||||
if len(self.payments) == 0 and self.is_pos:
|
||||
if len(self.payments) == 0 and self.is_pos and flt(self.grand_total) > 0:
|
||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||
|
||||
def check_if_consolidated_invoice(self):
|
||||
@@ -1010,9 +1008,9 @@ class SalesInvoice(SellingController):
|
||||
def validate_pos(self):
|
||||
if self.is_return:
|
||||
invoice_total = self.rounded_total or self.grand_total
|
||||
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > 1.0 / (
|
||||
10.0 ** (self.precision("grand_total") + 1.0)
|
||||
):
|
||||
if abs(flt(self.paid_amount)) + abs(flt(self.write_off_amount)) - abs(
|
||||
flt(invoice_total)
|
||||
) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
|
||||
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
||||
|
||||
def validate_warehouse(self):
|
||||
@@ -1703,14 +1701,6 @@ class SalesInvoice(SellingController):
|
||||
self.set("write_off_amount", reference_doc.get("write_off_amount"))
|
||||
self.due_date = None
|
||||
|
||||
def validate_serial_numbers(self):
|
||||
"""
|
||||
validate serial number agains Delivery Note and Sales Invoice
|
||||
"""
|
||||
for item in self.items:
|
||||
item.set_serial_no_against_delivery_note()
|
||||
item.validate_serial_against_delivery_note()
|
||||
|
||||
def update_project(self):
|
||||
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
|
||||
if self.project and self.project not in unique_projects:
|
||||
|
||||
@@ -15,7 +15,7 @@ frappe.listview_settings["Sales Invoice"] = {
|
||||
],
|
||||
get_indicator: function (doc) {
|
||||
const status_colors = {
|
||||
Draft: "grey",
|
||||
Draft: "red",
|
||||
Unpaid: "orange",
|
||||
Paid: "green",
|
||||
Return: "gray",
|
||||
|
||||
@@ -43,6 +43,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.get_item_details import get_item_tax_map
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
@@ -2871,13 +2872,26 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
item.save()
|
||||
|
||||
sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True)
|
||||
item_tax_map = get_item_tax_map(
|
||||
doc=sales_invoice,
|
||||
tax_template=sales_invoice.items[0].item_tax_template,
|
||||
)
|
||||
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
# Apply discount
|
||||
sales_invoice.apply_discount_on = "Net Total"
|
||||
sales_invoice.discount_amount = 300
|
||||
sales_invoice.save()
|
||||
|
||||
item_tax_map = get_item_tax_map(
|
||||
doc=sales_invoice,
|
||||
tax_template=sales_invoice.items[0].item_tax_template,
|
||||
)
|
||||
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
@IntegrationTestCase.change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_sales_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "250px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -41,7 +42,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -112,13 +113,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:36.003704",
|
||||
"modified": "2024-12-20 11:58:28.962370",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
|
||||
@@ -16,6 +16,7 @@ class SalesInvoiceAdvance(Document):
|
||||
|
||||
advance_amount: DF.Currency
|
||||
allocated_amount: DF.Currency
|
||||
difference_posting_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe.model.document import Document
|
||||
from frappe.utils.data import cint
|
||||
|
||||
from erpnext.assets.doctype.asset.depreciation import get_disposal_account_and_cost_center
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
class SalesInvoiceItem(Document):
|
||||
@@ -128,39 +128,3 @@ class SalesInvoiceItem(Document):
|
||||
self.income_account = disposal_account
|
||||
if not self.cost_center:
|
||||
self.cost_center = depreciation_cost_center
|
||||
|
||||
def set_serial_no_against_delivery_note(self):
|
||||
"""Set serial no based on delivery note."""
|
||||
if self.serial_no and self.delivery_note and self.qty != len(get_serial_nos(self.serial_no)):
|
||||
self.serial_no = get_delivery_note_serial_no(self.item_code, self.qty, self.delivery_note)
|
||||
|
||||
def validate_serial_against_delivery_note(self):
|
||||
"""Ensure the serial numbers in this Sales Invoice Item are same as in the linked Delivery Note."""
|
||||
if not self.delivery_note or not self.dn_detail:
|
||||
return
|
||||
|
||||
serial_nos = frappe.db.get_value("Delivery Note Item", self.dn_detail, "serial_no") or ""
|
||||
dn_serial_nos = set(get_serial_nos(serial_nos))
|
||||
|
||||
serial_nos = self.serial_no or ""
|
||||
si_serial_nos = set(get_serial_nos(serial_nos))
|
||||
serial_no_diff = si_serial_nos - dn_serial_nos
|
||||
|
||||
if serial_no_diff:
|
||||
dn_link = frappe.utils.get_link_to_form("Delivery Note", self.delivery_note)
|
||||
msg = (
|
||||
_("Row #{0}: The following serial numbers are not present in Delivery Note {1}:").format(
|
||||
self.idx, dn_link
|
||||
)
|
||||
+ " "
|
||||
+ ", ".join(frappe.bold(d) for d in serial_no_diff)
|
||||
)
|
||||
|
||||
frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
|
||||
|
||||
if self.serial_no and cint(self.qty) != len(si_serial_nos):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: {1} serial numbers are required for Item {2}. You have provided {3} serial numbers."
|
||||
).format(self.idx, self.qty, self.item_code, len(si_serial_nos))
|
||||
)
|
||||
|
||||
@@ -247,14 +247,14 @@ def get_tax_row_for_tds(tax_details, tax_amount):
|
||||
}
|
||||
|
||||
|
||||
def get_lower_deduction_certificate(company, tax_details, pan_no):
|
||||
def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
||||
ldc_name = frappe.db.get_value(
|
||||
"Lower Deduction Certificate",
|
||||
{
|
||||
"pan_no": pan_no,
|
||||
"tax_withholding_category": tax_details.tax_withholding_category,
|
||||
"valid_from": (">=", tax_details.from_date),
|
||||
"valid_upto": ("<=", tax_details.to_date),
|
||||
"valid_from": ("<=", posting_date),
|
||||
"valid_upto": (">=", posting_date),
|
||||
"company": company,
|
||||
},
|
||||
"name",
|
||||
@@ -302,7 +302,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
|
||||
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
if ldc:
|
||||
@@ -539,7 +539,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
)
|
||||
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.tax_withholding_net_total
|
||||
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
if type.payment_type == "Pay":
|
||||
@@ -551,9 +551,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.base_tax_withholding_net_total
|
||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
||||
else:
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Purchase Invoice",
|
||||
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Incoming Bills",
|
||||
"modified": "2024-11-20 19:08:37.043777",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Incoming Bills",
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Incoming Payment",
|
||||
"modified": "2020-07-22 13:06:20.237689",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Incoming Payment",
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Invoice",
|
||||
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Outgoing Bills",
|
||||
"modified": "2020-07-22 13:07:19.633101",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Outgoing Bills",
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Outgoing Payment",
|
||||
"modified": "2020-07-22 12:49:34.942896",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Outgoing Payment",
|
||||
|
||||
@@ -621,34 +621,41 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
|
||||
return due_date
|
||||
|
||||
|
||||
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
|
||||
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None, doctype=None):
|
||||
if getdate(due_date) < getdate(posting_date):
|
||||
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
|
||||
doctype_date = "Date"
|
||||
if doctype == "Purchase Invoice":
|
||||
doctype_date = "Supplier Invoice Date"
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
doctype_date = "Posting Date"
|
||||
|
||||
frappe.throw(_("Due Date cannot be before {0}").format(doctype_date))
|
||||
else:
|
||||
if not template_name:
|
||||
return
|
||||
validate_due_date_with_template(posting_date, due_date, bill_date, template_name)
|
||||
|
||||
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
|
||||
"%Y-%m-%d"
|
||||
|
||||
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name):
|
||||
if not template_name:
|
||||
return
|
||||
|
||||
default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
is_credit_controller = (
|
||||
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
|
||||
)
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
is_credit_controller = (
|
||||
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
|
||||
if is_credit_controller:
|
||||
msgprint(
|
||||
_("Note: Due Date exceeds allowed customer credit days by {0} day(s)").format(
|
||||
date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
if is_credit_controller:
|
||||
msgprint(
|
||||
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
|
||||
date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Due Date cannot be after {0}").format(formatdate(default_due_date)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -681,7 +688,7 @@ def set_taxes(
|
||||
):
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template
|
||||
|
||||
args = {party_type.lower(): party, "company": company}
|
||||
args = {frappe.scrub(party_type): party, "company": company}
|
||||
|
||||
if tax_category:
|
||||
args["tax_category"] = tax_category
|
||||
@@ -701,10 +708,10 @@ def set_taxes(
|
||||
else:
|
||||
args.update(get_party_details(party, party_type))
|
||||
|
||||
if party_type in ("Customer", "Lead", "Prospect"):
|
||||
if party_type in ("Customer", "Lead", "Prospect", "CRM Deal"):
|
||||
args.update({"tax_type": "Sales"})
|
||||
|
||||
if party_type in ["Lead", "Prospect"]:
|
||||
if party_type in ["Lead", "Prospect", "CRM Deal"]:
|
||||
args["customer"] = None
|
||||
del args[frappe.scrub(party_type)]
|
||||
else:
|
||||
@@ -764,6 +771,20 @@ def validate_party_frozen_disabled(party_type, party_name):
|
||||
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
|
||||
|
||||
|
||||
def validate_account_party_type(self):
|
||||
if self.is_cancelled:
|
||||
return
|
||||
|
||||
if self.party_type and self.party:
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type and (account_type not in ["Receivable", "Payable"]):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
|
||||
).format(self.account)
|
||||
)
|
||||
|
||||
|
||||
def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
|
||||
|
||||
|
||||
@@ -134,7 +134,6 @@ class ReceivablePayableReport:
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
@@ -150,6 +149,9 @@ class ReceivablePayableReport:
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -275,9 +277,6 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -551,9 +550,7 @@ class ReceivablePayableReport:
|
||||
self.append_payment_term(row, d, term)
|
||||
|
||||
def append_payment_term(self, row, d, term):
|
||||
if (
|
||||
self.filters.get("customer") or self.filters.get("supplier")
|
||||
) and d.currency == d.party_account_currency:
|
||||
if d.currency == d.party_account_currency:
|
||||
invoiced = d.payment_amount
|
||||
else:
|
||||
invoiced = d.base_payment_amount
|
||||
|
||||
@@ -28,15 +28,14 @@ def get_group_by_asset_category_data(filters):
|
||||
|
||||
for asset_category in asset_categories:
|
||||
row = frappe._dict()
|
||||
# row.asset_category = asset_category
|
||||
row.update(asset_category)
|
||||
|
||||
row.cost_as_on_to_date = (
|
||||
flt(row.cost_as_on_from_date)
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
- flt(row.cost_of_capitalized_asset)
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(
|
||||
@@ -53,11 +52,11 @@ def get_group_by_asset_category_data(filters):
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
@@ -85,12 +84,12 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
end), 0) as value_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
@@ -101,7 +100,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
end), 0) as value_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
@@ -112,7 +111,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset,
|
||||
end), 0) as value_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
@@ -123,7 +122,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_capitalized_asset
|
||||
end), 0) as value_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
@@ -164,12 +163,12 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
end), 0) as value_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
@@ -180,7 +179,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
end), 0) as value_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
@@ -191,7 +190,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset,
|
||||
end), 0) as value_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
@@ -202,7 +201,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_capitalized_asset
|
||||
end), 0) as value_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
@@ -232,15 +231,14 @@ def get_group_by_asset_data(filters):
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
# row.asset_category = asset_category
|
||||
row.update(asset_detail)
|
||||
|
||||
row.cost_as_on_to_date = (
|
||||
flt(row.cost_as_on_from_date)
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
- flt(row.cost_of_capitalized_asset)
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
@@ -251,11 +249,11 @@ def get_group_by_asset_data(filters):
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
@@ -446,38 +444,38 @@ def get_columns(filters):
|
||||
|
||||
columns += [
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "cost_as_on_from_date",
|
||||
"label": _("Value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "value_as_on_from_date",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of New Purchase"),
|
||||
"fieldname": "cost_of_new_purchase",
|
||||
"label": _("Value of New Purchase"),
|
||||
"fieldname": "value_of_new_purchase",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of Sold Asset"),
|
||||
"fieldname": "cost_of_sold_asset",
|
||||
"label": _("Value of Sold Asset"),
|
||||
"fieldname": "value_of_sold_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of Scrapped Asset"),
|
||||
"fieldname": "cost_of_scrapped_asset",
|
||||
"label": _("Value of Scrapped Asset"),
|
||||
"fieldname": "value_of_scrapped_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of New Capitalized Asset"),
|
||||
"fieldname": "cost_of_capitalized_asset",
|
||||
"label": _("Value of New Capitalized Asset"),
|
||||
"fieldname": "value_of_capitalized_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "cost_as_on_to_date",
|
||||
"label": _("Value as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "value_as_on_to_date",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
|
||||
@@ -142,7 +142,8 @@ def get_journal_entries(filters):
|
||||
where jvd.parent = jv.name and jv.docstatus=1
|
||||
and jvd.account = %(account)s and jv.posting_date <= %(report_date)s
|
||||
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
|
||||
and ifnull(jv.is_opening, 'No') = 'No'""",
|
||||
and ifnull(jv.is_opening, 'No') = 'No'
|
||||
and jv.company = %(company)s """,
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -163,6 +164,7 @@ def get_payment_entries(filters):
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date <= %(report_date)s
|
||||
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
|
||||
and company = %(company)s
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
@@ -181,6 +183,7 @@ def get_pos_entries(filters):
|
||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
||||
and account.name = sip.account and si.posting_date <= %(report_date)s and
|
||||
ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s
|
||||
and si.company = %(company)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""",
|
||||
|
||||
@@ -351,7 +351,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
|
||||
gl_entries_by_account,
|
||||
accounts_by_name,
|
||||
accounts,
|
||||
ignore_closing_entries=False,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
root_type=root_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -92,5 +92,27 @@ frappe.query_reports["Customer Ledger Summary"] = {
|
||||
fieldtype: "Data",
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
erpnext.utils.add_dimensions("Customer Ledger Summary", 14);
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||
|
||||
|
||||
class PartyLedgerSummaryReport:
|
||||
def __init__(self, filters=None):
|
||||
@@ -246,95 +253,143 @@ class PartyLedgerSummaryReport:
|
||||
return out
|
||||
|
||||
def get_gl_entries(self):
|
||||
conditions = self.prepare_conditions()
|
||||
join = join_field = ""
|
||||
if self.filters.party_type == "Customer":
|
||||
join_field = ", p.customer_name as party_name"
|
||||
join = "left join `tabCustomer` p on gle.party = p.name"
|
||||
elif self.filters.party_type == "Supplier":
|
||||
join_field = ", p.supplier_name as party_name"
|
||||
join = "left join `tabSupplier` p on gle.party = p.name"
|
||||
|
||||
self.gl_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
gle.posting_date, gle.party, gle.voucher_type, gle.voucher_no, gle.against_voucher_type,
|
||||
gle.against_voucher, gle.debit, gle.credit, gle.is_opening {join_field}
|
||||
from `tabGL Entry` gle
|
||||
{join}
|
||||
where
|
||||
gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != ''
|
||||
and gle.posting_date <= %(to_date)s {conditions}
|
||||
order by gle.posting_date
|
||||
""",
|
||||
self.filters,
|
||||
as_dict=True,
|
||||
gle = qb.DocType("GL Entry")
|
||||
query = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.posting_date,
|
||||
gle.party,
|
||||
gle.voucher_type,
|
||||
gle.voucher_no,
|
||||
gle.against_voucher_type,
|
||||
gle.against_voucher,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.is_opening,
|
||||
)
|
||||
.where(
|
||||
(gle.docstatus < 2)
|
||||
& (gle.is_cancelled == 0)
|
||||
& (gle.party_type == self.filters.party_type)
|
||||
& (IfNull(gle.party, "") != "")
|
||||
& (gle.posting_date <= self.filters.to_date)
|
||||
)
|
||||
.orderby(gle.posting_date)
|
||||
)
|
||||
|
||||
def prepare_conditions(self):
|
||||
conditions = [""]
|
||||
if self.filters.party_type == "Customer":
|
||||
customer = qb.DocType("Customer")
|
||||
query = (
|
||||
query.select(customer.customer_name.as_("party_name"))
|
||||
.left_join(customer)
|
||||
.on(customer.name == gle.party)
|
||||
)
|
||||
elif self.filters.party_type == "Supplier":
|
||||
supplier = qb.DocType("Supplier")
|
||||
query = (
|
||||
query.select(supplier.supplier_name.as_("party_name"))
|
||||
.left_join(supplier)
|
||||
.on(supplier.name == gle.party)
|
||||
)
|
||||
|
||||
query = self.prepare_conditions(query)
|
||||
self.gl_entries = query.run(as_dict=True)
|
||||
|
||||
def prepare_conditions(self, query):
|
||||
gle = qb.DocType("GL Entry")
|
||||
if self.filters.company:
|
||||
conditions.append("gle.company=%(company)s")
|
||||
query = query.where(gle.company == self.filters.company)
|
||||
|
||||
if self.filters.finance_book:
|
||||
conditions.append("ifnull(finance_book,'') in (%(finance_book)s, '')")
|
||||
query = query.where(IfNull(gle.finance_book, "") == self.filters.finance_book)
|
||||
|
||||
if self.filters.get("party"):
|
||||
conditions.append("party=%(party)s")
|
||||
if self.filters.party:
|
||||
query = query.where(gle.party == self.filters.party)
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
if self.filters.get("customer_group"):
|
||||
lft, rgt = frappe.get_cached_value(
|
||||
"Customer Group", self.filters["customer_group"], ["lft", "rgt"]
|
||||
customer = qb.DocType("Customer")
|
||||
if self.filters.customer_group:
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(customer)
|
||||
.select(customer.name)
|
||||
.where(customer.customer_group == self.filters.customer_group)
|
||||
)
|
||||
)
|
||||
|
||||
conditions.append(
|
||||
f"""party in (select name from tabCustomer
|
||||
where exists(select name from `tabCustomer Group` where lft >= {lft} and rgt <= {rgt}
|
||||
and name=tabCustomer.customer_group))"""
|
||||
if self.filters.territory:
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(customer)
|
||||
.select(customer.name)
|
||||
.where(customer.territory == self.filters.territory)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.get("territory"):
|
||||
lft, rgt = frappe.db.get_value("Territory", self.filters.get("territory"), ["lft", "rgt"])
|
||||
|
||||
conditions.append(
|
||||
f"""party in (select name from tabCustomer
|
||||
where exists(select name from `tabTerritory` where lft >= {lft} and rgt <= {rgt}
|
||||
and name=tabCustomer.territory))"""
|
||||
if self.filters.payment_terms_template:
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(customer)
|
||||
.select(customer.name)
|
||||
.where(customer.payment_terms == self.filters.payment_terms_template)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
conditions.append(
|
||||
"party in (select name from tabCustomer where payment_terms=%(payment_terms_template)s)"
|
||||
if self.filters.sales_partner:
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(customer)
|
||||
.select(customer.name)
|
||||
.where(customer.default_sales_partner == self.filters.sales_partner)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
conditions.append(
|
||||
"party in (select name from tabCustomer where default_sales_partner=%(sales_partner)s)"
|
||||
)
|
||||
|
||||
if self.filters.get("sales_person"):
|
||||
lft, rgt = frappe.db.get_value(
|
||||
"Sales Person", self.filters.get("sales_person"), ["lft", "rgt"]
|
||||
)
|
||||
|
||||
conditions.append(
|
||||
"""exists(select name from `tabSales Team` steam where
|
||||
steam.sales_person in (select name from `tabSales Person` where lft >= {} and rgt <= {})
|
||||
and ((steam.parent = voucher_no and steam.parenttype = voucher_type)
|
||||
or (steam.parent = against_voucher and steam.parenttype = against_voucher_type)
|
||||
or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)
|
||||
if self.filters.sales_person:
|
||||
sales_team = qb.DocType("Sales Team")
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(sales_team)
|
||||
.select(sales_team.parent)
|
||||
.where(sales_team.sales_person == self.filters.sales_person)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.party_type == "Supplier":
|
||||
if self.filters.get("supplier_group"):
|
||||
conditions.append(
|
||||
"""party in (select name from tabSupplier
|
||||
where supplier_group=%(supplier_group)s)"""
|
||||
if self.filters.supplier_group:
|
||||
supplier = qb.DocType("Supplier")
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.supplier_group == self.filters.supplier_group)
|
||||
)
|
||||
)
|
||||
|
||||
return " and ".join(conditions)
|
||||
if self.filters.cost_center:
|
||||
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
|
||||
query = query.where((gle.cost_center).isin(self.filters.cost_center))
|
||||
|
||||
if self.filters.project:
|
||||
query = query.where((gle.project).isin(self.filters.project))
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
if self.filters.get(dimension.fieldname):
|
||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, self.filters.get(dimension.fieldname)
|
||||
)
|
||||
query = query.where(
|
||||
(gle[dimension.fieldname]).isin(self.filters.get(dimension.fieldname))
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
(gle[dimension.fieldname]).isin(self.filters.get(dimension.fieldname))
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
def get_return_invoices(self):
|
||||
doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice"
|
||||
@@ -351,53 +406,45 @@ class PartyLedgerSummaryReport:
|
||||
]
|
||||
|
||||
def get_party_adjustment_amounts(self):
|
||||
conditions = self.prepare_conditions()
|
||||
account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
|
||||
income_or_expense_accounts = frappe.db.get_all(
|
||||
self.income_or_expense_accounts = frappe.db.get_all(
|
||||
"Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
|
||||
)
|
||||
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
|
||||
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
|
||||
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
if not income_or_expense_accounts:
|
||||
if not self.income_or_expense_accounts:
|
||||
# prevent empty 'in' condition
|
||||
income_or_expense_accounts.append("")
|
||||
self.income_or_expense_accounts.append("")
|
||||
else:
|
||||
# escape '%' in account name
|
||||
# ignoring frappe.db.escape as it replaces single quotes with double quotes
|
||||
income_or_expense_accounts = [x.replace("%", "%%") for x in income_or_expense_accounts]
|
||||
self.income_or_expense_accounts = [x.replace("%", "%%") for x in self.income_or_expense_accounts]
|
||||
|
||||
accounts_query = (
|
||||
gl = qb.DocType("GL Entry")
|
||||
accounts_query = self.get_base_accounts_query()
|
||||
accounts_query_voucher_no = accounts_query.select(gl.voucher_no)
|
||||
accounts_query_voucher_type = accounts_query.select(gl.voucher_type)
|
||||
|
||||
subquery = self.get_base_subquery()
|
||||
subquery_voucher_no = subquery.select(gl.voucher_no)
|
||||
subquery_voucher_type = subquery.select(gl.voucher_type)
|
||||
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_type, gl.voucher_no)
|
||||
.where(
|
||||
(gl.account.isin(income_or_expense_accounts))
|
||||
& (gl.posting_date.gte(self.filters.from_date))
|
||||
& (gl.posting_date.lte(self.filters.to_date))
|
||||
.select(
|
||||
gl.posting_date, gl.account, gl.party, gl.voucher_type, gl.voucher_no, gl.debit, gl.credit
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
posting_date, account, party, voucher_type, voucher_no, debit, credit
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
docstatus < 2 and is_cancelled = 0
|
||||
and (voucher_type, voucher_no) in (
|
||||
{accounts_query}
|
||||
) and (voucher_type, voucher_no) in (
|
||||
select voucher_type, voucher_no from `tabGL Entry` gle
|
||||
where gle.party_type=%(party_type)s and ifnull(party, '') != ''
|
||||
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions}
|
||||
)
|
||||
""",
|
||||
self.filters,
|
||||
as_dict=True,
|
||||
)
|
||||
.where(
|
||||
(gl.docstatus < 2)
|
||||
& (gl.is_cancelled == 0)
|
||||
& (gl.voucher_no.isin(accounts_query_voucher_no))
|
||||
& (gl.voucher_type.isin(accounts_query_voucher_type))
|
||||
& (gl.voucher_no.isin(subquery_voucher_no))
|
||||
& (gl.voucher_type.isin(subquery_voucher_type))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
self.party_adjustment_details = {}
|
||||
self.party_adjustment_accounts = set()
|
||||
@@ -439,6 +486,26 @@ class PartyLedgerSummaryReport:
|
||||
self.party_adjustment_details[party].setdefault(account, 0)
|
||||
self.party_adjustment_details[party][account] += amount
|
||||
|
||||
def get_base_accounts_query(self):
|
||||
gl = qb.DocType("GL Entry")
|
||||
query = qb.from_(gl).where(
|
||||
(gl.account.isin(self.income_or_expense_accounts))
|
||||
& (gl.posting_date.gte(self.filters.from_date))
|
||||
& (gl.posting_date.lte(self.filters.to_date))
|
||||
)
|
||||
return query
|
||||
|
||||
def get_base_subquery(self):
|
||||
gl = qb.DocType("GL Entry")
|
||||
query = qb.from_(gl).where(
|
||||
(gl.docstatus < 2)
|
||||
& (gl.party_type == self.filters.party_type)
|
||||
& (IfNull(gl.party, "") != "")
|
||||
& (gl.posting_date.between(self.filters.from_date, self.filters.to_date))
|
||||
)
|
||||
query = self.prepare_conditions(query)
|
||||
return query
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
|
||||
@@ -527,9 +527,15 @@ def get_accounting_entries(
|
||||
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
|
||||
query = query.where(ExistsCriterion(account_filter_query))
|
||||
|
||||
entries = query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
return entries
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += "and" + match_conditions
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):
|
||||
|
||||
@@ -35,9 +35,6 @@ def execute(filters=None):
|
||||
if filters.get("party"):
|
||||
filters.party = frappe.parse_json(filters.get("party"))
|
||||
|
||||
if filters.get("voucher_no") and not filters.get("group_by"):
|
||||
filters.group_by = "Group by Voucher (Consolidated)"
|
||||
|
||||
validate_filters(filters, account_details)
|
||||
|
||||
validate_party(filters)
|
||||
@@ -373,16 +370,21 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
if (not filters.get("group_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
|
||||
):
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
data += acc_dict.entries
|
||||
|
||||
# totals
|
||||
data.append(acc_dict.totals.total)
|
||||
if filters.get("group_by") or not filters.voucher_no:
|
||||
data.append(acc_dict.totals.total)
|
||||
|
||||
# closing
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
if (not filters.get("group_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
|
||||
):
|
||||
data.append(acc_dict.totals.closing)
|
||||
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
|
||||
@@ -637,6 +637,7 @@ class GrossProfitGenerator:
|
||||
packed_item_row = row.copy()
|
||||
packed_item_row.warehouse = packed_item.warehouse
|
||||
packed_item_row.qty = packed_item.total_qty * -1
|
||||
packed_item_row.serial_and_batch_bundle = packed_item.serial_and_batch_bundle
|
||||
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
|
||||
|
||||
return flt(buying_amount, self.currency_precision)
|
||||
@@ -728,6 +729,7 @@ class GrossProfitGenerator:
|
||||
"voucher_no": row.parent,
|
||||
"allow_zero_valuation": True,
|
||||
"company": self.filters.company,
|
||||
"item_code": item_code,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -748,12 +750,13 @@ class GrossProfitGenerator:
|
||||
.inner_join(purchase_invoice)
|
||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||
.select(
|
||||
purchase_invoice.name,
|
||||
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||
)
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||
.where(purchase_invoice_item.item_code == item_code)
|
||||
.where(purchase_invoice.is_return == 0)
|
||||
.where(purchase_invoice_item.parenttype == "Purchase Invoice")
|
||||
)
|
||||
|
||||
if row.project:
|
||||
@@ -790,7 +793,10 @@ class GrossProfitGenerator:
|
||||
"""
|
||||
|
||||
if self.filters.group_by == "Sales Person":
|
||||
sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives"
|
||||
sales_person_cols = """, sales.sales_person,
|
||||
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount,
|
||||
sales.incentives
|
||||
"""
|
||||
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
|
||||
else:
|
||||
sales_person_cols = ""
|
||||
@@ -993,6 +999,7 @@ class GrossProfitGenerator:
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"invoice": row.parent,
|
||||
"serial_and_batch_bundle": row.serial_and_batch_bundle,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1044,6 +1051,7 @@ class GrossProfitGenerator:
|
||||
pki.rate,
|
||||
(pki.rate * pki.qty).as_("base_amount"),
|
||||
pki.parent_detail_docname,
|
||||
pki.serial_and_batch_bundle,
|
||||
)
|
||||
.where(pki.docstatus == 1)
|
||||
)
|
||||
|
||||
@@ -319,7 +319,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Rate"),
|
||||
"label": _("Tax Rate"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"options": "currency",
|
||||
|
||||
@@ -74,5 +74,27 @@ frappe.query_reports["Supplier Ledger Summary"] = {
|
||||
fieldtype: "Data",
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
erpnext.utils.add_dimensions("Supplier Ledger Summary", 11);
|
||||
|
||||
@@ -72,8 +72,8 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
if rate:
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
|
||||
total_amount = grand_total = base_total
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
|
||||
(voucher_type, name)
|
||||
@@ -409,7 +409,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
"paid_amount_after_tax",
|
||||
"base_paid_amount",
|
||||
],
|
||||
"Journal Entry": ["total_amount"],
|
||||
"Journal Entry": ["tax_withholding_category", "total_debit"],
|
||||
}
|
||||
|
||||
entries = frappe.get_all(
|
||||
@@ -431,7 +431,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
elif doctype == "Payment Entry":
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
else:
|
||||
value = [entry.total_amount] * 3
|
||||
value = [entry.total_debit] * 3
|
||||
|
||||
net_total_map[(doctype, entry.name)] = value
|
||||
|
||||
|
||||
@@ -207,3 +207,23 @@ class AccountsTestMixin:
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_price_list(self):
|
||||
pl_name = "Mixin Price List"
|
||||
if not frappe.db.exists("Price List", pl_name):
|
||||
self.price_list = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Price List",
|
||||
"currency": "INR",
|
||||
"enabled": True,
|
||||
"selling": True,
|
||||
"buying": True,
|
||||
"price_list_name": pl_name,
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.name
|
||||
)
|
||||
else:
|
||||
self.price_list = frappe.get_doc("Price List", pl_name).name
|
||||
|
||||
@@ -113,7 +113,7 @@ def get_fiscal_years(
|
||||
# No match for restricting selectors
|
||||
if raise_on_missing:
|
||||
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(
|
||||
label, formatdate(transaction_date)
|
||||
_(label), formatdate(transaction_date)
|
||||
)
|
||||
if company:
|
||||
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
|
||||
|
||||
@@ -353,7 +353,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized",
|
||||
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -592,7 +592,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-29 14:25:56.436124",
|
||||
"modified": "2024-12-26 14:23:20.968882",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -19,6 +19,7 @@ from frappe.utils import (
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_comma_separated_links,
|
||||
@@ -110,7 +111,7 @@ class Asset(AccountsController):
|
||||
"Issue",
|
||||
"Receipt",
|
||||
"Capitalized",
|
||||
"Decapitalized",
|
||||
"Work In Progress",
|
||||
]
|
||||
supplier: DF.Link | None
|
||||
total_asset_cost: DF.Currency
|
||||
@@ -309,12 +310,14 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
def validate_precision(self):
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(self.gross_purchase_amount, float_precision)
|
||||
self.gross_purchase_amount = flt(
|
||||
self.gross_purchase_amount, self.precision("gross_purchase_amount")
|
||||
)
|
||||
|
||||
if self.opening_accumulated_depreciation:
|
||||
self.opening_accumulated_depreciation = flt(
|
||||
self.opening_accumulated_depreciation, float_precision
|
||||
self.opening_accumulated_depreciation, self.precision("opening_accumulated_depreciation")
|
||||
)
|
||||
|
||||
def validate_asset_values(self):
|
||||
@@ -411,6 +414,9 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
def validate_asset_finance_books(self, row):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
)
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
|
||||
frappe.throw(
|
||||
_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format(
|
||||
@@ -431,7 +437,10 @@ class Asset(AccountsController):
|
||||
self.opening_accumulated_depreciation = 0
|
||||
self.opening_number_of_booked_depreciations = 0
|
||||
else:
|
||||
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
depreciable_amount = flt(
|
||||
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
|
||||
self.precision("gross_purchase_amount"),
|
||||
)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
frappe.throw(
|
||||
_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
|
||||
@@ -482,11 +491,7 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_expected_value_after_useful_life(self):
|
||||
for row in self.get("finance_books"):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
)
|
||||
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
||||
|
||||
if not depr_schedule:
|
||||
continue
|
||||
|
||||
@@ -799,6 +804,9 @@ class Asset(AccountsController):
|
||||
):
|
||||
return args.get("rate_of_depreciation")
|
||||
|
||||
if args.get("rate_of_depreciation") and not flt(args.get("expected_value_after_useful_life")):
|
||||
return args.get("rate_of_depreciation")
|
||||
|
||||
if self.flags.increase_in_asset_value_due_to_repair:
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(
|
||||
args.get("value_after_depreciation")
|
||||
@@ -881,6 +889,7 @@ def get_asset_naming_series():
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(asset, item_code, company, serial_no=None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
si.company = company
|
||||
si.currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
@@ -897,6 +906,16 @@ def make_sales_invoice(asset, item_code, company, serial_no=None):
|
||||
"qty": 1,
|
||||
},
|
||||
)
|
||||
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
si.update(
|
||||
{
|
||||
dimension["fieldname"]: asset_doc.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
si.set_missing_values()
|
||||
return si
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
frappe.listview_settings["Asset"] = {
|
||||
add_fields: ["status"],
|
||||
add_fields: ["status", "docstatus"],
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status === "Fully Depreciated") {
|
||||
return [__("Fully Depreciated"), "green", "status,=,Fully Depreciated"];
|
||||
@@ -7,8 +8,10 @@ frappe.listview_settings["Asset"] = {
|
||||
return [__("Partially Depreciated"), "grey", "status,=,Partially Depreciated"];
|
||||
} else if (doc.status === "Sold") {
|
||||
return [__("Sold"), "green", "status,=,Sold"];
|
||||
} else if (["Capitalized", "Decapitalized"].includes(doc.status)) {
|
||||
return [__(doc.status), "grey", "status,=," + doc.status];
|
||||
} else if (doc.status === "Work In Progress") {
|
||||
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
|
||||
} else if (doc.status === "Capitalized") {
|
||||
return [__("Capitalized"), "grey", "status,=,Capitalized"];
|
||||
} else if (doc.status === "Scrapped") {
|
||||
return [__("Scrapped"), "grey", "status,=,Scrapped"];
|
||||
} else if (doc.status === "In Maintenance") {
|
||||
@@ -21,7 +24,7 @@ frappe.listview_settings["Asset"] = {
|
||||
return [__("Receipt"), "green", "status,=,Receipt"];
|
||||
} else if (doc.status === "Submitted") {
|
||||
return [__("Submitted"), "blue", "status,=,Submitted"];
|
||||
} else if (doc.status === "Draft") {
|
||||
} else if (doc.status === "Draft" || doc.docstatus === 0) {
|
||||
return [__("Draft"), "red", "status,=,Draft"];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -436,7 +436,7 @@ def scrap_asset(asset_name, scrap_date=None):
|
||||
|
||||
if asset.docstatus != 1:
|
||||
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"):
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"):
|
||||
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
|
||||
|
||||
today_date = getdate(today())
|
||||
|
||||
@@ -916,7 +916,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
["2030-12-31", 28630.14, 28630.14],
|
||||
["2031-12-31", 35684.93, 64315.07],
|
||||
["2032-12-31", 17842.46, 82157.53],
|
||||
["2033-06-06", 5342.46, 87499.99],
|
||||
["2033-06-06", 5342.47, 87500.00],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
@@ -1752,6 +1752,10 @@ def create_asset(**args):
|
||||
},
|
||||
)
|
||||
|
||||
if asset.is_composite_asset:
|
||||
asset.gross_purchase_amount = 0
|
||||
asset.purchase_amount = 0
|
||||
|
||||
if not args.do_not_save:
|
||||
try:
|
||||
asset.insert(ignore_if_duplicate=True)
|
||||
|
||||
@@ -36,11 +36,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
me.setup_warehouse_query();
|
||||
|
||||
me.frm.set_query("target_item_code", function () {
|
||||
if (me.frm.doc.entry_type == "Capitalization") {
|
||||
return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 });
|
||||
} else {
|
||||
return erpnext.queries.item({ is_stock_item: 1, is_fixed_asset: 0 });
|
||||
}
|
||||
return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 });
|
||||
});
|
||||
|
||||
me.frm.set_query("target_asset", function () {
|
||||
@@ -51,7 +47,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
|
||||
me.frm.set_query("asset", "asset_items", function () {
|
||||
var filters = {
|
||||
status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]],
|
||||
status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized"]],
|
||||
docstatus: 1,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,30 +8,26 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"company",
|
||||
"naming_series",
|
||||
"entry_type",
|
||||
"target_item_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"column_break_9",
|
||||
"capitalization_method",
|
||||
"target_item_code",
|
||||
"target_asset_location",
|
||||
"target_item_name",
|
||||
"target_asset",
|
||||
"target_asset_name",
|
||||
"target_warehouse",
|
||||
"target_qty",
|
||||
"target_stock_uom",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"column_break_5",
|
||||
"finance_book",
|
||||
"target_asset_location",
|
||||
"column_break_9",
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"set_posting_time",
|
||||
"finance_book",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"amended_from",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"section_break_16",
|
||||
"stock_items",
|
||||
"stock_items_total",
|
||||
@@ -58,12 +54,12 @@
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')",
|
||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || doc.capitalization_method=='Create a new composite asset'",
|
||||
"fieldname": "target_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Item Code",
|
||||
"mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
@@ -84,22 +80,18 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"fieldname": "target_asset",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Asset",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"no_copy": 1,
|
||||
"options": "Asset",
|
||||
"read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')"
|
||||
"read_only_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"fetch_from": "target_asset.asset_name",
|
||||
"fieldname": "target_asset_name",
|
||||
"fieldtype": "Data",
|
||||
@@ -162,7 +154,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))",
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consumed Stock Items"
|
||||
@@ -173,14 +165,6 @@
|
||||
"label": "Stock Items",
|
||||
"options": "Asset Capitalization Stock Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fieldname": "target_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Warehouse",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "target_has_batch_no",
|
||||
"fieldname": "target_batch_no",
|
||||
@@ -190,20 +174,9 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fieldname": "target_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Target Qty",
|
||||
"read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fetch_from": "target_item_code.stock_uom",
|
||||
"fieldname": "target_stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
"label": "Target Qty"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -241,16 +214,6 @@
|
||||
"label": "Assets",
|
||||
"options": "Asset Capitalization Asset Item"
|
||||
},
|
||||
{
|
||||
"default": "Capitalization",
|
||||
"fieldname": "entry_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Entry Type",
|
||||
"options": "Capitalization\nDecapitalization",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_items_total",
|
||||
"fieldtype": "Currency",
|
||||
@@ -272,7 +235,7 @@
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))",
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
|
||||
"fieldname": "service_expenses_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Service Expenses"
|
||||
@@ -337,26 +300,24 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
|
||||
"depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"fieldname": "target_asset_location",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Asset Location",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"options": "Location"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fieldname": "capitalization_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Capitalization Method",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:33.080441",
|
||||
"modified": "2025-01-08 13:14:33.008458",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
|
||||
@@ -42,7 +42,6 @@ force_fields = [
|
||||
"target_is_fixed_asset",
|
||||
"target_has_serial_no",
|
||||
"target_has_batch_no",
|
||||
"target_stock_uom",
|
||||
"stock_uom",
|
||||
"fixed_asset_account",
|
||||
"valuation_rate",
|
||||
@@ -74,7 +73,6 @@ class AssetCapitalization(StockController):
|
||||
capitalization_method: DF.Literal["", "Create a new composite asset", "Choose a WIP composite asset"]
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
entry_type: DF.Literal["Capitalization", "Decapitalization"]
|
||||
finance_book: DF.Link | None
|
||||
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
|
||||
posting_date: DF.Date
|
||||
@@ -97,8 +95,6 @@ class AssetCapitalization(StockController):
|
||||
target_item_name: DF.Data | None
|
||||
target_qty: DF.Float
|
||||
target_serial_no: DF.SmallText | None
|
||||
target_stock_uom: DF.Link | None
|
||||
target_warehouse: DF.Link | None
|
||||
title: DF.Data | None
|
||||
total_value: DF.Currency
|
||||
# end: auto-generated types
|
||||
@@ -191,31 +187,18 @@ class AssetCapitalization(StockController):
|
||||
def validate_target_item(self):
|
||||
target_item = frappe.get_cached_doc("Item", self.target_item_code)
|
||||
|
||||
if not target_item.is_fixed_asset and not target_item.is_stock_item:
|
||||
frappe.throw(
|
||||
_("Target Item {0} is neither a Fixed Asset nor a Stock Item").format(target_item.name)
|
||||
)
|
||||
|
||||
if self.entry_type == "Capitalization" and not target_item.is_fixed_asset:
|
||||
if not target_item.is_fixed_asset:
|
||||
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
|
||||
elif self.entry_type == "Decapitalization" and not target_item.is_stock_item:
|
||||
frappe.throw(_("Target Item {0} must be a Stock Item").format(target_item.name))
|
||||
|
||||
if target_item.is_fixed_asset:
|
||||
self.target_qty = 1
|
||||
if flt(self.target_qty) <= 0:
|
||||
frappe.throw(_("Target Qty must be a positive number"))
|
||||
|
||||
if not target_item.is_stock_item:
|
||||
self.target_warehouse = None
|
||||
if not target_item.has_batch_no:
|
||||
self.target_batch_no = None
|
||||
if not target_item.has_serial_no:
|
||||
self.target_serial_no = ""
|
||||
|
||||
if target_item.is_stock_item and not self.target_warehouse:
|
||||
frappe.throw(_("Target Warehouse is mandatory for Decapitalization"))
|
||||
|
||||
self.validate_item(target_item)
|
||||
|
||||
def validate_target_asset(self):
|
||||
@@ -232,7 +215,7 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"):
|
||||
if target_asset.status in ("Scrapped", "Sold", "Capitalized"):
|
||||
frappe.throw(
|
||||
_("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status)
|
||||
)
|
||||
@@ -274,7 +257,7 @@ class AssetCapitalization(StockController):
|
||||
|
||||
asset = self.get_asset_for_validation(d.asset)
|
||||
|
||||
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
|
||||
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized"):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Consumed Asset {1} cannot be {2}").format(
|
||||
d.idx, asset.name, asset.status
|
||||
@@ -315,9 +298,6 @@ class AssetCapitalization(StockController):
|
||||
d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
def validate_source_mandatory(self):
|
||||
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||
|
||||
if self.capitalization_method == "Create a new composite asset" and not (
|
||||
self.get("stock_items") or self.get("asset_items")
|
||||
):
|
||||
@@ -421,18 +401,6 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
|
||||
if self.entry_type == "Decapitalization" and not self.target_is_fixed_asset:
|
||||
sle = self.get_sl_entries(
|
||||
self,
|
||||
{
|
||||
"item_code": self.target_item_code,
|
||||
"warehouse": self.target_warehouse,
|
||||
"actual_qty": flt(self.target_qty),
|
||||
"incoming_rate": flt(self.target_incoming_rate),
|
||||
},
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
|
||||
# reverse sl entries if cancel
|
||||
if self.docstatus == 2:
|
||||
sl_entries.reverse()
|
||||
@@ -475,21 +443,18 @@ class AssetCapitalization(StockController):
|
||||
return gl_entries
|
||||
|
||||
def get_target_account(self):
|
||||
if self.target_is_fixed_asset:
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
|
||||
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
|
||||
if is_cwip_accounting_enabled(asset_category):
|
||||
target_account = get_asset_category_account(
|
||||
"capital_work_in_progress_account",
|
||||
asset_category=asset_category,
|
||||
company=self.company,
|
||||
)
|
||||
return target_account if target_account else self.target_fixed_asset_account
|
||||
else:
|
||||
return self.target_fixed_asset_account
|
||||
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
|
||||
if is_cwip_accounting_enabled(asset_category):
|
||||
target_account = get_asset_category_account(
|
||||
"capital_work_in_progress_account",
|
||||
asset_category=asset_category,
|
||||
company=self.company,
|
||||
)
|
||||
return target_account if target_account else self.target_fixed_asset_account
|
||||
else:
|
||||
return self.warehouse_account[self.target_warehouse]["account"]
|
||||
return self.target_fixed_asset_account
|
||||
|
||||
def get_gl_entries_for_consumed_stock_items(self, gl_entries, target_account, target_against, precision):
|
||||
# Consumed Stock Items
|
||||
@@ -590,33 +555,9 @@ class AssetCapitalization(StockController):
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Target Stock Item
|
||||
sle_list = self.sle_map.get(self.name)
|
||||
for sle in sle_list:
|
||||
stock_value_difference = flt(sle.stock_value_difference, precision)
|
||||
account = self.warehouse_account[sle.warehouse]["account"]
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": ", ".join(target_against),
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"debit": stock_value_difference,
|
||||
},
|
||||
self.warehouse_account[sle.warehouse]["account_currency"],
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def create_target_asset(self):
|
||||
if (
|
||||
self.entry_type != "Capitalization"
|
||||
or self.capitalization_method != "Create a new composite asset"
|
||||
):
|
||||
if self.capitalization_method != "Create a new composite asset":
|
||||
return
|
||||
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
@@ -639,6 +580,7 @@ class AssetCapitalization(StockController):
|
||||
self.target_fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
|
||||
)
|
||||
asset_doc.set_status("Work In Progress")
|
||||
|
||||
add_asset_activity(
|
||||
asset_doc.name,
|
||||
@@ -654,17 +596,15 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
|
||||
def update_target_asset(self):
|
||||
if (
|
||||
self.entry_type != "Capitalization"
|
||||
or self.capitalization_method != "Choose a WIP composite asset"
|
||||
):
|
||||
if self.capitalization_method != "Choose a WIP composite asset":
|
||||
return
|
||||
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_amount = total_target_asset_value
|
||||
asset_doc.gross_purchase_amount += total_target_asset_value
|
||||
asset_doc.purchase_amount += total_target_asset_value
|
||||
asset_doc.set_status("Work In Progress")
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.save()
|
||||
|
||||
@@ -699,14 +639,6 @@ class AssetCapitalization(StockController):
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
else:
|
||||
asset.set_status("Decapitalized")
|
||||
add_asset_activity(
|
||||
asset.name,
|
||||
_("Asset decapitalized after Asset Capitalization {0} was submitted").format(
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
else:
|
||||
asset.set_status()
|
||||
add_asset_activity(
|
||||
@@ -728,16 +660,12 @@ def get_target_item_details(item_code=None, company=None):
|
||||
|
||||
# Set Item Details
|
||||
out.target_item_name = item.item_name
|
||||
out.target_stock_uom = item.stock_uom
|
||||
out.target_is_fixed_asset = cint(item.is_fixed_asset)
|
||||
out.target_has_batch_no = cint(item.has_batch_no)
|
||||
out.target_has_serial_no = cint(item.has_serial_no)
|
||||
|
||||
if out.target_is_fixed_asset:
|
||||
out.target_qty = 1
|
||||
out.target_warehouse = None
|
||||
else:
|
||||
out.target_asset = None
|
||||
|
||||
if not out.target_has_batch_no:
|
||||
out.target_batch_no = None
|
||||
|
||||
@@ -61,7 +61,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
capitalization_method="Create a new composite asset",
|
||||
target_item_code="Macbook Pro",
|
||||
target_asset_location="Test Location",
|
||||
@@ -76,7 +75,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
|
||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||
@@ -96,6 +94,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.status, "Work In Progress")
|
||||
|
||||
# Test Consumed Asset values
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||
@@ -151,7 +150,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
capitalization_method="Create a new composite asset",
|
||||
target_item_code="Macbook Pro",
|
||||
target_asset_location="Test Location",
|
||||
@@ -166,7 +164,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
|
||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||
@@ -243,7 +240,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
capitalization_method="Choose a WIP composite asset",
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
@@ -255,7 +251,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
|
||||
self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset")
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
|
||||
@@ -270,6 +265,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.status, "Work In Progress")
|
||||
|
||||
# Test General Ledger Entries
|
||||
expected_gle = {
|
||||
@@ -295,110 +291,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
def test_decapitalization_with_depreciation(self):
|
||||
# Variables
|
||||
purchase_date = "2020-01-01"
|
||||
depreciation_start_date = "2020-12-31"
|
||||
capitalization_date = "2021-06-30"
|
||||
|
||||
total_number_of_depreciations = 3
|
||||
expected_value_after_useful_life = 10_000
|
||||
consumed_asset_purchase_value = 100_000
|
||||
consumed_asset_current_value = 70_000
|
||||
consumed_asset_value_before_disposal = 55_000
|
||||
|
||||
target_qty = 10
|
||||
target_incoming_rate = 5500
|
||||
|
||||
depreciation_before_disposal_amount = 15_000
|
||||
accumulated_depreciation = 45_000
|
||||
|
||||
# to accomodate for depreciation on disposal calculation minor difference
|
||||
consumed_asset_value_before_disposal = 55_123.29
|
||||
target_incoming_rate = 5512.329
|
||||
depreciation_before_disposal_amount = 14_876.71
|
||||
accumulated_depreciation = 44_876.71
|
||||
|
||||
# Create assets
|
||||
consumed_asset = create_depreciation_asset(
|
||||
asset_name="Asset Capitalization Consumable Asset",
|
||||
asset_value=consumed_asset_purchase_value,
|
||||
purchase_date=purchase_date,
|
||||
depreciation_start_date=depreciation_start_date,
|
||||
depreciation_method="Straight Line",
|
||||
total_number_of_depreciations=total_number_of_depreciations,
|
||||
frequency_of_depreciation=12,
|
||||
expected_value_after_useful_life=expected_value_after_useful_life,
|
||||
company="_Test Company with perpetual inventory",
|
||||
submit=1,
|
||||
)
|
||||
|
||||
first_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Active")
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Decapitalization",
|
||||
posting_date=capitalization_date, # half a year
|
||||
target_item_code="Capitalization Target Stock Item",
|
||||
target_qty=target_qty,
|
||||
consumed_asset=consumed_asset.name,
|
||||
company="_Test Company with perpetual inventory",
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.entry_type, "Decapitalization")
|
||||
|
||||
self.assertEqual(
|
||||
asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value
|
||||
)
|
||||
self.assertEqual(
|
||||
asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal
|
||||
)
|
||||
self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value_before_disposal)
|
||||
|
||||
self.assertEqual(asset_capitalization.total_value, consumed_asset_value_before_disposal)
|
||||
self.assertEqual(asset_capitalization.target_incoming_rate, target_incoming_rate)
|
||||
|
||||
# Test Consumed Asset values
|
||||
consumed_asset.reload()
|
||||
self.assertEqual(consumed_asset.status, "Decapitalized")
|
||||
|
||||
first_asset_depr_schedule.load_from_db()
|
||||
|
||||
second_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
|
||||
self.assertEqual(second_asset_depr_schedule.status, "Active")
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||
|
||||
depr_schedule_of_consumed_asset = second_asset_depr_schedule.get("depreciation_schedule")
|
||||
|
||||
consumed_depreciation_schedule = [
|
||||
d
|
||||
for d in depr_schedule_of_consumed_asset
|
||||
if getdate(d.schedule_date) == getdate(capitalization_date)
|
||||
]
|
||||
self.assertTrue(consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry)
|
||||
self.assertEqual(
|
||||
consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount
|
||||
)
|
||||
|
||||
# Test General Ledger Entries
|
||||
expected_gle = {
|
||||
"_Test Warehouse - TCP1": consumed_asset_value_before_disposal,
|
||||
"_Test Accumulated Depreciations - TCP1": accumulated_depreciation,
|
||||
"_Test Fixed Asset - TCP1": -consumed_asset_purchase_value,
|
||||
}
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
self.assertEqual(actual_gle, expected_gle)
|
||||
|
||||
# Cancel Asset Capitalization and make test entries and status are reversed
|
||||
asset_capitalization.reload()
|
||||
asset_capitalization.cancel()
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Partially Depreciated")
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
def test_capitalize_only_service_item(self):
|
||||
company = "_Test Company"
|
||||
# Variables
|
||||
@@ -418,7 +310,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
capitalization_method="Choose a WIP composite asset",
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
@@ -466,13 +357,11 @@ def create_asset_capitalization(**args):
|
||||
target_item_code = target_asset.item_code or args.target_item_code
|
||||
company = target_asset.company or args.company or "_Test Company"
|
||||
warehouse = args.warehouse or create_warehouse("_Test Warehouse", company=company)
|
||||
target_warehouse = args.target_warehouse or warehouse
|
||||
source_warehouse = args.source_warehouse or warehouse
|
||||
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
asset_capitalization.update(
|
||||
{
|
||||
"entry_type": args.entry_type or "Capitalization",
|
||||
"capitalization_method": args.capitalization_method or None,
|
||||
"company": company,
|
||||
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
|
||||
@@ -480,7 +369,6 @@ def create_asset_capitalization(**args):
|
||||
"target_item_code": target_item_code,
|
||||
"target_asset": target_asset.name,
|
||||
"target_asset_location": "Test Location",
|
||||
"target_warehouse": target_warehouse,
|
||||
"target_qty": flt(args.target_qty) or 1,
|
||||
"target_batch_no": args.target_batch_no,
|
||||
"target_serial_no": args.target_serial_no,
|
||||
|
||||
@@ -13,6 +13,7 @@ from frappe.utils import (
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
is_last_day_of_the_month,
|
||||
month_diff,
|
||||
@@ -344,7 +345,7 @@ class AssetDepreciationSchedule(Document):
|
||||
date_of_disposal,
|
||||
original_schedule_date=schedule_date,
|
||||
)
|
||||
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
if depreciation_amount > 0:
|
||||
self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
|
||||
|
||||
@@ -430,6 +431,7 @@ class AssetDepreciationSchedule(Document):
|
||||
|
||||
if not depreciation_amount:
|
||||
continue
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
value_after_depreciation = flt(
|
||||
value_after_depreciation - flt(depreciation_amount),
|
||||
asset_doc.precision("gross_purchase_amount"),
|
||||
@@ -443,6 +445,7 @@ class AssetDepreciationSchedule(Document):
|
||||
depreciation_amount += flt(value_after_depreciation) - flt(
|
||||
row.expected_value_after_useful_life
|
||||
)
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
skip_row = True
|
||||
|
||||
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
|
||||
@@ -517,10 +520,13 @@ class AssetDepreciationSchedule(Document):
|
||||
i - 1
|
||||
].accumulated_depreciation_amount
|
||||
else:
|
||||
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||
accumulated_depreciation = flt(
|
||||
self.opening_accumulated_depreciation,
|
||||
asset_doc.precision("opening_accumulated_depreciation"),
|
||||
)
|
||||
|
||||
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
value_after_depreciation -= flt(d.depreciation_amount)
|
||||
value_after_depreciation = flt(value_after_depreciation, d.precision("depreciation_amount"))
|
||||
|
||||
# for the last row, if depreciation method = Straight Line
|
||||
if (
|
||||
@@ -530,12 +536,11 @@ class AssetDepreciationSchedule(Document):
|
||||
and not date_of_return
|
||||
and not row.shift_based
|
||||
):
|
||||
depreciation_amount += flt(
|
||||
d.depreciation_amount += flt(
|
||||
value_after_depreciation - flt(row.expected_value_after_useful_life),
|
||||
d.precision("depreciation_amount"),
|
||||
)
|
||||
|
||||
d.depreciation_amount = depreciation_amount
|
||||
accumulated_depreciation += d.depreciation_amount
|
||||
d.accumulated_depreciation_amount = flt(
|
||||
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
|
||||
@@ -1058,7 +1063,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
if not current_asset_depr_schedule_doc:
|
||||
frappe.throw(
|
||||
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
|
||||
asset_doc.name, row.finance_book
|
||||
get_link_to_form("Asset", asset_doc.name), row.finance_book
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1104,7 +1109,7 @@ def get_temp_asset_depr_schedule_doc(
|
||||
if not current_asset_depr_schedule_doc:
|
||||
frappe.throw(
|
||||
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
|
||||
asset_doc.name, row.finance_book
|
||||
get_link_to_form("Asset", asset_doc.name), row.finance_book
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@
|
||||
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
|
||||
"fieldname": "rate_of_depreciation",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Rate of Depreciation (%)"
|
||||
"label": "Rate of Depreciation (%)",
|
||||
"mandatory_depends_on": "eval:doc.depreciation_method == 'Written Down Value'"
|
||||
},
|
||||
{
|
||||
"fieldname": "salvage_value_percentage",
|
||||
@@ -128,7 +129,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-29 14:36:54.399034",
|
||||
"modified": "2024-12-13 12:11:03.743209",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Asset",
|
||||
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\",false]]",
|
||||
"filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\"]]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "New Assets (This Year)",
|
||||
"modified": "2020-07-23 13:45:20.418766",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "New Assets (This Year)",
|
||||
|
||||
@@ -66,12 +66,12 @@ def get_conditions(filters):
|
||||
conditions["cost_center"] = filters.get("cost_center")
|
||||
|
||||
if status:
|
||||
# In Store assets are those that are not sold or scrapped or capitalized or decapitalized
|
||||
# In Store assets are those that are not sold or scrapped or capitalized
|
||||
operand = "not in"
|
||||
if status not in "In Location":
|
||||
operand = "in"
|
||||
|
||||
conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized", "Decapitalized"])
|
||||
conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized"])
|
||||
|
||||
return conditions
|
||||
|
||||
@@ -272,9 +272,9 @@ def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
query = query.where(asset.cost_center == filters.cost_center)
|
||||
if filters.status:
|
||||
if filters.status == "In Location":
|
||||
query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
|
||||
query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized"]))
|
||||
else:
|
||||
query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
|
||||
query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized"]))
|
||||
if finance_book:
|
||||
query = query.where((gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull()))
|
||||
else:
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
"doctype": "Dashboard Chart",
|
||||
"document_type": "Material Request",
|
||||
"dynamic_filters_json": "[[\"Material Request\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Material Request\",\"status\",\"not in\",[\"Draft\",\"Cancelled\",\"Stopped\",null],false],[\"Material Request\",\"material_request_type\",\"=\",\"Purchase\",false],[\"Material Request\",\"docstatus\",\"=\",\"1\",false],[\"Material Request\",\"transaction_date\",\"Timespan\",\"last quarter\",false]]",
|
||||
"filters_json": "[[\"Material Request\",\"status\",\"not in\",[\"Draft\",\"Cancelled\",\"Stopped\",null]],[\"Material Request\",\"material_request_type\",\"=\",\"Purchase\"],[\"Material Request\",\"docstatus\",\"=\",\"1\"],[\"Material Request\",\"transaction_date\",\"Timespan\",\"last quarter\"]]",
|
||||
"group_by_based_on": "status",
|
||||
"group_by_type": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"last_synced_on": "2020-07-22 12:43:56.961250",
|
||||
"modified": "2020-07-22 21:20:51.840194",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Material Request Analysis",
|
||||
|
||||
@@ -402,13 +402,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
me.make_subcontracting_order();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
if (!doc.items.every((item) => item.qty == item.sco_qty)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
me.make_subcontracting_order();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +469,9 @@ class PurchaseOrder(BuyingController):
|
||||
if self.is_against_so():
|
||||
self.update_status_updater()
|
||||
|
||||
if self.is_against_pp():
|
||||
self.update_status_updater_if_from_pp()
|
||||
|
||||
self.update_prevdoc_status()
|
||||
if not self.is_subcontracted or self.is_old_subcontracting_flow:
|
||||
self.update_requested_qty()
|
||||
@@ -550,6 +553,20 @@ class PurchaseOrder(BuyingController):
|
||||
}
|
||||
)
|
||||
|
||||
def update_status_updater_if_from_pp(self):
|
||||
self.status_updater.append(
|
||||
{
|
||||
"source_dt": "Purchase Order Item",
|
||||
"target_dt": "Production Plan Sub Assembly Item",
|
||||
"join_field": "production_plan_sub_assembly_item",
|
||||
"target_field": "received_qty",
|
||||
"target_parent_dt": "Production Plan",
|
||||
"target_parent_field": "",
|
||||
"target_ref_field": "qty",
|
||||
"source_field": "fg_item_qty",
|
||||
}
|
||||
)
|
||||
|
||||
def update_delivered_qty_in_sales_order(self):
|
||||
"""Update delivered qty in Sales Order for drop ship"""
|
||||
sales_orders_to_update = []
|
||||
@@ -570,6 +587,9 @@ class PurchaseOrder(BuyingController):
|
||||
def is_against_so(self):
|
||||
return any(d.sales_order for d in self.items if d.sales_order)
|
||||
|
||||
def is_against_pp(self):
|
||||
return any(d.production_plan for d in self.items if d.production_plan)
|
||||
|
||||
def set_received_qty_for_drop_ship_items(self):
|
||||
for item in self.items:
|
||||
if item.delivered_by_supplier == 1:
|
||||
@@ -871,27 +891,40 @@ def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_order(source_name, target_doc=None, save=False, submit=False, notify=False):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
if not is_po_fully_subcontracted(source_name):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
return target_doc
|
||||
else:
|
||||
frappe.throw(_("This PO has been fully subcontracted."))
|
||||
|
||||
|
||||
def is_po_fully_subcontracted(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.sco_qty))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
@@ -943,7 +976,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": [],
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.sco_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
@@ -951,12 +985,3 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_subcontracting_order_created(po_name) -> bool:
|
||||
return (
|
||||
True
|
||||
if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]})
|
||||
else False
|
||||
)
|
||||
|
||||
@@ -1025,7 +1025,7 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
)
|
||||
|
||||
def update_items(po, qty):
|
||||
trans_items = [po.items[0].as_dict()]
|
||||
trans_items = [po.items[0].as_dict().update({"docname": po.items[0].name})]
|
||||
trans_items[0]["qty"] = qty
|
||||
trans_items[0]["fg_item_qty"] = qty
|
||||
trans_items = json.dumps(trans_items, default=str)
|
||||
@@ -1080,6 +1080,73 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertEqual(po.items[0].qty, 30)
|
||||
self.assertEqual(po.items[0].fg_item_qty, 30)
|
||||
|
||||
def test_new_sc_flow(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order
|
||||
|
||||
po = create_po_for_sc_testing()
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
sco.items[0].qty = 5
|
||||
sco.items.pop(1)
|
||||
sco.items[1].qty = 25
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 1: Quantity of Service Items should change based on change in Quantity of its corresponding Finished Goods Item
|
||||
self.assertEqual(sco.service_items[0].qty, 5)
|
||||
|
||||
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].sco_qty, 5)
|
||||
self.assertEqual(po.items[1].sco_qty, 0)
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
|
||||
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
|
||||
self.assertEqual(sco.items[0].amount, 2000)
|
||||
self.assertEqual(sco.service_items[0].amount, 500)
|
||||
|
||||
# Test - 4: Service Items should be removed if its corresponding Finished Good line item is deleted
|
||||
self.assertEqual(len(sco.service_items), 2)
|
||||
|
||||
# Test - 5: Service Item quantity calculation should be based upon conversion factor calculated from its corresponding PO Item
|
||||
self.assertEqual(sco.service_items[1].qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
sco.items[0].qty = 6
|
||||
|
||||
# Test - 6: Saving document should not be allowed if Quantity exceeds available Subcontracting Quantity of any Purchase Order Item
|
||||
self.assertRaises(frappe.ValidationError, sco.save)
|
||||
|
||||
sco.items[0].qty = 5
|
||||
sco.items.pop()
|
||||
sco.items.pop()
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
# Test - 7: Since line item 1 is now fully subcontracted, new SCO should by default only have the remaining 2 line items
|
||||
self.assertEqual(len(sco.items), 2)
|
||||
|
||||
sco.items.pop(0)
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 25)
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 8: Since this PO is now fully subcontracted, creating a new SCO from it should throw error
|
||||
self.assertRaises(frappe.ValidationError, make_subcontracting_order, po.name)
|
||||
|
||||
@IntegrationTestCase.change_settings("Buying Settings", {"auto_create_subcontracting_order": 1})
|
||||
def test_auto_create_subcontracting_order(self):
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
@@ -1173,6 +1240,53 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertEqual(po.per_billed, 100)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
make_bom_for_subcontracted_items,
|
||||
make_raw_materials,
|
||||
make_service_items,
|
||||
make_subcontracted_items,
|
||||
)
|
||||
|
||||
make_subcontracted_items()
|
||||
make_raw_materials()
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA1",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 2",
|
||||
"qty": 20,
|
||||
"rate": 25,
|
||||
"fg_item": "Subcontracted Item SA2",
|
||||
"fg_item_qty": 15,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 3",
|
||||
"qty": 25,
|
||||
"rate": 10,
|
||||
"fg_item": "Subcontracted Item SA3",
|
||||
"fg_item_qty": 50,
|
||||
},
|
||||
]
|
||||
|
||||
return create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-05-24 19:29:06",
|
||||
"creation": "2024-12-09 12:54:24.652161",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
@@ -26,6 +26,7 @@
|
||||
"quantity_and_rate",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"sco_qty",
|
||||
"col_break2",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
@@ -929,13 +930,21 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "sco_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-02 06:20:10.508290",
|
||||
"modified": "2024-12-10 12:11:18.536089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -52,6 +52,7 @@ class PurchaseOrderItem(Document):
|
||||
item_name: DF.Data
|
||||
item_tax_rate: DF.Code | None
|
||||
item_tax_template: DF.Link | None
|
||||
job_card: DF.Link | None
|
||||
last_purchase_rate: DF.Currency
|
||||
manufacturer: DF.Link | None
|
||||
manufacturer_part_no: DF.Data | None
|
||||
@@ -81,6 +82,7 @@ class PurchaseOrderItem(Document):
|
||||
sales_order_item: DF.Data | None
|
||||
sales_order_packed_item: DF.Data | None
|
||||
schedule_date: DF.Date
|
||||
sco_qty: DF.Float
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
stock_uom_rate: DF.Currency
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Purchase Order",
|
||||
"dynamic_filters_json": "[[\"Purchase Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Purchase Order\",\"transaction_date\",\"Timespan\",\"this year\",false],[\"Purchase Order\",\"status\",\"not in\",[\"Draft\",\"Cancelled\",\"Closed\",null],false],[\"Purchase Order\",\"docstatus\",\"=\",\"1\",false]]",
|
||||
"filters_json": "[[\"Purchase Order\",\"transaction_date\",\"Timespan\",\"this year\"],[\"Purchase Order\",\"status\",\"not in\",[\"Draft\",\"Cancelled\",\"Closed\",null]],[\"Purchase Order\",\"docstatus\",\"=\",\"1\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Annual Purchase",
|
||||
"modified": "2020-07-22 21:21:58.755188",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Annual Purchase",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Purchase Order",
|
||||
"dynamic_filters_json": "[[\"Purchase Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Purchase Order\",\"status\",\"in\",[\"To Receive and Bill\",\"To Bill\",null],false],[\"Purchase Order\",\"docstatus\",\"=\",1,false]]",
|
||||
"filters_json": "[[\"Purchase Order\",\"status\",\"in\",[\"To Receive and Bill\",\"To Bill\",null]],[\"Purchase Order\",\"docstatus\",\"=\",1]]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Purchase Orders to Bill",
|
||||
"modified": "2020-07-22 12:48:10.300711",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Orders to Bill",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Purchase Order",
|
||||
"dynamic_filters_json": "[[\"Purchase Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Purchase Order\",\"status\",\"in\",[\"To Receive and Bill\",\"To Receive\",null],false],[\"Purchase Order\",\"docstatus\",\"=\",1,false]]",
|
||||
"filters_json": "[[\"Purchase Order\",\"status\",\"in\",[\"To Receive and Bill\",\"To Receive\",null]],[\"Purchase Order\",\"docstatus\",\"=\",1]]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Purchase Orders to Receive",
|
||||
"modified": "2020-07-22 12:47:47.460080",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Orders to Receive",
|
||||
|
||||
@@ -18,11 +18,12 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
update_received_amount(data)
|
||||
|
||||
if not data:
|
||||
return [], [], None, []
|
||||
|
||||
update_received_amount(data)
|
||||
|
||||
data, chart_data = prepare_data(data, filters)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
@@ -103,6 +104,11 @@ def get_received_amount_data(data):
|
||||
pr = frappe.qb.DocType("Purchase Receipt")
|
||||
pr_item = frappe.qb.DocType("Purchase Receipt Item")
|
||||
|
||||
po_items = [row.name for row in data]
|
||||
|
||||
if not po_items:
|
||||
return frappe._dict()
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pr)
|
||||
.inner_join(pr_item)
|
||||
@@ -111,12 +117,10 @@ def get_received_amount_data(data):
|
||||
pr_item.purchase_order_item,
|
||||
Sum(pr_item.base_amount).as_("received_qty_amount"),
|
||||
)
|
||||
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
|
||||
.where((pr.docstatus == 1) & (pr_item.purchase_order_item.isin(po_items)))
|
||||
.groupby(pr_item.purchase_order_item)
|
||||
)
|
||||
|
||||
query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
|
||||
|
||||
data = query.run()
|
||||
|
||||
if not data:
|
||||
@@ -180,7 +184,7 @@ def prepare_data(data, filters):
|
||||
|
||||
|
||||
def prepare_chart_data(pending, completed):
|
||||
labels = ["Amount to Bill", "Billed Amount"]
|
||||
labels = [_("Amount to Bill"), _("Billed Amount")]
|
||||
|
||||
return {
|
||||
"data": {"labels": labels, "datasets": [{"values": [pending, completed]}]},
|
||||
|
||||
@@ -383,13 +383,14 @@ class AccountsController(TransactionBase):
|
||||
== 1
|
||||
)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
|
||||
)
|
||||
frappe.db.sql(
|
||||
"delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s",
|
||||
(self.doctype, self.name),
|
||||
)
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
frappe.qb.from_(gle).delete().where(
|
||||
(gle.voucher_type == self.doctype) & (gle.voucher_no == self.name)
|
||||
).run()
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
frappe.qb.from_(sle).delete().where(
|
||||
(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
|
||||
).run()
|
||||
|
||||
def remove_serial_and_batch_bundle(self):
|
||||
bundles = frappe.get_all(
|
||||
@@ -467,9 +468,16 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_invoice_documents_schedule(self):
|
||||
if self.is_return:
|
||||
if (
|
||||
self.is_return
|
||||
or (self.doctype == "Purchase Invoice" and self.is_paid)
|
||||
or (self.doctype == "Sales Invoice" and self.is_pos)
|
||||
or self.get("is_opening") == "Yes"
|
||||
):
|
||||
self.payment_terms_template = ""
|
||||
self.payment_schedule = []
|
||||
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
self.validate_payment_schedule_dates()
|
||||
@@ -663,21 +671,15 @@ class AccountsController(TransactionBase):
|
||||
if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date):
|
||||
self.due_date = posting_date
|
||||
|
||||
elif self.doctype == "Sales Invoice":
|
||||
if not self.due_date:
|
||||
frappe.throw(_("Due Date is mandatory"))
|
||||
elif self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
bill_date = self.bill_date if self.doctype == "Purchase Invoice" else None
|
||||
|
||||
validate_due_date(
|
||||
posting_date,
|
||||
self.due_date,
|
||||
self.payment_terms_template,
|
||||
)
|
||||
elif self.doctype == "Purchase Invoice":
|
||||
validate_due_date(
|
||||
posting_date,
|
||||
self.due_date,
|
||||
self.bill_date,
|
||||
self.payment_terms_template,
|
||||
posting_date=posting_date,
|
||||
due_date=self.due_date,
|
||||
bill_date=bill_date,
|
||||
template_name=self.payment_terms_template,
|
||||
doctype=self.doctype,
|
||||
)
|
||||
|
||||
def set_price_list_currency(self, buying_or_selling):
|
||||
@@ -1164,11 +1166,12 @@ class AccountsController(TransactionBase):
|
||||
def clear_unallocated_advances(self, childtype, parentfield):
|
||||
self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]}))
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tab{}` where parentfield={} and parent = {}
|
||||
and allocated_amount = 0""".format(childtype, "%s", "%s"),
|
||||
(parentfield, self.name),
|
||||
)
|
||||
doctype = frappe.qb.DocType(childtype)
|
||||
frappe.qb.from_(doctype).delete().where(
|
||||
(doctype.parentfield == parentfield)
|
||||
& (doctype.parent == self.name)
|
||||
& (doctype.allocated_amount == 0)
|
||||
).run()
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_shipping_rule(self):
|
||||
@@ -1218,6 +1221,7 @@ class AccountsController(TransactionBase):
|
||||
"advance_amount": flt(d.amount),
|
||||
"allocated_amount": allocated_amount,
|
||||
"ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry
|
||||
"difference_posting_date": self.posting_date,
|
||||
}
|
||||
if d.get("paid_from"):
|
||||
advance_row["account"] = d.paid_from
|
||||
@@ -1228,7 +1232,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def get_advance_entries(self, include_unallocated=True):
|
||||
party_account = []
|
||||
if self.doctype == "Sales Invoice":
|
||||
default_advance_account = None
|
||||
|
||||
if self.doctype in ["Sales Invoice", "POS Invoice"]:
|
||||
party_type = "Customer"
|
||||
party = self.customer
|
||||
amount_field = "credit_in_account_currency"
|
||||
@@ -1243,10 +1249,14 @@ class AccountsController(TransactionBase):
|
||||
order_doctype = "Purchase Order"
|
||||
party_account.append(self.credit_to)
|
||||
|
||||
party_account.extend(
|
||||
get_party_account(party_type, party=party, company=self.company, include_advance=True)
|
||||
party_accounts = get_party_account(
|
||||
party_type, party=party, company=self.company, include_advance=True
|
||||
)
|
||||
|
||||
if party_accounts:
|
||||
party_account.append(party_accounts[0])
|
||||
default_advance_account = party_accounts[1] if len(party_accounts) == 2 else None
|
||||
|
||||
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
journal_entries = get_advance_journal_entries(
|
||||
@@ -1254,7 +1264,13 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
party_type, party, party_account, order_doctype, order_list, include_unallocated
|
||||
party_type,
|
||||
party,
|
||||
party_account,
|
||||
order_doctype,
|
||||
order_list,
|
||||
default_advance_account,
|
||||
include_unallocated,
|
||||
)
|
||||
|
||||
res = journal_entries + payment_entries
|
||||
@@ -1511,7 +1527,6 @@ class AccountsController(TransactionBase):
|
||||
gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
)
|
||||
|
||||
je = create_gain_loss_journal(
|
||||
self.company,
|
||||
args.get("difference_posting_date") if args else self.posting_date,
|
||||
@@ -1597,6 +1612,7 @@ class AccountsController(TransactionBase):
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
),
|
||||
"exchange_gain_loss": flt(d.get("exchange_gain_loss")),
|
||||
"difference_posting_date": d.get("difference_posting_date"),
|
||||
}
|
||||
)
|
||||
lst.append(args)
|
||||
@@ -2142,11 +2158,9 @@ class AccountsController(TransactionBase):
|
||||
for adv in self.advances:
|
||||
consider_for_total_advance = True
|
||||
if adv.reference_name == linked_doc_name:
|
||||
frappe.db.sql(
|
||||
f"""delete from `tab{self.doctype} Advance`
|
||||
where name = %s""",
|
||||
adv.name,
|
||||
)
|
||||
doctype = frappe.qb.DocType(self.doctype + " Advance")
|
||||
frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run()
|
||||
|
||||
consider_for_total_advance = False
|
||||
|
||||
if consider_for_total_advance:
|
||||
@@ -2361,6 +2375,7 @@ class AccountsController(TransactionBase):
|
||||
return
|
||||
|
||||
for d in self.get("payment_schedule"):
|
||||
d.validate_from_to_dates("discount_date", "due_date")
|
||||
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
|
||||
frappe.throw(
|
||||
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
|
||||
@@ -2946,6 +2961,7 @@ def get_advance_payment_entries(
|
||||
party_account,
|
||||
order_doctype,
|
||||
order_list=None,
|
||||
default_advance_account=None,
|
||||
include_unallocated=True,
|
||||
against_all_orders=False,
|
||||
limit=None,
|
||||
@@ -2959,6 +2975,7 @@ def get_advance_payment_entries(
|
||||
party_type,
|
||||
party,
|
||||
party_account,
|
||||
default_advance_account,
|
||||
limit,
|
||||
condition,
|
||||
)
|
||||
@@ -2982,6 +2999,7 @@ def get_advance_payment_entries(
|
||||
party_type,
|
||||
party,
|
||||
party_account,
|
||||
default_advance_account,
|
||||
limit,
|
||||
condition,
|
||||
)
|
||||
@@ -2997,6 +3015,7 @@ def get_common_query(
|
||||
party_type,
|
||||
party,
|
||||
party_account,
|
||||
default_advance_account,
|
||||
limit,
|
||||
condition,
|
||||
):
|
||||
@@ -3018,14 +3037,22 @@ def get_common_query(
|
||||
.where(payment_entry.docstatus == 1)
|
||||
)
|
||||
|
||||
if payment_type == "Receive":
|
||||
q = q.select((payment_entry.paid_from_account_currency).as_("currency"))
|
||||
q = q.select(payment_entry.paid_from)
|
||||
q = q.where(payment_entry.paid_from.isin(party_account))
|
||||
field = "paid_from" if payment_type == "Receive" else "paid_to"
|
||||
|
||||
q = q.select((payment_entry[f"{field}_account_currency"]).as_("currency"))
|
||||
q = q.select(payment_entry[field])
|
||||
account_condition = payment_entry[field].isin(party_account)
|
||||
if default_advance_account:
|
||||
q = q.where(
|
||||
account_condition
|
||||
| (
|
||||
(payment_entry[field] == default_advance_account)
|
||||
& (payment_entry.book_advance_payments_in_separate_party_account == 1)
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
q = q.select((payment_entry.paid_to_account_currency).as_("currency"))
|
||||
q = q.select(payment_entry.paid_to)
|
||||
q = q.where(payment_entry.paid_to.isin(party_account))
|
||||
q = q.where(account_condition)
|
||||
|
||||
if payment_type == "Receive":
|
||||
q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate"))
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
@@ -745,6 +746,7 @@ class BuyingController(SubcontractingController):
|
||||
items_data = get_asset_item_details(asset_items)
|
||||
messages = []
|
||||
alert = False
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
|
||||
|
||||
for d in self.items:
|
||||
if d.is_fixed_asset:
|
||||
@@ -756,11 +758,11 @@ class BuyingController(SubcontractingController):
|
||||
if item_data.get("asset_naming_series"):
|
||||
created_assets = []
|
||||
if item_data.get("is_grouped_asset"):
|
||||
asset = self.make_asset(d, is_grouped_asset=True)
|
||||
asset = self.make_asset(d, accounting_dimensions, is_grouped_asset=True)
|
||||
created_assets.append(asset)
|
||||
else:
|
||||
for _qty in range(cint(d.qty)):
|
||||
asset = self.make_asset(d)
|
||||
asset = self.make_asset(d, accounting_dimensions)
|
||||
created_assets.append(asset)
|
||||
|
||||
if len(created_assets) > 5:
|
||||
@@ -799,7 +801,7 @@ class BuyingController(SubcontractingController):
|
||||
for message in messages:
|
||||
frappe.msgprint(message, title="Success", indicator="green", alert=alert)
|
||||
|
||||
def make_asset(self, row, is_grouped_asset=False):
|
||||
def make_asset(self, row, accounting_dimensions, is_grouped_asset=False):
|
||||
if not row.asset_location:
|
||||
frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code))
|
||||
|
||||
@@ -830,6 +832,13 @@ class BuyingController(SubcontractingController):
|
||||
"purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
asset.update(
|
||||
{
|
||||
dimension["fieldname"]: self.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
asset.flags.ignore_validate = True
|
||||
asset.flags.ignore_mandatory = True
|
||||
|
||||
@@ -276,10 +276,14 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
qb_filter_or_conditions = []
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
if filters and filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
)
|
||||
if filters:
|
||||
if filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
qb_filter_and_conditions.append(proj.company == filters.get("company"))
|
||||
|
||||
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ def validate_return(doc):
|
||||
|
||||
if doc.return_against:
|
||||
validate_return_against(doc)
|
||||
|
||||
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and not doc.update_stock:
|
||||
return
|
||||
|
||||
validate_returned_items(doc)
|
||||
|
||||
|
||||
@@ -72,16 +76,13 @@ def validate_return_against(doc):
|
||||
def validate_returned_items(doc):
|
||||
valid_items = frappe._dict()
|
||||
|
||||
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor"
|
||||
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor, name"
|
||||
if doc.doctype != "Purchase Invoice":
|
||||
select_fields += ",serial_no, batch_no"
|
||||
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
select_fields += ",rejected_qty, received_qty"
|
||||
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
select_fields += ",name"
|
||||
|
||||
for d in frappe.db.sql(
|
||||
f"""select {select_fields} from `tab{doc.doctype} Item` where parent = %s""",
|
||||
doc.return_against,
|
||||
@@ -109,11 +110,13 @@ def validate_returned_items(doc):
|
||||
for d in doc.get("items"):
|
||||
key = d.item_code
|
||||
raise_exception = False
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice"]:
|
||||
field = frappe.scrub(doc.doctype) + "_item"
|
||||
if d.get(field):
|
||||
key = (d.item_code, d.get(field))
|
||||
raise_exception = True
|
||||
elif doc.doctype == "Delivery Note":
|
||||
key = (d.item_code, d.get("dn_detail"))
|
||||
|
||||
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
|
||||
if key not in valid_items:
|
||||
@@ -125,7 +128,7 @@ def validate_returned_items(doc):
|
||||
)
|
||||
else:
|
||||
ref = valid_items.get(key, frappe._dict())
|
||||
validate_quantity(doc, d, ref, valid_items, already_returned_items)
|
||||
validate_quantity(doc, key, d, ref, valid_items, already_returned_items)
|
||||
|
||||
if (
|
||||
ref.rate
|
||||
@@ -155,7 +158,7 @@ def validate_returned_items(doc):
|
||||
frappe.throw(_("At least one item should be entered with negative quantity in return document"))
|
||||
|
||||
|
||||
def validate_quantity(doc, args, ref, valid_items, already_returned_items):
|
||||
def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
fields = ["stock_qty"]
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
|
||||
if not args.get("return_qty_from_rejected_warehouse"):
|
||||
@@ -163,7 +166,7 @@ def validate_quantity(doc, args, ref, valid_items, already_returned_items):
|
||||
else:
|
||||
fields.extend(["received_qty"])
|
||||
|
||||
already_returned_data = already_returned_items.get(args.item_code) or {}
|
||||
already_returned_data = already_returned_items.get(key) or {}
|
||||
|
||||
company_currency = erpnext.get_company_currency(doc.company)
|
||||
stock_qty_precision = get_field_precision(
|
||||
@@ -249,15 +252,20 @@ def get_already_returned_items(doc):
|
||||
column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty,
|
||||
sum(abs(child.received_qty) * child.conversion_factor) as received_qty"""
|
||||
|
||||
field = (
|
||||
frappe.scrub(doc.doctype) + "_item"
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"]
|
||||
else "dn_detail"
|
||||
)
|
||||
data = frappe.db.sql(
|
||||
f"""
|
||||
select {column}
|
||||
select {column}, {field}
|
||||
from
|
||||
`tab{doc.doctype} Item` child, `tab{doc.doctype}` par
|
||||
where
|
||||
child.parent = par.name and par.docstatus = 1
|
||||
and par.is_return = 1 and par.return_against = %s
|
||||
group by item_code
|
||||
group by item_code, {field}
|
||||
""",
|
||||
doc.return_against,
|
||||
as_dict=1,
|
||||
@@ -267,7 +275,7 @@ def get_already_returned_items(doc):
|
||||
|
||||
for d in data:
|
||||
items.setdefault(
|
||||
d.item_code,
|
||||
(d.item_code, d.get(field)),
|
||||
frappe._dict(
|
||||
{
|
||||
"qty": d.get("qty"),
|
||||
|
||||
@@ -134,9 +134,13 @@ status_map = {
|
||||
"Partially Received",
|
||||
"eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'",
|
||||
],
|
||||
[
|
||||
"Partially Received",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'",
|
||||
],
|
||||
[
|
||||
"Partially Ordered",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'",
|
||||
],
|
||||
[
|
||||
"Manufactured",
|
||||
@@ -201,19 +205,19 @@ class StatusUpdater(Document):
|
||||
Get the status of the document.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the status. This allows callers to receive
|
||||
a dictionary for efficient bulk updates, for example when `per_billed`
|
||||
and other status fields also need to be updated.
|
||||
dict: A dictionary containing the status. This allows callers to receive
|
||||
a dictionary for efficient bulk updates, for example when `per_billed`
|
||||
and other status fields also need to be updated.
|
||||
|
||||
Note:
|
||||
Can be overriden on a doctype to implement more localized status updater logic.
|
||||
Can be overriden on a doctype to implement more localized status updater logic.
|
||||
|
||||
Example:
|
||||
{
|
||||
"status": "Draft",
|
||||
"per_billed": 50,
|
||||
"billing_status": "Partly Billed"
|
||||
}
|
||||
{
|
||||
"status": "Draft",
|
||||
"per_billed": 50,
|
||||
"billing_status": "Partly Billed"
|
||||
}
|
||||
"""
|
||||
if self.doctype not in status_map:
|
||||
return {"status": self.status}
|
||||
@@ -275,9 +279,20 @@ class StatusUpdater(Document):
|
||||
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
||||
args["name"] = d.get(args["join_field"])
|
||||
|
||||
is_from_pp = (
|
||||
hasattr(d, "production_plan_sub_assembly_item")
|
||||
and frappe.db.get_value(
|
||||
"Production Plan Sub Assembly Item",
|
||||
d.production_plan_sub_assembly_item,
|
||||
"type_of_manufacturing",
|
||||
)
|
||||
== "Subcontract"
|
||||
)
|
||||
args["item_code"] = "production_item" if is_from_pp else "item_code"
|
||||
|
||||
# get all qty where qty > target_field
|
||||
item = frappe.db.sql(
|
||||
"""select item_code, `{target_ref_field}`,
|
||||
"""select `{item_code}` as item_code, `{target_ref_field}`,
|
||||
`{target_field}`, parenttype, parent from `tab{target_dt}`
|
||||
where `{target_ref_field}` < `{target_field}`
|
||||
and name=%s and docstatus=1""".format(**args),
|
||||
@@ -421,6 +436,13 @@ class StatusUpdater(Document):
|
||||
if d.doctype != args["source_dt"]:
|
||||
continue
|
||||
|
||||
if (
|
||||
d.get("material_request")
|
||||
and frappe.db.get_value("Material Request", d.material_request, "material_request_type")
|
||||
== "Subcontracting"
|
||||
):
|
||||
args.update({"source_field": "fg_item_qty"})
|
||||
|
||||
self._update_modified(args, update_modified)
|
||||
|
||||
# updates qty in the child table
|
||||
|
||||
@@ -156,7 +156,7 @@ class StockController(AccountsController):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
is_material_issue = False
|
||||
if self.doctype == "Stock Entry" and self.purpose == "Material Issue":
|
||||
if self.doctype == "Stock Entry" and self.purpose in ["Material Issue", "Material Transfer"]:
|
||||
is_material_issue = True
|
||||
|
||||
for d in self.get("items"):
|
||||
@@ -530,7 +530,7 @@ class StockController(AccountsController):
|
||||
"account": warehouse_account[sle.warehouse]["account"],
|
||||
"against": expense_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.project or self.get("project"),
|
||||
"project": sle.get("project") or item_row.project or self.get("project"),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": flt(sle.stock_value_difference, precision),
|
||||
"is_opening": item_row.get("is_opening")
|
||||
@@ -550,7 +550,9 @@ class StockController(AccountsController):
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(sle.stock_value_difference, precision),
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"project": sle.get("project")
|
||||
or item_row.get("project")
|
||||
or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening")
|
||||
or self.get("is_opening")
|
||||
or "No",
|
||||
@@ -678,23 +680,34 @@ class StockController(AccountsController):
|
||||
|
||||
def get_stock_ledger_details(self):
|
||||
stock_ledger = {}
|
||||
stock_ledger_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, warehouse, stock_value_difference, valuation_rate,
|
||||
voucher_detail_no, item_code, posting_date, posting_time,
|
||||
actual_qty, qty_after_transaction
|
||||
from
|
||||
`tabStock Ledger Entry`
|
||||
where
|
||||
voucher_type=%s and voucher_no=%s and is_cancelled = 0
|
||||
""",
|
||||
(self.doctype, self.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
stock_ledger_entries = (
|
||||
frappe.qb.from_(table)
|
||||
.select(
|
||||
table.name,
|
||||
table.warehouse,
|
||||
table.stock_value_difference,
|
||||
table.valuation_rate,
|
||||
table.voucher_detail_no,
|
||||
table.item_code,
|
||||
table.posting_date,
|
||||
table.posting_time,
|
||||
table.actual_qty,
|
||||
table.qty_after_transaction,
|
||||
table.project,
|
||||
)
|
||||
.where(
|
||||
(table.voucher_type == self.doctype)
|
||||
& (table.voucher_no == self.name)
|
||||
& (table.is_cancelled == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
for sle in stock_ledger_entries:
|
||||
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
|
||||
|
||||
return stock_ledger
|
||||
|
||||
def check_expense_account(self, item):
|
||||
@@ -1002,6 +1015,9 @@ class StockController(AccountsController):
|
||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||
qi_required = True # inward stock needs inspection
|
||||
|
||||
if row.get("is_scrap_item"):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
self.validate_qi_presence(row)
|
||||
if self.docstatus == 1:
|
||||
@@ -1761,6 +1777,9 @@ def make_bundle_for_material_transfer(**kwargs):
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
if kwargs.do_not_submit:
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
else:
|
||||
bundle_doc.submit()
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -103,6 +103,29 @@ class SubcontractingController(StockController):
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
)
|
||||
|
||||
if (
|
||||
self.doctype == "Subcontracting Order" and not item.sc_conversion_factor
|
||||
): # this condition will only be true if user has recently updated from develop branch
|
||||
service_item_qty = frappe.get_value(
|
||||
"Subcontracting Order Service Item",
|
||||
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
|
||||
fieldname=["qty"],
|
||||
)
|
||||
item.sc_conversion_factor = service_item_qty / item.qty
|
||||
|
||||
if (
|
||||
self.doctype not in "Subcontracting Receipt"
|
||||
and item.qty
|
||||
> flt(get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item))
|
||||
/ item.sc_conversion_factor
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
|
||||
).format(item.idx, item.item_name)
|
||||
)
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if item.bom:
|
||||
is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
|
||||
|
||||
@@ -1116,6 +1139,12 @@ def get_item_details(items):
|
||||
return item_details
|
||||
|
||||
|
||||
def get_pending_sco_qty(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name)
|
||||
return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(
|
||||
subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None
|
||||
@@ -1234,6 +1263,7 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
|
||||
"item_code": row.item_details["rm_item_code"],
|
||||
"subcontracted_item": row.item_details["main_item_code"],
|
||||
"serial_no": "\n".join(row.serial_no) if row.serial_no else "",
|
||||
"use_serial_batch_fields": 1,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1274,10 +1304,13 @@ def make_return_stock_entry_for_subcontract(
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if item_details := value.get("item_details"):
|
||||
item_details["serial_and_batch_bundle"] = None
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field, batch_no)
|
||||
add_items_in_ste(ste_doc, value, qty, rm_details, rm_detail_field, batch_no)
|
||||
else:
|
||||
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from erpnext.controllers.accounts_controller import (
|
||||
validate_taxes_and_charges,
|
||||
)
|
||||
from erpnext.deprecation_dumpster import deprecated
|
||||
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template
|
||||
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template, get_item_tax_map
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
logger = frappe.logger(__name__)
|
||||
@@ -74,6 +74,7 @@ class calculate_taxes_and_totals:
|
||||
self.validate_conversion_rate()
|
||||
self.calculate_item_values()
|
||||
self.validate_item_tax_template()
|
||||
self.update_item_tax_map()
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
self.calculate_net_total()
|
||||
@@ -140,6 +141,14 @@ class calculate_taxes_and_totals:
|
||||
)
|
||||
)
|
||||
|
||||
def update_item_tax_map(self):
|
||||
for item in self.doc.items:
|
||||
item.item_tax_rate = get_item_tax_map(
|
||||
doc=self.doc,
|
||||
tax_template=item.item_tax_template,
|
||||
as_json=True,
|
||||
)
|
||||
|
||||
def validate_conversion_rate(self):
|
||||
# validate conversion rate
|
||||
company_currency = erpnext.get_company_currency(self.doc.company)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user