mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-24 13:15:03 +00:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c57e9f8c8 | ||
|
|
5f62fc5a99 | ||
|
|
13bebe71b0 | ||
|
|
b1161f446f | ||
|
|
b8485e1d28 | ||
|
|
e422ae16e5 | ||
|
|
dcdd8e9824 | ||
|
|
9d98d0efc8 | ||
|
|
af60368e54 | ||
|
|
ab37e5754b | ||
|
|
9a27d3cedc | ||
|
|
0b591ba7eb | ||
|
|
8ea9a9e467 | ||
|
|
cfc50073ed | ||
|
|
25075e5981 | ||
|
|
6a03f99546 | ||
|
|
b140ce71d7 | ||
|
|
f72d4b6984 | ||
|
|
d86f7839ab | ||
|
|
7f7f403f5b | ||
|
|
df0ca2d198 | ||
|
|
c52362531c | ||
|
|
ca4bb96fb4 | ||
|
|
50d10d5c8d | ||
|
|
4aa072a8eb | ||
|
|
223fe62638 | ||
|
|
bbecd36a5d | ||
|
|
9f28993575 | ||
|
|
1ad16c368e | ||
|
|
c1002d3787 | ||
|
|
8ad32c4593 | ||
|
|
0b54cb9a7c | ||
|
|
44d9fb7a69 | ||
|
|
2a25302c35 | ||
|
|
0cac9e9b9a | ||
|
|
efa7602fae | ||
|
|
b926b49afa | ||
|
|
8108d7fdba | ||
|
|
3689a2deae | ||
|
|
5a7b901327 | ||
|
|
51b03d40aa | ||
|
|
47d1c3b5a3 | ||
|
|
f9794e5b44 | ||
|
|
401fd7fca7 | ||
|
|
34f8a370d1 | ||
|
|
ac960d26e8 | ||
|
|
aa27e19a58 | ||
|
|
1ed52c3b41 | ||
|
|
fea51b7086 | ||
|
|
49192d90d0 | ||
|
|
f9af53cd87 | ||
|
|
5319683dbf | ||
|
|
32bfc2f555 | ||
|
|
a948f2e095 | ||
|
|
6113cc1e43 | ||
|
|
b915e7f637 | ||
|
|
0d03e03925 | ||
|
|
6149306b78 | ||
|
|
a0523c7956 | ||
|
|
7931c2d182 | ||
|
|
7658053dbd | ||
|
|
1b85cdcf48 | ||
|
|
16a80cc04a | ||
|
|
cf98ebf7fe | ||
|
|
84638f58fd | ||
|
|
b612ab5823 | ||
|
|
0d94eba021 | ||
|
|
7d95113499 | ||
|
|
afd2529c2a | ||
|
|
86edcfc1fd | ||
|
|
b840271d2a | ||
|
|
55ac00a510 | ||
|
|
c0b32c446b | ||
|
|
1522d76a3b | ||
|
|
83e676f128 | ||
|
|
eef2f3c5d4 | ||
|
|
821cc215e3 | ||
|
|
3e09abc495 | ||
|
|
d0748b1b67 | ||
|
|
7fc70e3f20 | ||
|
|
24a6f611d8 | ||
|
|
4b91b7691a | ||
|
|
6ea5307b0e | ||
|
|
d65101a5f2 | ||
|
|
bd8e4eeddc | ||
|
|
f6b49845e2 | ||
|
|
676dde59c2 | ||
|
|
72868eee04 | ||
|
|
04c1fc71dd | ||
|
|
786db3d0fa | ||
|
|
cd5174e423 | ||
|
|
20d5a79839 | ||
|
|
73c1bf972e | ||
|
|
6301b321d8 | ||
|
|
8b75401db9 | ||
|
|
b5856d4f1f | ||
|
|
47422111b4 | ||
|
|
c5cd0fcd29 | ||
|
|
cae8cee398 | ||
|
|
cc96145ac9 | ||
|
|
2b16eb5381 | ||
|
|
97d9030f26 | ||
|
|
156dcf535d | ||
|
|
e3855949e1 | ||
|
|
2d2f30e6cf | ||
|
|
3117052779 | ||
|
|
eac5c91c8f | ||
|
|
ef2f4118d9 | ||
|
|
8f73978a26 | ||
|
|
28bb9c39e8 | ||
|
|
aef6b62f7d | ||
|
|
a2ffdc7805 | ||
|
|
8b0efab13e | ||
|
|
6323e641f6 | ||
|
|
6c4655dd72 | ||
|
|
33b5be03a8 | ||
|
|
b37602c716 | ||
|
|
75a4c31948 | ||
|
|
eef907a275 | ||
|
|
de43c123e2 | ||
|
|
0576c5c37f | ||
|
|
f60a3bcedf | ||
|
|
1be19819fb | ||
|
|
b59d253d93 | ||
|
|
d74c498efe | ||
|
|
57f79a2240 | ||
|
|
fb1ea9524b | ||
|
|
8d30b4d14e | ||
|
|
c102e51eb1 | ||
|
|
d6e0c6c969 | ||
|
|
5c9ac27478 | ||
|
|
efc7b9ac56 | ||
|
|
2c3e765896 | ||
|
|
4e347d835e | ||
|
|
e73aab0df5 | ||
|
|
bcd3351999 | ||
|
|
67e45cf002 | ||
|
|
149827562b | ||
|
|
fac4e99b0e | ||
|
|
8f0d270746 | ||
|
|
a718737931 | ||
|
|
b004855e7c | ||
|
|
6a382f1430 | ||
|
|
172fdad244 | ||
|
|
2c2a25ab16 | ||
|
|
ff46ae5bc1 | ||
|
|
692a44816f | ||
|
|
f3b8d6867b | ||
|
|
69c5695f6e | ||
|
|
f9d96726f0 | ||
|
|
4aa92572ef | ||
|
|
f2b946d325 | ||
|
|
13c7fceb91 | ||
|
|
fe5e42d2dc | ||
|
|
7ff7ec7929 | ||
|
|
ae5ce97fd0 | ||
|
|
52fdc7cecd | ||
|
|
d9b342f257 | ||
|
|
dec0caeac5 | ||
|
|
224a92587d | ||
|
|
aca8d663dd | ||
|
|
6c10393164 | ||
|
|
3eb28bb0e0 | ||
|
|
de09da31bc | ||
|
|
835abfd88e | ||
|
|
fd427ad2ef | ||
|
|
2f60f235a8 | ||
|
|
1c10e4e92f | ||
|
|
c90d33acc3 | ||
|
|
ccaf0d4b85 | ||
|
|
d955986342 | ||
|
|
c127cbac57 | ||
|
|
c6bc5f5d7d | ||
|
|
1522600eb5 | ||
|
|
fb75180a7d | ||
|
|
5d7d3d8c19 | ||
|
|
73a21c294c | ||
|
|
60feb7cbd4 | ||
|
|
153e961df7 | ||
|
|
86f4bf6e01 | ||
|
|
7fdf2a7d45 | ||
|
|
3906e5c33f | ||
|
|
72581dd0bf | ||
|
|
10ee6f3e22 | ||
|
|
db4fe59bca | ||
|
|
495273365b | ||
|
|
3c3f092382 | ||
|
|
693687d8a3 | ||
|
|
c070a140f2 | ||
|
|
f411bcc8b5 | ||
|
|
763cc18aad | ||
|
|
671f728c4a | ||
|
|
0e088dde36 | ||
|
|
546da29761 | ||
|
|
0eac720182 | ||
|
|
ef15429d98 | ||
|
|
b9b4f6316d | ||
|
|
04f5a72e08 | ||
|
|
412e22fb4e | ||
|
|
767529f0ec | ||
|
|
2403cdc4d7 | ||
|
|
4e367dedec | ||
|
|
f8099a6847 | ||
|
|
e7f1cda3c1 | ||
|
|
46a2b7a07e | ||
|
|
bdc65daadd | ||
|
|
7848f1146b | ||
|
|
ad06652ed5 | ||
|
|
98797fa918 | ||
|
|
4a390ae3de |
6
.github/workflows/patch.yml
vendored
6
.github/workflows/patch.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
6
.github/workflows/server-tests-mariadb.yml
vendored
6
.github/workflows/server-tests-mariadb.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
6
.github/workflows/server-tests-postgres.yml
vendored
6
.github/workflows/server-tests-postgres.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.49.2"
|
||||
__version__ = "15.51.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
{
|
||||
"country_code": "ch",
|
||||
"name": "240812 Schulkontenrahmen VEB - DE",
|
||||
"tree": {
|
||||
"Aktiven": {
|
||||
"account_number": "1",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Umlaufvermögen": {
|
||||
"account_number": "10",
|
||||
"is_group": 1,
|
||||
"Flüssige Mittel": {
|
||||
"account_number": "100",
|
||||
"is_group": 1,
|
||||
"Kasse": {
|
||||
"account_number": "1000",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Bankguthaben": {
|
||||
"account_number": "1020",
|
||||
"account_type": "Bank"
|
||||
}
|
||||
},
|
||||
"Kurzfristig gehaltene Aktiven mit Börsenkurs": {
|
||||
"account_number": "106",
|
||||
"is_group": 1,
|
||||
"Wertschriften": {
|
||||
"account_number": "1060"
|
||||
},
|
||||
"Wertberichtigungen Wertschriften": {
|
||||
"account_number": "1069"
|
||||
}
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen": {
|
||||
"account_number": "110",
|
||||
"is_group": 1,
|
||||
"Forderungen aus Lieferungen und Leistungen (Debitoren)": {
|
||||
"account_number": "1100"
|
||||
},
|
||||
"Delkredere": {
|
||||
"account_number": "1109"
|
||||
}
|
||||
},
|
||||
"Übrige kurzfristige Forderungen": {
|
||||
"account_number": "114",
|
||||
"is_group": 1,
|
||||
"Vorschüsse und Darlehen": {
|
||||
"account_number": "1140"
|
||||
},
|
||||
"Wertberichtigungen Vorschüsse und Darlehen": {
|
||||
"account_number": "1149"
|
||||
},
|
||||
"Vorsteuer MWST Material, Waren, Dienstleistungen, Energie": {
|
||||
"account_number": "1170"
|
||||
},
|
||||
"Vorsteuer MWST Investitionen, übriger Betriebsaufwand": {
|
||||
"account_number": "1171"
|
||||
},
|
||||
"Verrechnungssteuer": {
|
||||
"account_number": "1176"
|
||||
},
|
||||
"Forderungen gegenüber Sozialversicherungen und Vorsorgeeinrichtungen": {
|
||||
"account_number": "1180"
|
||||
},
|
||||
"Quellensteuer": {
|
||||
"account_number": "1189"
|
||||
},
|
||||
"Sonstige kurzfristige Forderungen": {
|
||||
"account_number": "1190"
|
||||
},
|
||||
"Wertberichtigungen sonstige kurzfristige Forderungen": {
|
||||
"account_number": "1199"
|
||||
}
|
||||
},
|
||||
"Vorräte und nicht fakturierte Dienstleistungen": {
|
||||
"account_number": "120",
|
||||
"is_group": 1,
|
||||
"Handelswaren": {
|
||||
"account_number": "1200"
|
||||
},
|
||||
"Rohstoffe": {
|
||||
"account_number": "1210"
|
||||
},
|
||||
"Werkstoffe": {
|
||||
"account_number": "1220"
|
||||
},
|
||||
"Hilfs- und Verbrauchsmaterial": {
|
||||
"account_number": "1230"
|
||||
},
|
||||
"Handelswaren in Konsignation": {
|
||||
"account_number": "1250"
|
||||
},
|
||||
"Fertige Erzeugnisse": {
|
||||
"account_number": "1260"
|
||||
},
|
||||
"Unfertige Erzeugnisse": {
|
||||
"account_number": "1270"
|
||||
},
|
||||
"Nicht fakturierte Dienstleistungen": {
|
||||
"account_number": "1280"
|
||||
}
|
||||
},
|
||||
"Aktive Rechnungsabgrenzungen": {
|
||||
"account_number": "130",
|
||||
"is_group": 1,
|
||||
"Bezahlter Aufwand des Folgejahres": {
|
||||
"account_number": "1300"
|
||||
},
|
||||
"Noch nicht erhaltener Ertrag": {
|
||||
"account_number": "1301"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Anlagevermögen": {
|
||||
"account_number": "14",
|
||||
"is_group": 1,
|
||||
"Finanzanlagen": {
|
||||
"account_number": "140",
|
||||
"is_group": 1,
|
||||
"Wertschriften": {
|
||||
"account_number": "1400"
|
||||
},
|
||||
"Wertberichtigungen Wertschriften": {
|
||||
"account_number": "1409"
|
||||
},
|
||||
"Darlehen": {
|
||||
"account_number": "1440"
|
||||
},
|
||||
"Hypotheken": {
|
||||
"account_number": "1441"
|
||||
},
|
||||
"Wertberichtigungen langfristige Forderungen": {
|
||||
"account_number": "1449"
|
||||
}
|
||||
},
|
||||
"Beteiligungen": {
|
||||
"account_number": "148",
|
||||
"is_group": 1,
|
||||
"Beteiligungen": {
|
||||
"account_number": "1480"
|
||||
},
|
||||
"Wertberichtigungen Beteiligungen": {
|
||||
"account_number": "1489"
|
||||
}
|
||||
},
|
||||
"Mobile Sachanlagen": {
|
||||
"account_number": "150",
|
||||
"is_group": 1,
|
||||
"Maschinen und Apparate": {
|
||||
"account_number": "1500"
|
||||
},
|
||||
"Wertberichtigungen Maschinen und Apparate": {
|
||||
"account_number": "1509"
|
||||
},
|
||||
"Mobiliar und Einrichtungen": {
|
||||
"account_number": "1510"
|
||||
},
|
||||
"Wertberichtigungen Mobiliar und Einrichtungen": {
|
||||
"account_number": "1519"
|
||||
},
|
||||
"Büromaschinen, Informatik, Kommunikationstechnologie": {
|
||||
"account_number": "1520"
|
||||
},
|
||||
"Wertberichtigungen Büromaschinen, Informatik, Kommunikationstechnologie": {
|
||||
"account_number": "1529"
|
||||
},
|
||||
"Fahrzeuge": {
|
||||
"account_number": "1530"
|
||||
},
|
||||
"Wertberichtigungen Fahrzeuge": {
|
||||
"account_number": "1539"
|
||||
},
|
||||
"Werkzeuge und Geräte": {
|
||||
"account_number": "1540"
|
||||
},
|
||||
"Wertberichtigungen Werkzeuge und Geräte": {
|
||||
"account_number": "1549"
|
||||
}
|
||||
},
|
||||
"Immobile Sachanlagen": {
|
||||
"account_number": "160",
|
||||
"is_group": 1,
|
||||
"Geschäftsliegenschaften": {
|
||||
"account_number": "1600"
|
||||
},
|
||||
"Wertberichtigungen Geschäftsliegenschaften": {
|
||||
"account_number": "1609"
|
||||
}
|
||||
},
|
||||
"Immaterielle Werte": {
|
||||
"account_number": "170",
|
||||
"is_group": 1,
|
||||
"Patente, Know-how, Lizenzen, Rechte, Entwicklungen": {
|
||||
"account_number": "1700"
|
||||
},
|
||||
"Wertberichtigungen Patente, Know-how, Lizenzen, Rechte, Entwicklungen": {
|
||||
"account_number": "1709"
|
||||
},
|
||||
"Goodwill": {
|
||||
"account_number": "1770"
|
||||
},
|
||||
"Wertberichtigungen Goodwill": {
|
||||
"account_number": "1779"
|
||||
}
|
||||
},
|
||||
"Nicht einbezahltes Grund-, Gesellschafter- oder Stiftungskapital": {
|
||||
"account_number": "180",
|
||||
"is_group": 1,
|
||||
"Nicht einbezahltes Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": {
|
||||
"account_number": "1850"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Passiven": {
|
||||
"account_number": "2",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Kurzfristiges Fremdkapital": {
|
||||
"account_number": "20",
|
||||
"is_group": 1,
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen": {
|
||||
"account_number": "200",
|
||||
"is_group": 1,
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen (Kreditoren)": {
|
||||
"account_number": "2000"
|
||||
},
|
||||
"Erhaltene Anzahlungen": {
|
||||
"account_number": "2030"
|
||||
}
|
||||
},
|
||||
"Kurzfristige verzinsliche Verbindlichkeiten": {
|
||||
"account_number": "210",
|
||||
"is_group": 1,
|
||||
"Bankverbindlichkeiten": {
|
||||
"account_number": "2100"
|
||||
},
|
||||
"Verbindlichkeiten aus Finanzierungsleasing": {
|
||||
"account_number": "2120"
|
||||
},
|
||||
"Übrige verzinsliche Verbindlichkeiten": {
|
||||
"account_number": "2140"
|
||||
}
|
||||
},
|
||||
"Übrige kurzfristige Verbindlichkeiten": {
|
||||
"account_number": "220",
|
||||
"is_group": 1,
|
||||
"Geschuldete MWST (Umsatzsteuer)": {
|
||||
"account_number": "2200"
|
||||
},
|
||||
"Abrechnungskonto MWST": {
|
||||
"account_number": "2201"
|
||||
},
|
||||
"Verrechnungssteuer": {
|
||||
"account_number": "2206"
|
||||
},
|
||||
"Direkte Steuern": {
|
||||
"account_number": "2208"
|
||||
},
|
||||
"Sonstige kurzfristige Verbindlichkeiten": {
|
||||
"account_number": "2210"
|
||||
},
|
||||
"Beschlossene Ausschüttungen": {
|
||||
"account_number": "2261"
|
||||
},
|
||||
"Sozialversicherungen und Vorsorgeeinrichtungen": {
|
||||
"account_number": "2270"
|
||||
},
|
||||
"Quellensteuer": {
|
||||
"account_number": "2279"
|
||||
}
|
||||
},
|
||||
"Passive Rechnungsabgrenzungen und kurzfristige Rückstellungen": {
|
||||
"account_number": "230",
|
||||
"is_group": 1,
|
||||
"Noch nicht bezahlter Aufwand": {
|
||||
"account_number": "2300"
|
||||
},
|
||||
"Erhaltener Ertrag des Folgejahres": {
|
||||
"account_number": "2301"
|
||||
},
|
||||
"Kurzfristige Rückstellungen": {
|
||||
"account_number": "2330"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Langfristiges Fremdkapital": {
|
||||
"account_number": "24",
|
||||
"is_group": 1,
|
||||
"Langfristige verzinsliche Verbindlichkeiten": {
|
||||
"account_number": "240",
|
||||
"is_group": 1,
|
||||
"Bankverbindlichkeiten": {
|
||||
"account_number": "2400"
|
||||
},
|
||||
"Verbindlichkeiten aus Finanzierungsleasing": {
|
||||
"account_number": "2420"
|
||||
},
|
||||
"Obligationenanleihen": {
|
||||
"account_number": "2430"
|
||||
},
|
||||
"Darlehen": {
|
||||
"account_number": "2450"
|
||||
},
|
||||
"Hypotheken": {
|
||||
"account_number": "2451"
|
||||
}
|
||||
},
|
||||
"Übrige langfristige Verbindlichkeiten": {
|
||||
"account_number": "250",
|
||||
"is_group": 1,
|
||||
"Übrige langfristige Verbindlichkeiten (unverzinslich)": {
|
||||
"account_number": "2500"
|
||||
}
|
||||
},
|
||||
"Rückstellungen sowie vom Gesetz vorgesehene ähnliche Positionen": {
|
||||
"account_number": "260",
|
||||
"is_group": 1,
|
||||
"Rückstellungen": {
|
||||
"account_number": "2600"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Eigenkapital (juristische Personen)": {
|
||||
"account_number": "28",
|
||||
"is_group": 1,
|
||||
"Grund-, Gesellschafter- oder Stiftungskapital": {
|
||||
"account_number": "280",
|
||||
"is_group": 1,
|
||||
"Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": {
|
||||
"account_number": "2800"
|
||||
}
|
||||
},
|
||||
"Reserven und Jahresgewinn oder Jahresverlust": {
|
||||
"account_number": "290",
|
||||
"is_group": 1,
|
||||
"Gesetzliche Kapitalreserve": {
|
||||
"account_number": "2900"
|
||||
},
|
||||
"Reserve für eigene Kapitalanteile": {
|
||||
"account_number": "2930"
|
||||
},
|
||||
"Aufwertungsreserve": {
|
||||
"account_number": "2940"
|
||||
},
|
||||
"Gesetzliche Gewinnreserve": {
|
||||
"account_number": "2950"
|
||||
},
|
||||
"Freiwillige Gewinnreserven": {
|
||||
"account_number": "2960"
|
||||
},
|
||||
"Gewinnvortrag oder Verlustvortrag": {
|
||||
"account_number": "2970"
|
||||
},
|
||||
"Jahresgewinn oder Jahresverlust": {
|
||||
"account_number": "2979"
|
||||
},
|
||||
"Eigene Aktien, Stammanteile oder Anteilscheine (Minusposten)": {
|
||||
"account_number": "2980"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Betrieblicher Ertrag aus Lieferungen und Leistungen": {
|
||||
"account_number": "3",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Produktionserlöse": {
|
||||
"account_number": "3000"
|
||||
},
|
||||
"Handelserlöse": {
|
||||
"account_number": "3200"
|
||||
},
|
||||
"Dienstleistungserlöse": {
|
||||
"account_number": "3400"
|
||||
},
|
||||
"Übrige Erlöse aus Lieferungen und Leistungen": {
|
||||
"account_number": "3600"
|
||||
},
|
||||
"Eigenleistungen": {
|
||||
"account_number": "3700"
|
||||
},
|
||||
"Eigenverbrauch": {
|
||||
"account_number": "3710"
|
||||
},
|
||||
"Erlösminderungen": {
|
||||
"account_number": "3800"
|
||||
},
|
||||
"Verluste Forderungen (Debitoren), Veränderung Delkredere": {
|
||||
"account_number": "3805"
|
||||
},
|
||||
"Bestandesänderungen unfertige Erzeugnisse": {
|
||||
"account_number": "3900"
|
||||
},
|
||||
"Bestandesänderungen fertige Erzeugnisse": {
|
||||
"account_number": "3901"
|
||||
},
|
||||
"Bestandesänderungen nicht fakturierte Dienstleistungen": {
|
||||
"account_number": "3940"
|
||||
}
|
||||
},
|
||||
"Aufwand für Material, Handelswaren, Dienstleistungen und Energie": {
|
||||
"account_number": "4",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Materialaufwand Produktion": {
|
||||
"account_number": "4000"
|
||||
},
|
||||
"Handelswarenaufwand": {
|
||||
"account_number": "4200"
|
||||
},
|
||||
"Aufwand für bezogene Dienstleistungen": {
|
||||
"account_number": "4400"
|
||||
},
|
||||
"Energieaufwand zur Leistungserstellung": {
|
||||
"account_number": "4500"
|
||||
},
|
||||
"Aufwandminderungen": {
|
||||
"account_number": "4900"
|
||||
}
|
||||
},
|
||||
"Personalaufwand": {
|
||||
"account_number": "5",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Lohnaufwand": {
|
||||
"account_number": "5000"
|
||||
},
|
||||
"Sozialversicherungsaufwand": {
|
||||
"account_number": "5700"
|
||||
},
|
||||
"Übriger Personalaufwand": {
|
||||
"account_number": "5800"
|
||||
},
|
||||
"Leistungen Dritter": {
|
||||
"account_number": "5900"
|
||||
}
|
||||
},
|
||||
"Übriger betrieblicher Aufwand, Abschreibungen und Wertberichtigungen sowie Finanzergebnis": {
|
||||
"account_number": "6",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Raumaufwand": {
|
||||
"account_number": "6000"
|
||||
},
|
||||
"Unterhalt, Reparaturen, Ersatz mobile Sachanlagen": {
|
||||
"account_number": "6100"
|
||||
},
|
||||
"Leasingaufwand mobile Sachanlagen": {
|
||||
"account_number": "6105"
|
||||
},
|
||||
"Fahrzeug- und Transportaufwand": {
|
||||
"account_number": "6200"
|
||||
},
|
||||
"Fahrzeugleasing und -mieten": {
|
||||
"account_number": "6260"
|
||||
},
|
||||
"Sachversicherungen, Abgaben, Gebühren, Bewilligungen": {
|
||||
"account_number": "6300"
|
||||
},
|
||||
"Energie- und Entsorgungsaufwand": {
|
||||
"account_number": "6400"
|
||||
},
|
||||
"Verwaltungsaufwand": {
|
||||
"account_number": "6500"
|
||||
},
|
||||
"Informatikaufwand inkl. Leasing": {
|
||||
"account_number": "6570"
|
||||
},
|
||||
"Werbeaufwand": {
|
||||
"account_number": "6600"
|
||||
},
|
||||
"Sonstiger betrieblicher Aufwand": {
|
||||
"account_number": "6700"
|
||||
},
|
||||
"Abschreibungen und Wertberichtigungen auf Positionen des Anlagevermögens": {
|
||||
"account_number": "6800"
|
||||
},
|
||||
"Finanzaufwand": {
|
||||
"account_number": "6900"
|
||||
},
|
||||
"Finanzertrag": {
|
||||
"account_number": "6950"
|
||||
}
|
||||
},
|
||||
"Betrieblicher Nebenerfolg": {
|
||||
"account_number": "7",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Ertrag Nebenbetrieb": {
|
||||
"account_number": "7000"
|
||||
},
|
||||
"Aufwand Nebenbetrieb": {
|
||||
"account_number": "7010"
|
||||
},
|
||||
"Ertrag betriebliche Liegenschaft": {
|
||||
"account_number": "7500"
|
||||
},
|
||||
"Aufwand betriebliche Liegenschaft": {
|
||||
"account_number": "7510"
|
||||
}
|
||||
},
|
||||
"Betriebsfremder, ausserordentlicher, einmaliger oder periodenfremder Aufwand und Ertrag": {
|
||||
"account_number": "8",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Betriebsfremder Aufwand": {
|
||||
"account_number": "8000"
|
||||
},
|
||||
"Betriebsfremder Ertrag": {
|
||||
"account_number": "8100"
|
||||
},
|
||||
"Ausserordentlicher, einmaliger oder periodenfremder Aufwand": {
|
||||
"account_number": "8500"
|
||||
},
|
||||
"Ausserordentlicher, einmaliger oder periodenfremder Ertrag": {
|
||||
"account_number": "8510"
|
||||
},
|
||||
"Direkte Steuern": {
|
||||
"account_number": "8900"
|
||||
}
|
||||
},
|
||||
"Abschluss": {
|
||||
"account_number": "9",
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Jahresgewinn oder Jahresverlust": {
|
||||
"account_number": "9200"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"read_only_depends_on": "eval:!doc.__islocal",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"auto_reconciliation_job_trigger",
|
||||
"reconciliation_queue_size",
|
||||
"column_break_resa",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -389,7 +390,7 @@
|
||||
{
|
||||
"fieldname": "section_break_jpd0",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Reconciliations"
|
||||
"label": "Payment Reconciliation Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -523,6 +524,14 @@
|
||||
"fieldname": "ignore_is_opening_check_for_reporting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Is Opening check for reporting"
|
||||
},
|
||||
{
|
||||
"default": "Payment",
|
||||
"description": "Only applies for Normal Payments",
|
||||
"fieldname": "exchange_gain_loss_posting_date",
|
||||
"fieldtype": "Select",
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -530,7 +539,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-18 21:24:19.840745",
|
||||
"modified": "2025-01-23 13:15:44.077853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -45,6 +45,7 @@ class AccountsSettings(Document):
|
||||
enable_fuzzy_matching: DF.Check
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
|
||||
@@ -19,10 +19,15 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (!frm.doc.company) {
|
||||
frm.set_value("company", frappe.defaults.get_default("company"));
|
||||
}
|
||||
|
||||
// Set default filter dates
|
||||
let today = frappe.datetime.get_today();
|
||||
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
||||
frm.doc.bank_statement_to_date = today;
|
||||
|
||||
frm.trigger("bank_account");
|
||||
},
|
||||
|
||||
@@ -98,7 +103,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
|
||||
make_reconciliation_tool(frm) {
|
||||
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
frm.trigger("get_cleared_balance").then(() => {
|
||||
if (
|
||||
frm.doc.bank_account &&
|
||||
@@ -114,7 +119,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
get_account_opening_balance(frm) {
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||
args: {
|
||||
@@ -130,7 +135,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
get_cleared_balance(frm) {
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||
args: {
|
||||
|
||||
@@ -802,7 +802,6 @@ def get_je_matching_query(
|
||||
.where(je.clearance_date.isnull())
|
||||
.where(jea.account == common_filters.bank_account)
|
||||
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
||||
.where(je.docstatus == 1)
|
||||
.where(filter_by_date)
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
)
|
||||
|
||||
@@ -45,45 +45,41 @@ class AutoMatchbyAccountIBAN:
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
return None
|
||||
|
||||
result = self.match_account_in_party()
|
||||
return result
|
||||
return self.match_account_in_party()
|
||||
|
||||
def match_account_in_party(self) -> tuple | None:
|
||||
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
or_filters = self.get_or_filters()
|
||||
"""
|
||||
Returns (Party Type, Party) if a matching account is found in Bank Account or Employee:
|
||||
1. Get party from a matching (iban/account no) Bank Account
|
||||
2. If not found, get party from Employee with matching bank account details (iban/account no)
|
||||
"""
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
# Nothing to match
|
||||
return None
|
||||
|
||||
for party in parties:
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
|
||||
)
|
||||
# Search for a matching Bank Account that has party set
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account",
|
||||
or_filters=self.get_or_filters(),
|
||||
filters={"party_type": ("is", "set"), "party": ("is", "set")},
|
||||
fields=["party", "party_type"],
|
||||
limit_page_length=1,
|
||||
)
|
||||
if result := party_result[0] if party_result else None:
|
||||
return (result["party_type"], result["party"])
|
||||
|
||||
if party == "Employee" and not party_result:
|
||||
# Search in Bank Accounts first for Employee, and then Employee record
|
||||
if "bank_account_no" in or_filters:
|
||||
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
|
||||
# If no party is found, search in Employee (since it has bank account details)
|
||||
if employee_result := frappe.db.get_all(
|
||||
"Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1
|
||||
):
|
||||
return ("Employee", employee_result[0])
|
||||
|
||||
party_result = frappe.db.get_all(
|
||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||
)
|
||||
|
||||
if "bank_ac_no" in or_filters:
|
||||
or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
|
||||
|
||||
if party_result:
|
||||
result = (
|
||||
party,
|
||||
party_result[0],
|
||||
)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def get_or_filters(self) -> dict:
|
||||
def get_or_filters(self, party: str | None = None) -> dict:
|
||||
"""Return OR filters for Bank Account and IBAN"""
|
||||
or_filters = {}
|
||||
if self.bank_party_account_number:
|
||||
or_filters["bank_account_no"] = self.bank_party_account_number
|
||||
bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no"
|
||||
or_filters[bank_ac_field] = self.bank_party_account_number
|
||||
|
||||
if self.bank_party_iban:
|
||||
or_filters["iban"] = self.bank_party_iban
|
||||
@@ -103,8 +99,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
if not (self.bank_party_name or self.description):
|
||||
return None
|
||||
|
||||
result = self.match_party_name_desc_in_party()
|
||||
return result
|
||||
return self.match_party_name_desc_in_party()
|
||||
|
||||
def match_party_name_desc_in_party(self) -> tuple | None:
|
||||
"""Fuzzy search party name and/or description against parties in the system"""
|
||||
@@ -113,7 +108,8 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
for party in parties:
|
||||
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
|
||||
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
|
||||
field = f"{party.lower()}_name"
|
||||
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
|
||||
|
||||
for field in ["bank_party_name", "description"]:
|
||||
if not self.get(field):
|
||||
@@ -132,16 +128,14 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None:
|
||||
skip = False
|
||||
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
|
||||
result = process.extract(
|
||||
query=self.get(field),
|
||||
choices={row.get("name"): row.get("party_name") for row in names},
|
||||
scorer=fuzz.token_set_ratio,
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
if not party_name:
|
||||
return None, skip
|
||||
|
||||
return (
|
||||
party,
|
||||
party_name,
|
||||
), skip
|
||||
return ((party, party_name), skip) if party_name else (None, skip)
|
||||
|
||||
def process_fuzzy_result(self, result: list | None):
|
||||
"""
|
||||
@@ -150,30 +144,30 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
Returns: Result, Skip (whether or not to discontinue matching)
|
||||
"""
|
||||
PARTY, SCORE, CUTOFF = 0, 1, 80
|
||||
SCORE, PARTY_ID, CUTOFF = 1, 2, 80
|
||||
|
||||
if not result or not len(result):
|
||||
return None, False
|
||||
|
||||
first_result = result[0]
|
||||
if len(result) == 1:
|
||||
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
|
||||
return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
|
||||
|
||||
second_result = result[1]
|
||||
if first_result[SCORE] > CUTOFF:
|
||||
second_result = result[1]
|
||||
# If multiple matches with the same score, return None but discontinue matching
|
||||
# Matches were found but were too close to distinguish between
|
||||
if first_result[SCORE] == second_result[SCORE]:
|
||||
return None, True
|
||||
|
||||
return first_result[PARTY], True
|
||||
return first_result[PARTY_ID], True
|
||||
else:
|
||||
return None, False
|
||||
|
||||
|
||||
def get_parties_in_order(deposit: float) -> list:
|
||||
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
|
||||
if flt(deposit) > 0:
|
||||
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
|
||||
|
||||
return parties
|
||||
return (
|
||||
["Customer", "Supplier", "Employee"] # most -> least likely to pay us
|
||||
if flt(deposit) > 0
|
||||
else ["Supplier", "Employee", "Customer"] # most -> least likely to receive from us
|
||||
)
|
||||
|
||||
@@ -430,12 +430,6 @@ frappe.ui.form.on("Journal Entry Account", {
|
||||
});
|
||||
}
|
||||
},
|
||||
cost_center: function (frm, dt, dn) {
|
||||
// Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
|
||||
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
|
||||
erpnext.journal_entry.set_account_details(frm, dt, dn);
|
||||
}
|
||||
},
|
||||
|
||||
account: function (frm, dt, dn) {
|
||||
erpnext.journal_entry.set_account_details(frm, dt, dn);
|
||||
|
||||
@@ -146,10 +146,9 @@ class TestJournalEntry(unittest.TestCase):
|
||||
"credit_in_account_currency": 0 if diff > 0 else abs(diff),
|
||||
},
|
||||
)
|
||||
jv.insert()
|
||||
|
||||
if account_bal == stock_bal:
|
||||
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
|
||||
self.assertRaises(StockAccountInvalidTransaction, jv.save)
|
||||
frappe.db.rollback()
|
||||
else:
|
||||
jv.submit()
|
||||
|
||||
@@ -543,7 +543,7 @@ class PaymentEntry(AccountsController):
|
||||
if d.reference_doctype not in valid_reference_doctypes:
|
||||
frappe.throw(
|
||||
_("Reference Doctype must be one of {0}").format(
|
||||
comma_or(_(d) for d in valid_reference_doctypes)
|
||||
comma_or([_(d) for d in valid_reference_doctypes])
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1576,6 +1576,14 @@ class PaymentEntry(AccountsController):
|
||||
elif self.payment_type in ("Pay", "Internal Transfer"):
|
||||
return self.paid_from
|
||||
|
||||
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
conversion_rate = self.target_exchange_rate
|
||||
if self.paid_from_account_currency != company_currency:
|
||||
conversion_rate = self.source_exchange_rate
|
||||
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
for d in self.get("references"):
|
||||
@@ -1790,7 +1798,7 @@ class PaymentEntry(AccountsController):
|
||||
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
||||
|
||||
for ref in self.references:
|
||||
reference_outstanding_amount = ref.outstanding_amount
|
||||
reference_outstanding_amount = flt(ref.outstanding_amount)
|
||||
abs_outstanding_amount = abs(reference_outstanding_amount)
|
||||
|
||||
if reference_outstanding_amount > 0:
|
||||
@@ -2235,10 +2243,17 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
outstanding_invoices = []
|
||||
negative_outstanding_invoices = []
|
||||
|
||||
party_account = args.get("party_account")
|
||||
|
||||
# get party account if advance account is set.
|
||||
if args.get("book_advance_payments_in_separate_party_account"):
|
||||
party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
|
||||
else:
|
||||
party_account = args.get("party_account")
|
||||
accounts = get_party_account(
|
||||
args.get("party_type"), args.get("party"), args.get("company"), include_advance=True
|
||||
)
|
||||
advance_account = accounts[1] if len(accounts) >= 1 else None
|
||||
|
||||
if party_account == advance_account:
|
||||
party_account = accounts[0]
|
||||
|
||||
if args.get("get_outstanding_invoices"):
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
@@ -2818,6 +2833,7 @@ def get_payment_entry(
|
||||
pe.paid_amount = paid_amount
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
|
||||
|
||||
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
||||
pe.project = doc.get("project") or reduce(
|
||||
@@ -2826,7 +2842,7 @@ def get_payment_entry(
|
||||
|
||||
if pe.party_type in ["Customer", "Supplier"]:
|
||||
bank_account = get_party_bank_account(pe.party_type, pe.party)
|
||||
pe.set("bank_account", bank_account)
|
||||
pe.set("party_bank_account", bank_account)
|
||||
pe.set_bank_account_data()
|
||||
|
||||
# only Purchase Invoice can be blocked individually
|
||||
|
||||
@@ -335,6 +335,7 @@ class PaymentReconciliation(Document):
|
||||
for payment in non_reconciled_payments:
|
||||
row = self.append("payments", {})
|
||||
row.update(payment)
|
||||
row.is_advance = payment.book_advance_payments_in_separate_party_account
|
||||
|
||||
def get_invoice_entries(self):
|
||||
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
|
||||
@@ -424,6 +425,9 @@ class PaymentReconciliation(Document):
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
|
||||
exc_gain_loss_posting_date = frappe.db.get_single_value(
|
||||
"Accounts Settings", "exchange_gain_loss_posting_date", cache=True
|
||||
)
|
||||
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
|
||||
default_exchange_gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
@@ -450,6 +454,11 @@ class PaymentReconciliation(Document):
|
||||
res.difference_account = default_exchange_gain_loss_account
|
||||
res.exchange_rate = inv.get("exchange_rate")
|
||||
res.update({"gain_loss_posting_date": pay.get("posting_date")})
|
||||
if not pay.get("is_advance"):
|
||||
if exc_gain_loss_posting_date == "Invoice":
|
||||
res.update({"gain_loss_posting_date": inv.get("invoice_date")})
|
||||
elif exc_gain_loss_posting_date == "Reconciliation Date":
|
||||
res.update({"gain_loss_posting_date": nowdate()})
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
|
||||
@@ -83,8 +83,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
|
||||
def test_payment_entry_against_purchase_invoice(self):
|
||||
si_usd = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
debit_to="_Test Payable USD - _TC",
|
||||
supplier="_Test Supplier USD",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
)
|
||||
@@ -108,8 +107,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
|
||||
def test_multiple_payment_entry_against_purchase_invoice(self):
|
||||
purchase_invoice = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
debit_to="_Test Payable USD - _TC",
|
||||
supplier="_Test Supplier USD",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
)
|
||||
|
||||
@@ -224,9 +224,6 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
@@ -256,9 +253,6 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
pcv_doc.reload()
|
||||
pcv_doc.cancel()
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
|
||||
@@ -23,6 +23,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",
|
||||
@@ -375,6 +376,12 @@
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_receipt_on_order_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Receipt on Order Complete"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -402,7 +409,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2022-08-10 12:57:06.241439",
|
||||
"modified": "2025-01-01 11:07:03.161950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -47,6 +47,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,8 +415,6 @@ 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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"gain_loss_posting_date",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
@@ -153,11 +154,16 @@
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Reconciled"
|
||||
},
|
||||
{
|
||||
"fieldname": "gain_loss_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-20 21:05:43.121945",
|
||||
"modified": "2025-01-23 16:09:01.058574",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log Allocations",
|
||||
@@ -167,4 +173,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class ProcessPaymentReconciliationLogAllocations(Document):
|
||||
difference_account: DF.Link | None
|
||||
difference_amount: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
gain_loss_posting_date: DF.Date | None
|
||||
invoice_number: DF.DynamicLink
|
||||
invoice_type: DF.Link
|
||||
is_advance: DF.Data | None
|
||||
|
||||
@@ -321,9 +321,12 @@ def get_recipients_and_cc(customer, doc):
|
||||
recipients = []
|
||||
for clist in doc.customers:
|
||||
if clist.customer == customer:
|
||||
recipients.append(clist.billing_email)
|
||||
if clist.billing_email:
|
||||
for email in clist.billing_email.split(","):
|
||||
recipients.append(email.strip())
|
||||
if doc.primary_mandatory and clist.primary_email:
|
||||
recipients.append(clist.primary_email)
|
||||
for email in clist.primary_email.split(","):
|
||||
recipients.append(email.strip())
|
||||
cc = []
|
||||
if doc.cc_to != "":
|
||||
try:
|
||||
|
||||
@@ -332,6 +332,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
|
||||
|
||||
let payment_terms_template = this.frm.doc.payment_terms_template;
|
||||
|
||||
erpnext.utils.get_party_details(
|
||||
this.frm,
|
||||
"erpnext.accounts.party.get_party_details",
|
||||
@@ -352,6 +354,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
||||
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
||||
|
||||
// while duplicating, don't change payment terms
|
||||
if (me.frm.doc.__run_link_triggers === false) {
|
||||
me.frm.set_value("payment_terms_template", payment_terms_template);
|
||||
me.frm.refresh_field("payment_terms_template");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -368,6 +376,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
tax_withholding_category(frm) {
|
||||
var me = this;
|
||||
let filtered_taxes = (me.frm.doc.taxes || []).filter((row) => !row.is_tax_withholding_account);
|
||||
me.frm.clear_table("taxes");
|
||||
|
||||
filtered_taxes.forEach((row) => {
|
||||
me.frm.add_child("taxes", row);
|
||||
});
|
||||
|
||||
me.frm.refresh_field("taxes");
|
||||
}
|
||||
|
||||
credit_to() {
|
||||
var me = this;
|
||||
if (this.frm.doc.credit_to) {
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
@@ -33,7 +32,7 @@ from erpnext.accounts.general_ledger import (
|
||||
merge_similar_entries,
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
@@ -838,12 +837,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def update_supplier_outstanding(self, update_outstanding):
|
||||
if update_outstanding == "No":
|
||||
update_outstanding_amt(
|
||||
self.credit_to,
|
||||
"Supplier",
|
||||
self.supplier,
|
||||
self.doctype,
|
||||
self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
update_voucher_outstanding(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
account=self.credit_to,
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None):
|
||||
@@ -1126,6 +1125,7 @@ class PurchaseInvoice(BuyingController):
|
||||
exchange_rate_map[item.purchase_receipt]
|
||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||
and item.net_rate == net_rate_map[item.pr_detail]
|
||||
and item.item_code in stock_items
|
||||
):
|
||||
discrepancy_caused_by_exchange_rate_difference = (
|
||||
item.qty * item.net_rate
|
||||
@@ -1796,13 +1796,13 @@ class PurchaseInvoice(BuyingController):
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
for row in voucher_wise_amount:
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
{
|
||||
"voucher_name": voucher_no,
|
||||
"voucher_type": voucher_details.get("voucher_type"),
|
||||
"taxable_amount": voucher_details.get("amount"),
|
||||
"voucher_name": row.voucher_name,
|
||||
"voucher_type": row.voucher_type,
|
||||
"taxable_amount": row.taxable_amount,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -45,12 +45,16 @@ frappe.listview_settings["Purchase Invoice"] = {
|
||||
},
|
||||
|
||||
onload: function (listview) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
if (frappe.model.can_create("Purchase Receipt")) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
|
||||
});
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -372,6 +372,53 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
)
|
||||
|
||||
# Creating Purchase Invoice with USD currency
|
||||
pr = frappe.new_doc("Purchase Receipt")
|
||||
pr.currency = "USD"
|
||||
pr.company = "_Test Company with perpetual inventory"
|
||||
pr.conversion_rate = (70,)
|
||||
pr.supplier = "_Test Supplier USD"
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Non Stock Item",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
},
|
||||
)
|
||||
pr.append(
|
||||
"items",
|
||||
{"item_code": "_Test Item", "qty": 1, "rate": 5, "warehouse": "Stores - TCP1"},
|
||||
)
|
||||
pr.insert()
|
||||
pr.submit()
|
||||
|
||||
# Createing purchase invoice against Purchase Receipt
|
||||
pi = create_purchase_invoice(pr.name)
|
||||
pi.conversion_rate = 80
|
||||
pi.credit_to = "_Test Payable USD - TCP1"
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# Get exchnage gain and loss account
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account")
|
||||
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
amount = frappe.db.get_value(
|
||||
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
|
||||
)
|
||||
|
||||
discrepancy_caused_by_exchange_rate_diff = abs(
|
||||
pi.items[1].base_net_amount - pr.items[1].base_net_amount
|
||||
)
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
def test_purchase_invoice_change_naming_series(self):
|
||||
pi = frappe.copy_doc(test_records[1])
|
||||
pi.insert()
|
||||
|
||||
@@ -16,6 +16,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
setup(doc) {
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
this.frm.make_methods = {
|
||||
Dunning: this.make_dunning.bind(this),
|
||||
"Invoice Discounting": this.make_invoice_discounting.bind(this),
|
||||
};
|
||||
}
|
||||
company() {
|
||||
super.company();
|
||||
@@ -61,9 +65,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
refresh(doc, dt, dn) {
|
||||
const me = this;
|
||||
super.refresh();
|
||||
if (cur_frm.msgbox && cur_frm.msgbox.$wrapper.is(":visible")) {
|
||||
|
||||
if (this.frm?.msgbox && this.frm.msgbox.$wrapper.is(":visible")) {
|
||||
// hide new msgbox
|
||||
cur_frm.msgbox.hide();
|
||||
this.frm.msgbox.hide();
|
||||
}
|
||||
|
||||
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
|
||||
@@ -121,12 +126,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
cur_frm.add_custom_button(
|
||||
this.frm.add_custom_button(
|
||||
__("Invoice Discounting"),
|
||||
function () {
|
||||
cur_frm.events.create_invoice_discounting(cur_frm);
|
||||
},
|
||||
this.make_invoice_discounting.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
|
||||
@@ -135,22 +137,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
.reduce((prev, current) => prev || current, false);
|
||||
|
||||
if (payment_is_overdue) {
|
||||
this.frm.add_custom_button(
|
||||
__("Dunning"),
|
||||
() => {
|
||||
this.frm.events.create_dunning(this.frm);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
this.frm.add_custom_button(__("Dunning"), this.make_dunning.bind(this), __("Create"));
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.docstatus === 1) {
|
||||
cur_frm.add_custom_button(
|
||||
__("Maintenance Schedule"),
|
||||
function () {
|
||||
cur_frm.cscript.make_maintenance_schedule();
|
||||
},
|
||||
this.make_maintenance_schedule.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
@@ -185,6 +179,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
make_invoice_discounting() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_dunning() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_maintenance_schedule() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
||||
@@ -1045,20 +1053,6 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
frm.set_df_property("return_against", "label", __("Adjustment Against"));
|
||||
}
|
||||
},
|
||||
|
||||
create_invoice_discounting: function (frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: frm,
|
||||
});
|
||||
},
|
||||
|
||||
create_dunning: function (frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: frm,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
|
||||
@@ -24,7 +24,11 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_disposal_account_and_cost_center,
|
||||
@@ -1192,14 +1196,14 @@ class SalesInvoice(SellingController):
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if update_outstanding == "No":
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
|
||||
update_outstanding_amt(
|
||||
self.debit_to,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.doctype,
|
||||
self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
update_voucher_outstanding(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
account=self.debit_to,
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
)
|
||||
|
||||
elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):
|
||||
|
||||
@@ -32,12 +32,16 @@ frappe.listview_settings["Sales Invoice"] = {
|
||||
right_column: "grand_total",
|
||||
|
||||
onload: function (listview) {
|
||||
listview.page.add_action_item(__("Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
if (frappe.model.can_create("Delivery Note")) {
|
||||
listview.page.add_action_item(__("Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
|
||||
});
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4235,6 +4235,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.company = "_Test Company"
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
@@ -4245,6 +4246,30 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
def test_pos_returns_with_party_account_currency(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.payments = []
|
||||
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
|
||||
pos_profile.save()
|
||||
|
||||
pos = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
currency="USD",
|
||||
conversion_rate=86.595000000,
|
||||
qty=2,
|
||||
do_not_save=True,
|
||||
)
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.debit_to = "_Test Receivable USD - _TC"
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35})
|
||||
pos.save().submit()
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -634,9 +634,7 @@ class Subscription(Document):
|
||||
"""
|
||||
invoice = frappe.get_all(
|
||||
self.invoice_document_type,
|
||||
{
|
||||
"subscription": self.name,
|
||||
},
|
||||
{"subscription": self.name, "docstatus": ("<", 2)},
|
||||
limit=1,
|
||||
order_by="to_date desc",
|
||||
pluck="name",
|
||||
@@ -675,6 +673,7 @@ class Subscription(Document):
|
||||
self.invoice_document_type,
|
||||
{
|
||||
"subscription": self.name,
|
||||
"docstatus": 1,
|
||||
"status": ["!=", "Paid"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -87,6 +87,7 @@ def get_party_details(inv):
|
||||
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
if inv.doctype == "Payment Entry":
|
||||
inv.tax_withholding_net_total = inv.net_total
|
||||
inv.base_tax_withholding_net_total = inv.net_total
|
||||
|
||||
pan_no = ""
|
||||
parties = []
|
||||
@@ -326,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
# once tds is deducted, not need to add vouchers in the invoice
|
||||
voucher_wise_amount = {}
|
||||
else:
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount)
|
||||
|
||||
elif party_type == "Customer":
|
||||
if tax_deducted:
|
||||
@@ -356,13 +357,16 @@ def is_tax_deducted_on_the_basis_of_inv(vouchers):
|
||||
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = (
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total"
|
||||
)
|
||||
voucher_wise_amount = {}
|
||||
voucher_wise_amount = []
|
||||
vouchers = []
|
||||
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = [
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
|
||||
"name",
|
||||
"grand_total",
|
||||
]
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
frappe.scrub(party_type): ["in", parties],
|
||||
@@ -376,15 +380,24 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
||||
)
|
||||
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field])
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
|
||||
|
||||
for d in invoices_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
|
||||
voucher_wise_amount.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_name": d.name,
|
||||
"voucher_type": doctype,
|
||||
"taxable_amount": d.base_net_total,
|
||||
"grand_total": d.grand_total,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
journal_entries_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT j.name, ja.credit - ja.debit AS amount
|
||||
SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type
|
||||
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
|
||||
WHERE
|
||||
j.name = ja.parent
|
||||
@@ -403,13 +416,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if journal_entries_details:
|
||||
for d in journal_entries_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
|
||||
for d in journal_entries_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_name": d.name,
|
||||
"voucher_type": "Journal Entry",
|
||||
"taxable_amount": d.amount,
|
||||
"reference_type": d.reference_type,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
@@ -508,12 +528,24 @@ def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
|
||||
return advance_tax_from_across_fiscal_year
|
||||
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount):
|
||||
tds_amount = 0
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
|
||||
pi_grand_total = 0
|
||||
pi_base_net_total = 0
|
||||
jv_credit_amt = 0
|
||||
pe_credit_amt = 0
|
||||
|
||||
for row in voucher_wise_amount:
|
||||
if row.voucher_type == "Purchase Invoice":
|
||||
pi_grand_total += row.get("grand_total", 0)
|
||||
pi_base_net_total += row.get("taxable_amount", 0)
|
||||
|
||||
if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice":
|
||||
jv_credit_amt += row.get("taxable_amount", 0)
|
||||
|
||||
## for TDS to be deducted on advances
|
||||
payment_entry_filters = {
|
||||
pe_filters = {
|
||||
"party_type": "Supplier",
|
||||
"party": ("in", parties),
|
||||
"docstatus": 1,
|
||||
@@ -524,70 +556,49 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
"company": inv.company,
|
||||
}
|
||||
|
||||
field = "sum(tax_withholding_net_total)"
|
||||
consider_party_ledger_amt = cint(tax_details.consider_party_ledger_amount)
|
||||
|
||||
if cint(tax_details.consider_party_ledger_amount):
|
||||
invoice_filters.pop("apply_tds", None)
|
||||
field = "sum(grand_total)"
|
||||
|
||||
payment_entry_filters.pop("apply_tax_withholding_amount", None)
|
||||
payment_entry_filters.pop("tax_withholding_category", None)
|
||||
|
||||
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
|
||||
supp_jv_credit_amt = (
|
||||
frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"parent": ("in", vouchers),
|
||||
"docstatus": 1,
|
||||
"party": ("in", parties),
|
||||
"reference_type": ("!=", "Purchase Invoice"),
|
||||
},
|
||||
"sum(credit_in_account_currency - debit_in_account_currency)",
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
if consider_party_ledger_amt:
|
||||
pe_filters.pop("apply_tax_withholding_amount", None)
|
||||
pe_filters.pop("tax_withholding_category", None)
|
||||
|
||||
# Get Amount via payment entry
|
||||
payment_entry_amounts = frappe.db.get_all(
|
||||
payment_entries = frappe.db.get_all(
|
||||
"Payment Entry",
|
||||
filters=payment_entry_filters,
|
||||
fields=["sum(unallocated_amount) as amount", "payment_type"],
|
||||
group_by="payment_type",
|
||||
filters=pe_filters,
|
||||
fields=["name", "unallocated_amount as taxable_amount", "payment_type"],
|
||||
)
|
||||
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
if type.payment_type == "Pay":
|
||||
supp_credit_amt += type.amount
|
||||
else:
|
||||
supp_credit_amt -= type.amount
|
||||
for row in payment_entries:
|
||||
value = row.taxable_amount if row.payment_type == "Pay" else -1 * row.taxable_amount
|
||||
pe_credit_amt += value
|
||||
voucher_wise_amount.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_name": row.name,
|
||||
"voucher_type": "Payment Entry",
|
||||
"taxable_amount": value,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
threshold = tax_details.get("threshold", 0)
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
supp_credit_amt = jv_credit_amt + pe_credit_amt + inv.get("tax_withholding_net_total", 0)
|
||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
||||
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
||||
else:
|
||||
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
|
||||
# if consider_party_ledger_amount is checked, then threshold will be based on grand total
|
||||
amt_for_threshold = pi_grand_total if consider_party_ledger_amt else pi_base_net_total
|
||||
|
||||
has_cumulative_threshold_breached = (
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
cumulative_threshold_breached = (
|
||||
cumulative_threshold and (supp_credit_amt + amt_for_threshold) >= cumulative_threshold
|
||||
)
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (has_cumulative_threshold_breached):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
|
||||
)
|
||||
supp_credit_amt += net_total
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (cumulative_threshold_breached):
|
||||
supp_credit_amt += pi_base_net_total
|
||||
|
||||
if has_cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
|
||||
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
|
||||
if cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
|
||||
supp_credit_amt = pi_base_net_total + tax_withholding_net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
tds_amount = get_lower_deduction_amount(
|
||||
|
||||
@@ -569,6 +569,15 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
pe = create_payment_entry(
|
||||
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier6", paid_amount=1000
|
||||
)
|
||||
pe.apply_tax_withholding_amount = 1
|
||||
pe.tax_withholding_category = "Test Multi Invoice Category"
|
||||
pe.save()
|
||||
pe.submit()
|
||||
invoices.append(pe)
|
||||
|
||||
pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True)
|
||||
pi2.apply_tds = 1
|
||||
pi2.tax_withholding_category = "Test Multi Invoice Category"
|
||||
@@ -584,6 +593,8 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[2].voucher_name == pe.name)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[2].taxable_amount == pe.paid_amount)
|
||||
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
|
||||
@@ -35,7 +35,7 @@ def make_gl_entries(
|
||||
make_acc_dimensions_offsetting_entry(gl_map)
|
||||
validate_accounting_period(gl_map)
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
create_payment_ledger_entry(
|
||||
@@ -163,12 +163,12 @@ def validate_accounting_period(gl_map):
|
||||
)
|
||||
|
||||
|
||||
def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False):
|
||||
if not gl_map:
|
||||
return []
|
||||
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
|
||||
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision, from_repost)
|
||||
|
||||
if merge_entries:
|
||||
gl_map = merge_similar_entries(gl_map, precision)
|
||||
@@ -178,13 +178,17 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
return gl_map
|
||||
|
||||
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_repost=False):
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
|
||||
# Validate budget against main cost center
|
||||
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
|
||||
if not from_repost:
|
||||
validate_expense_against_budget(
|
||||
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
|
||||
)
|
||||
|
||||
cost_center_allocation = get_cost_center_allocation_data(
|
||||
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-02-25 17:03:34",
|
||||
"disable_prepared_report": 0,
|
||||
@@ -9,7 +9,7 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2022-02-11 10:18:36.956558",
|
||||
"modified": "2025-01-27 18:40:24.493829",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Gross Profit",
|
||||
|
||||
@@ -178,7 +178,14 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
# removing Item Code and Item Name columns
|
||||
del columns[4:6]
|
||||
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
|
||||
for src in gross_profit_data.si_list:
|
||||
if src.indent == 1:
|
||||
total_base_amount += src.base_amount or 0.0
|
||||
total_buying_amount += src.buying_amount or 0.0
|
||||
|
||||
row = frappe._dict()
|
||||
row.indent = src.indent
|
||||
row.parent_invoice = src.parent_invoice
|
||||
@@ -189,6 +196,27 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"sales_invoice": "Total",
|
||||
"qty": None,
|
||||
"avg._selling_rate": None,
|
||||
"valuation_rate": None,
|
||||
"selling_amount": total_base_amount,
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_%": flt(
|
||||
(total_gross_profit / total_base_amount) * 100.0,
|
||||
cint(frappe.db.get_default("currency_precision")) or 3,
|
||||
)
|
||||
if total_base_amount
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
||||
for src in gross_profit_data.grouped_data:
|
||||
|
||||
@@ -605,3 +605,33 @@ class TestGrossProfit(FrappeTestCase):
|
||||
item_from_sinv2 = [x for x in data if x.parent_invoice == sinv2.name]
|
||||
self.assertEqual(len(item_from_sinv2), 1)
|
||||
self.assertEqual(1800, item_from_sinv2[0].valuation_rate)
|
||||
|
||||
def test_gross_profit_groupby_invoices(self):
|
||||
create_sales_invoice(
|
||||
qty=1,
|
||||
rate=100,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
)
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
@@ -1644,7 +1644,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
|
||||
if wh_details.account == account and not wh_details.is_group
|
||||
]
|
||||
|
||||
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
|
||||
total_stock_value = get_stock_value_on(related_warehouses, posting_date, company=company)
|
||||
|
||||
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
|
||||
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
|
||||
|
||||
@@ -34,6 +34,7 @@ frappe.ui.form.on("Depreciation Schedule", {
|
||||
asset_depr_schedule_name: frm.doc.name,
|
||||
date: row.schedule_date,
|
||||
},
|
||||
debounce: 1000,
|
||||
callback: function (r) {
|
||||
frappe.model.sync(r.message);
|
||||
frm.refresh();
|
||||
|
||||
@@ -1069,7 +1069,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
|
||||
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||
if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
|
||||
value_after_depreciation = row.value_after_depreciation + difference_amount
|
||||
value_after_depreciation = row.value_after_depreciation - difference_amount
|
||||
|
||||
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
|
||||
"Written Down Value",
|
||||
|
||||
@@ -5,7 +5,6 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
frappe.ui.form.on("Asset Value Adjustment", {
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("company", "cost_center", "cost_center");
|
||||
frm.set_query("cost_center", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -22,6 +21,14 @@ frappe.ui.form.on("Asset Value Adjustment", {
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("difference_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
@@ -37,7 +44,7 @@ frappe.ui.form.on("Asset Value Adjustment", {
|
||||
},
|
||||
|
||||
asset: function (frm) {
|
||||
frm.trigger("set_current_asset_value");
|
||||
frm.trigger("set_acc_dimension");
|
||||
},
|
||||
|
||||
finance_book: function (frm) {
|
||||
@@ -60,4 +67,15 @@ frappe.ui.form.on("Asset Value Adjustment", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
set_acc_dimension: function (frm) {
|
||||
if (frm.doc.asset) {
|
||||
frm.call({
|
||||
method: "erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment.get_value_of_accounting_dimensions",
|
||||
args: {
|
||||
asset_name: frm.doc.asset,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"new_asset_value",
|
||||
"column_break_11",
|
||||
"difference_amount",
|
||||
"difference_account",
|
||||
"journal_entry",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
@@ -54,6 +55,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Journal Entry",
|
||||
"options": "Journal Entry",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -79,6 +81,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "New Asset Value",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -120,12 +123,20 @@
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Difference Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-22 14:10:23.085181",
|
||||
"modified": "2024-08-13 16:21:18.639208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Value Adjustment",
|
||||
@@ -182,4 +193,4 @@
|
||||
"sort_order": "DESC",
|
||||
"title_field": "asset",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class AssetValueAdjustment(Document):
|
||||
cost_center: DF.Link | None
|
||||
current_asset_value: DF.Currency
|
||||
date: DF.Date
|
||||
difference_account: DF.Link
|
||||
difference_amount: DF.Currency
|
||||
finance_book: DF.Link | None
|
||||
journal_entry: DF.Link | None
|
||||
@@ -47,6 +48,7 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
def on_submit(self):
|
||||
self.make_depreciation_entry()
|
||||
self.set_value_after_depreciation()
|
||||
self.update_asset(self.new_asset_value)
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
@@ -76,7 +78,10 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
|
||||
def set_difference_amount(self):
|
||||
self.difference_amount = flt(self.current_asset_value - self.new_asset_value)
|
||||
self.difference_amount = flt(self.new_asset_value - self.current_asset_value)
|
||||
|
||||
def set_value_after_depreciation(self):
|
||||
frappe.db.set_value("Asset", self.asset, "value_after_depreciation", self.new_asset_value)
|
||||
|
||||
def set_current_asset_value(self):
|
||||
if not self.current_asset_value and self.asset:
|
||||
@@ -85,7 +90,7 @@ class AssetValueAdjustment(Document):
|
||||
def make_depreciation_entry(self):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
(
|
||||
_,
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
@@ -95,28 +100,41 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = self.date
|
||||
je.company = self.company
|
||||
je.remark = f"Depreciation Entry against {self.asset} worth {self.difference_amount}"
|
||||
je.remark = f"Revaluation Entry against {self.asset} worth {self.difference_amount}"
|
||||
je.finance_book = self.finance_book
|
||||
|
||||
credit_entry = {
|
||||
"account": accumulated_depreciation_account,
|
||||
"credit_in_account_currency": self.difference_amount,
|
||||
"cost_center": depreciation_cost_center or self.cost_center,
|
||||
entry_template = {
|
||||
"cost_center": self.cost_center or depreciation_cost_center,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": self.asset,
|
||||
"reference_name": asset.name,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": depreciation_expense_account,
|
||||
"debit_in_account_currency": self.difference_amount,
|
||||
"cost_center": depreciation_cost_center or self.cost_center,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": self.asset,
|
||||
}
|
||||
if self.difference_amount < 0:
|
||||
credit_entry = {
|
||||
"account": fixed_asset_account,
|
||||
"credit_in_account_currency": -self.difference_amount,
|
||||
**entry_template,
|
||||
}
|
||||
debit_entry = {
|
||||
"account": self.difference_account,
|
||||
"debit_in_account_currency": -self.difference_amount,
|
||||
**entry_template,
|
||||
}
|
||||
elif self.difference_amount > 0:
|
||||
credit_entry = {
|
||||
"account": self.difference_account,
|
||||
"credit_in_account_currency": self.difference_amount,
|
||||
**entry_template,
|
||||
}
|
||||
debit_entry = {
|
||||
"account": fixed_asset_account,
|
||||
"debit_in_account_currency": self.difference_amount,
|
||||
**entry_template,
|
||||
}
|
||||
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
|
||||
@@ -179,3 +197,9 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_value_of_accounting_dimensions(asset_name):
|
||||
dimension_fields = [*frappe.get_list("Accounting Dimension", pluck="fieldname"), "cost_center"]
|
||||
return frappe.db.get_value("Asset", asset_name, fieldname=dimension_fields, as_dict=True)
|
||||
|
||||
@@ -93,8 +93,8 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
|
||||
("_Test Depreciations - _TC", 4625.29, 0.0),
|
||||
("_Test Difference Account - _TC", 4625.29, 0.0),
|
||||
("_Test Fixed Asset - _TC", 0.0, 4625.29),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
@@ -177,8 +177,8 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
|
||||
# Test gl entry creted from asset value adjustemnet
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 0.0, 5625.29),
|
||||
("_Test Depreciations - _TC", 5625.29, 0.0),
|
||||
("_Test Difference Account - _TC", 5625.29, 0.0),
|
||||
("_Test Fixed Asset - _TC", 0.0, 5625.29),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
@@ -259,6 +259,39 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_difference_amount(self):
|
||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location")
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
asset_doc.calculate_depreciation = 1
|
||||
asset_doc.available_for_use_date = "2023-01-15"
|
||||
asset_doc.purchase_date = "2023-01-15"
|
||||
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 200,
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 12,
|
||||
"frequency_of_depreciation": 1,
|
||||
"depreciation_start_date": "2023-01-31",
|
||||
},
|
||||
)
|
||||
asset_doc.submit()
|
||||
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name,
|
||||
current_asset_value=54000,
|
||||
new_asset_value=50000.0,
|
||||
date="2023-08-21",
|
||||
)
|
||||
adj_doc.submit()
|
||||
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
|
||||
self.assertEqual(difference_amount, -4000)
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
|
||||
|
||||
|
||||
def make_asset_value_adjustment(**args):
|
||||
args = frappe._dict(args)
|
||||
@@ -272,7 +305,22 @@ def make_asset_value_adjustment(**args):
|
||||
"new_asset_value": args.new_asset_value,
|
||||
"current_asset_value": args.current_asset_value,
|
||||
"cost_center": args.cost_center or "Main - _TC",
|
||||
"difference_account": make_difference_account(),
|
||||
}
|
||||
).insert()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def make_difference_account(**args):
|
||||
account = "_Test Difference Account - _TC"
|
||||
if not frappe.db.exists("Account", account):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "_Test Difference Account"
|
||||
acc.parent_account = "Direct Income - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.is_group = 0
|
||||
acc.insert()
|
||||
return acc.name
|
||||
else:
|
||||
return account
|
||||
|
||||
@@ -367,7 +367,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
|
||||
if (is_drop_ship && doc.status != "Delivered") {
|
||||
this.frm.add_custom_button(__("Delivered"), this.delivered_by_supplier, __("Status"));
|
||||
this.frm.add_custom_button(
|
||||
__("Delivered"),
|
||||
this.delivered_by_supplier.bind(this),
|
||||
__("Status")
|
||||
);
|
||||
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
|
||||
}
|
||||
|
||||
@@ -44,16 +44,22 @@ frappe.listview_settings["Purchase Order"] = {
|
||||
listview.call_for_selected_items(method, { status: "Submitted" });
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Purchase Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
|
||||
});
|
||||
if (frappe.model.can_create("Purchase Invoice")) {
|
||||
listview.page.add_action_item(__("Purchase Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Purchase Receipt"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
|
||||
});
|
||||
if (frappe.model.can_create("Purchase Receipt")) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Advance Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
|
||||
});
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
listview.page.add_action_item(__("Advance Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,12 +11,20 @@ frappe.listview_settings["Supplier Quotation"] = {
|
||||
},
|
||||
|
||||
onload: function (listview) {
|
||||
listview.page.add_action_item(__("Purchase Order"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
|
||||
});
|
||||
if (frappe.model.can_create("Purchase Order")) {
|
||||
listview.page.add_action_item(__("Purchase Order"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Purchase Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice");
|
||||
});
|
||||
if (frappe.model.can_create("Purchase Invoice")) {
|
||||
listview.page.add_action_item(__("Purchase Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(
|
||||
listview,
|
||||
"Supplier Quotation",
|
||||
"Purchase Invoice"
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections import defaultdict
|
||||
import frappe
|
||||
from frappe import _, bold, qb, throw
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder import Criterion, DocType
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import (
|
||||
@@ -252,6 +252,8 @@ class AccountsController(TransactionBase):
|
||||
self.validate_deferred_income_expense_account()
|
||||
self.set_inter_company_account()
|
||||
|
||||
self.set_taxes_and_charges()
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
self.calculate_paid_amount()
|
||||
# apply tax withholding only if checked and applicable
|
||||
@@ -266,6 +268,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.set_total_in_words()
|
||||
self.set_default_letter_head()
|
||||
self.validate_company_in_accounting_dimension()
|
||||
|
||||
def set_default_letter_head(self):
|
||||
if hasattr(self, "letter_head") and not self.letter_head:
|
||||
@@ -403,6 +406,39 @@ class AccountsController(TransactionBase):
|
||||
for row in batches:
|
||||
frappe.delete_doc("Batch", row.name)
|
||||
|
||||
def validate_company_in_accounting_dimension(self):
|
||||
doc_field = DocType("DocField")
|
||||
accounting_dimension = DocType("Accounting Dimension")
|
||||
dimension_list = (
|
||||
frappe.qb.from_(accounting_dimension)
|
||||
.select(accounting_dimension.document_type)
|
||||
.join(doc_field)
|
||||
.on(doc_field.parent == accounting_dimension.document_type)
|
||||
.where(doc_field.fieldname == "company")
|
||||
).run(as_list=True)
|
||||
|
||||
dimension_list = sum(dimension_list, ["Project"])
|
||||
self.validate_company(dimension_list)
|
||||
|
||||
for child in self.get_all_children() or []:
|
||||
self.validate_company(dimension_list, child)
|
||||
|
||||
def validate_company(self, dimension_list, child=None):
|
||||
for dimension in dimension_list:
|
||||
if not child:
|
||||
dimension_value = self.get(frappe.scrub(dimension))
|
||||
else:
|
||||
dimension_value = child.get(frappe.scrub(dimension))
|
||||
|
||||
if dimension_value:
|
||||
company = frappe.get_cached_value(dimension, dimension_value, "company")
|
||||
if company and company != self.company:
|
||||
frappe.throw(
|
||||
_("{0}: {1} does not belong to the Company: {2}").format(
|
||||
dimension, frappe.bold(dimension_value), self.company
|
||||
)
|
||||
)
|
||||
|
||||
def validate_return_against_account(self):
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
|
||||
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
|
||||
@@ -785,6 +821,9 @@ class AccountsController(TransactionBase):
|
||||
and item.get("use_serial_batch_fields")
|
||||
)
|
||||
):
|
||||
if fieldname == "batch_no" and not item.batch_no:
|
||||
item.set("rate", ret.get("rate"))
|
||||
item.set("price_list_rate", ret.get("price_list_rate"))
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
|
||||
@@ -935,6 +974,12 @@ class AccountsController(TransactionBase):
|
||||
):
|
||||
return True
|
||||
|
||||
def set_taxes_and_charges(self):
|
||||
if frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
|
||||
if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"):
|
||||
if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
|
||||
self.append_taxes_from_master(tax_master_doctype)
|
||||
|
||||
def append_taxes_from_master(self, tax_master_doctype=None):
|
||||
if self.get("taxes_and_charges"):
|
||||
if not tax_master_doctype:
|
||||
@@ -2413,10 +2458,15 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
if (
|
||||
flt(total, self.precision("grand_total")) - flt(grand_total, self.precision("grand_total"))
|
||||
abs(
|
||||
flt(total, self.precision("grand_total"))
|
||||
- flt(grand_total, self.precision("grand_total"))
|
||||
)
|
||||
> 0.1
|
||||
or flt(base_total, self.precision("base_grand_total"))
|
||||
- flt(base_grand_total, self.precision("base_grand_total"))
|
||||
or abs(
|
||||
flt(base_total, self.precision("base_grand_total"))
|
||||
- flt(base_grand_total, self.precision("base_grand_total"))
|
||||
)
|
||||
> 0.1
|
||||
):
|
||||
frappe.throw(
|
||||
@@ -2969,6 +3019,7 @@ def get_advance_payment_entries(
|
||||
(payment_ref.allocated_amount).as_("amount"),
|
||||
(payment_ref.name).as_("reference_row"),
|
||||
(payment_ref.reference_name).as_("against_order"),
|
||||
(payment_entry.book_advance_payments_in_separate_party_account),
|
||||
)
|
||||
|
||||
q = q.where(payment_ref.reference_doctype == order_doctype)
|
||||
@@ -3013,6 +3064,7 @@ def get_common_query(
|
||||
(payment_entry.name).as_("reference_name"),
|
||||
payment_entry.posting_date,
|
||||
(payment_entry.remarks).as_("remarks"),
|
||||
(payment_entry.book_advance_payments_in_separate_party_account),
|
||||
)
|
||||
.where(payment_entry.payment_type == payment_type)
|
||||
.where(payment_entry.party_type == party_type)
|
||||
@@ -3684,6 +3736,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
parent.update_project()
|
||||
|
||||
@@ -807,7 +807,27 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
item_group = item_group_doc.parent_item_group
|
||||
|
||||
if not taxes:
|
||||
return frappe.get_all("Item Tax Template", filters={"disabled": 0, "company": company}, as_list=True)
|
||||
or_filters = []
|
||||
if txt:
|
||||
search_fields = ["name"]
|
||||
|
||||
tax_template_doc = frappe.get_meta("Item Tax Template")
|
||||
|
||||
if title_field := tax_template_doc.title_field:
|
||||
search_fields.append(title_field)
|
||||
if tax_template_doc.search_fields:
|
||||
search_fields.extend(tax_template_doc.get_search_fields())
|
||||
|
||||
for f in search_fields:
|
||||
or_filters.append([doctype, f.strip(), "like", f"%{txt}%"])
|
||||
|
||||
return frappe.get_list(
|
||||
"Item Tax Template",
|
||||
filters={"disabled": 0, "company": company},
|
||||
or_filters=or_filters,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
else:
|
||||
valid_from = filters.get("valid_from")
|
||||
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
|
||||
@@ -820,7 +840,8 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
}
|
||||
|
||||
taxes = _get_item_tax_template(args, taxes, for_validate=True)
|
||||
return [(d,) for d in set(taxes)]
|
||||
txt = txt.lower()
|
||||
return [(d,) for d in set(taxes) if not txt or txt in d.lower()]
|
||||
|
||||
|
||||
def get_fields(doctype, fields=None):
|
||||
|
||||
@@ -174,7 +174,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
)
|
||||
|
||||
for column in fields:
|
||||
returned_qty = flt(already_returned_data.get(column, 0)) if len(already_returned_data) > 0 else 0
|
||||
returned_qty = (
|
||||
flt(already_returned_data.get(column, 0), stock_qty_precision)
|
||||
if len(already_returned_data) > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
if column == "stock_qty" and not args.get("return_qty_from_rejected_warehouse"):
|
||||
reference_qty = ref.get(column)
|
||||
@@ -186,7 +190,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
|
||||
max_returnable_qty = flt(reference_qty, stock_qty_precision) - returned_qty
|
||||
max_returnable_qty = flt(flt(reference_qty, stock_qty_precision) - returned_qty, stock_qty_precision)
|
||||
label = column.replace("_", " ").title()
|
||||
|
||||
if reference_qty:
|
||||
@@ -370,6 +374,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if doc.get("is_return"):
|
||||
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
|
||||
doc.consolidated_invoice = ""
|
||||
# no copy enabled for party_account_currency
|
||||
doc.party_account_currency = source.party_account_currency
|
||||
doc.set("payments", [])
|
||||
doc.update_billed_amount_in_delivery_note = True
|
||||
for data in source.payments:
|
||||
@@ -1150,3 +1156,9 @@ def get_available_serial_nos(serial_nos, warehouse):
|
||||
return frappe.get_all(
|
||||
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_data(invoice):
|
||||
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
|
||||
return payment
|
||||
|
||||
@@ -333,7 +333,7 @@ class SellingController(StockController):
|
||||
"batch_no": p.batch_no if self.docstatus == 2 else None,
|
||||
"uom": p.uom,
|
||||
"serial_and_batch_bundle": p.serial_and_batch_bundle
|
||||
or get_serial_and_batch_bundle(p, self),
|
||||
or get_serial_and_batch_bundle(p, self, d),
|
||||
"name": d.name,
|
||||
"target_warehouse": p.target_warehouse,
|
||||
"company": self.company,
|
||||
@@ -714,6 +714,16 @@ class SellingController(StockController):
|
||||
if self.doctype == "POS Invoice":
|
||||
return
|
||||
|
||||
items = [item.item_code for item in self.get("items")]
|
||||
item_stock_map = frappe._dict(
|
||||
frappe.get_all(
|
||||
"Item",
|
||||
filters={"item_code": ["in", items]},
|
||||
fields=["item_code", "is_stock_item"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
for d in self.get("items"):
|
||||
if self.doctype == "Sales Invoice":
|
||||
stock_items = [
|
||||
@@ -747,7 +757,7 @@ class SellingController(StockController):
|
||||
frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
|
||||
if item_stock_map.get(d.item_code):
|
||||
if stock_items in check_list:
|
||||
frappe.throw(duplicate_items_msg)
|
||||
else:
|
||||
@@ -789,7 +799,7 @@ def set_default_income_account_for_item(obj):
|
||||
set_item_default(d.item_code, obj.company, "income_account", d.income_account)
|
||||
|
||||
|
||||
def get_serial_and_batch_bundle(child, parent):
|
||||
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if child.get("use_serial_batch_fields"):
|
||||
@@ -809,7 +819,7 @@ def get_serial_and_batch_bundle(child, parent):
|
||||
"warehouse": child.warehouse,
|
||||
"voucher_type": parent.doctype,
|
||||
"voucher_no": parent.name if parent.docstatus < 2 else None,
|
||||
"voucher_detail_no": child.name,
|
||||
"voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name,
|
||||
"posting_date": parent.posting_date,
|
||||
"posting_time": parent.posting_time,
|
||||
"qty": child.qty,
|
||||
|
||||
@@ -216,6 +216,10 @@ class StockController(AccountsController):
|
||||
if self.doctype == "Asset Capitalization":
|
||||
table_name = "stock_items"
|
||||
|
||||
parent_details = frappe._dict()
|
||||
if table_name == "packed_items":
|
||||
parent_details = self.get_parent_details_for_packed_items()
|
||||
|
||||
for row in self.get(table_name):
|
||||
if row.serial_and_batch_bundle and (row.serial_no or row.batch_no):
|
||||
self.validate_serial_nos_and_batches_with_bundle(row)
|
||||
@@ -246,13 +250,20 @@ class StockController(AccountsController):
|
||||
}
|
||||
|
||||
if row.get("qty") or row.get("consumed_qty") or row.get("stock_qty"):
|
||||
self.update_bundle_details(bundle_details, table_name, row)
|
||||
self.update_bundle_details(bundle_details, table_name, row, parent_details=parent_details)
|
||||
self.create_serial_batch_bundle(bundle_details, row)
|
||||
|
||||
if row.get("rejected_qty"):
|
||||
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||
self.create_serial_batch_bundle(bundle_details, row)
|
||||
|
||||
def get_parent_details_for_packed_items(self):
|
||||
parent_details = frappe._dict()
|
||||
for row in self.get("items"):
|
||||
parent_details[row.name] = row
|
||||
|
||||
return parent_details
|
||||
|
||||
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
||||
if not self.get("is_return"):
|
||||
return
|
||||
@@ -387,7 +398,7 @@ class StockController(AccountsController):
|
||||
|
||||
return False
|
||||
|
||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False, parent_details=None):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
# Since qty field is different for different doctypes
|
||||
@@ -429,6 +440,11 @@ class StockController(AccountsController):
|
||||
warehouse = row.get("target_warehouse") or row.get("warehouse")
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
if table_name == "packed_items":
|
||||
if not warehouse:
|
||||
warehouse = parent_details[row.parent_detail_docname].warehouse
|
||||
bundle_details["voucher_detail_no"] = parent_details[row.parent_detail_docname].name
|
||||
|
||||
bundle_details.update(
|
||||
{
|
||||
"qty": qty,
|
||||
@@ -921,9 +937,11 @@ class StockController(AccountsController):
|
||||
row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname])
|
||||
|
||||
def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
from erpnext.stock.serial_batch_bundle import update_batch_qty
|
||||
from erpnext.stock.stock_ledger import make_sl_entries
|
||||
|
||||
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
||||
update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher)
|
||||
|
||||
def make_gl_entries_on_cancel(self):
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
@@ -1546,6 +1564,27 @@ def repost_required_for_queue(doc: StockController) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def check_item_quality_inspection(doctype, items):
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
inspection_fieldname_map = {
|
||||
"Purchase Receipt": "inspection_required_before_purchase",
|
||||
"Purchase Invoice": "inspection_required_before_purchase",
|
||||
"Subcontracting Receipt": "inspection_required_before_purchase",
|
||||
"Sales Invoice": "inspection_required_before_delivery",
|
||||
"Delivery Note": "inspection_required_before_delivery",
|
||||
}
|
||||
|
||||
items_to_remove = []
|
||||
for item in items:
|
||||
if not frappe.db.get_value("Item", item.get("item_code"), inspection_fieldname_map.get(doctype)):
|
||||
items_to_remove.append(item)
|
||||
items = [item for item in items if item not in items_to_remove]
|
||||
|
||||
return items
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
if isinstance(items, str):
|
||||
|
||||
@@ -113,11 +113,10 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
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
|
||||
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.get_precision("Purchase Order Item", "qty"),
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, getdate, nowdate
|
||||
from frappe.utils.data import getdate as convert_to_date
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
@@ -16,6 +17,7 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
@@ -868,6 +870,67 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertEqual(pi.items[0].rate, arms_length_price)
|
||||
self.assertEqual(pi.items[0].valuation_rate, 100)
|
||||
|
||||
@change_settings("Accounts Settings", {"exchange_gain_loss_posting_date": "Reconciliation Date"})
|
||||
def test_17_gain_loss_posting_date_for_normal_payment(self):
|
||||
# Sales Invoice in Foreign Currency
|
||||
rate = 80
|
||||
rate_in_account_currency = 1
|
||||
|
||||
adv_date = convert_to_date(add_days(nowdate(), -2))
|
||||
inv_date = convert_to_date(add_days(nowdate(), -1))
|
||||
|
||||
si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=rate_in_account_currency)
|
||||
|
||||
# Test payments with different exchange rates
|
||||
pe = self.create_payment_entry(posting_date=adv_date, amount=1, source_exc_rate=75.1).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.from_invoice_date = add_days(nowdate(), -1)
|
||||
pr.to_invoice_date = nowdate()
|
||||
pr.from_payment_date = add_days(nowdate(), -2)
|
||||
pr.to_payment_date = nowdate()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
# Outstanding in both currencies should be '0'
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_pe), 1)
|
||||
self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
|
||||
|
||||
self.assertEqual(
|
||||
getdate(nowdate()), frappe.db.get_value("Journal Entry", exc_je_for_pe[0].parent, "posting_date")
|
||||
)
|
||||
# Cancel Payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
# outstanding should be same as grand total
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, rate_in_account_currency)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been cancelled
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_pe, [])
|
||||
|
||||
def test_20_journal_against_sales_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||
@@ -1470,32 +1533,32 @@ class TestAccountsController(FrappeTestCase):
|
||||
|
||||
# Invoices
|
||||
si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||
si1.department = "Management"
|
||||
si1.department = "Management - _TC"
|
||||
si1.save().submit()
|
||||
|
||||
si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||
si2.department = "Operations"
|
||||
si2.department = "Operations - _TC"
|
||||
si2.save().submit()
|
||||
|
||||
# Payments
|
||||
cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||
cr_note1.department = "Management"
|
||||
cr_note1.department = "Management - _TC"
|
||||
cr_note1.is_return = 1
|
||||
cr_note1.save().submit()
|
||||
|
||||
cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||
cr_note2.department = "Legal"
|
||||
cr_note2.department = "Legal - _TC"
|
||||
cr_note2.is_return = 1
|
||||
cr_note2.save().submit()
|
||||
|
||||
pe1 = get_payment_entry(si1.doctype, si1.name)
|
||||
pe1.references = []
|
||||
pe1.department = "Research & Development"
|
||||
pe1.department = "Research & Development - _TC"
|
||||
pe1.save().submit()
|
||||
|
||||
pe2 = get_payment_entry(si1.doctype, si1.name)
|
||||
pe2.references = []
|
||||
pe2.department = "Management"
|
||||
pe2.department = "Management - _TC"
|
||||
pe2.save().submit()
|
||||
|
||||
je1 = self.create_journal_entry(
|
||||
@@ -1508,7 +1571,7 @@ class TestAccountsController(FrappeTestCase):
|
||||
)
|
||||
je1.accounts[0].party_type = "Customer"
|
||||
je1.accounts[0].party = self.customer
|
||||
je1.accounts[0].department = "Management"
|
||||
je1.accounts[0].department = "Management - _TC"
|
||||
je1.save().submit()
|
||||
|
||||
# assert dimension filter's result
|
||||
@@ -1517,17 +1580,17 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertEqual(len(pr.invoices), 2)
|
||||
self.assertEqual(len(pr.payments), 5)
|
||||
|
||||
pr.department = "Legal"
|
||||
pr.department = "Legal - _TC"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
pr.department = "Management"
|
||||
pr.department = "Management - _TC"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 3)
|
||||
|
||||
pr.department = "Research & Development"
|
||||
pr.department = "Research & Development - _TC"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
@@ -1538,17 +1601,17 @@ class TestAccountsController(FrappeTestCase):
|
||||
|
||||
# Invoice
|
||||
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||
si.department = "Management"
|
||||
si.department = "Management - _TC"
|
||||
si.save().submit()
|
||||
|
||||
# Payment
|
||||
cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||
cr_note.department = "Management"
|
||||
cr_note.department = "Management - _TC"
|
||||
cr_note.is_return = 1
|
||||
cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.department = "Management"
|
||||
pr.department = "Management - _TC"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
@@ -1580,7 +1643,7 @@ class TestAccountsController(FrappeTestCase):
|
||||
# Sales Invoice in Foreign Currency
|
||||
self.setup_dimensions()
|
||||
rate_in_account_currency = 1
|
||||
dpt = "Research & Development"
|
||||
dpt = "Research & Development - _TC"
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True)
|
||||
si.department = dpt
|
||||
@@ -1615,7 +1678,7 @@ class TestAccountsController(FrappeTestCase):
|
||||
|
||||
def test_93_dimension_inheritance_on_advance(self):
|
||||
self.setup_dimensions()
|
||||
dpt = "Research & Development"
|
||||
dpt = "Research & Development - _TC"
|
||||
|
||||
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
|
||||
adv.department = dpt
|
||||
@@ -2073,3 +2136,13 @@ class TestAccountsController(FrappeTestCase):
|
||||
journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent)
|
||||
purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name)
|
||||
self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date)
|
||||
|
||||
def test_company_validation_in_dimension(self):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
project = make_project({"project_name": "_Test Demo Project1", "company": "_Test Company 1"})
|
||||
si.project = project.name
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
si_1 = create_sales_invoice(do_not_submit=True)
|
||||
si_1.items[0].project = project.name
|
||||
self.assertRaises(frappe.ValidationError, si_1.save)
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Comments"
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -516,7 +516,7 @@
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2023-12-01 18:46:49.468526",
|
||||
"modified": "2025-01-31 13:40:08.094759",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
@@ -584,4 +584,4 @@
|
||||
"states": [],
|
||||
"subject_field": "title",
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ def create_custom_fields_for_frappe_crm():
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_prospect_against_crm_deal():
|
||||
frappe.only_for("System Manager")
|
||||
doc = frappe.form_dict
|
||||
prospect = frappe.get_doc(
|
||||
{
|
||||
@@ -152,7 +151,6 @@ def contact_exists(email, mobile_no):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_customer(customer_data=None):
|
||||
frappe.only_for("System Manager")
|
||||
if not customer_data:
|
||||
customer_data = frappe.form_dict
|
||||
|
||||
|
||||
@@ -55,12 +55,13 @@ def get_columns():
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "address", "label": _("Address"), "fieldtype": "Data", "width": 130},
|
||||
{"fieldname": "state", "label": _("State"), "fieldtype": "Data", "width": 100},
|
||||
{"fieldname": "pincode", "label": _("Postal Code"), "fieldtype": "Data", "width": 90},
|
||||
{"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130},
|
||||
{"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90},
|
||||
{"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"label": _("Country"),
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"options": "Country",
|
||||
"width": 100,
|
||||
@@ -93,8 +94,9 @@ def get_data(filters):
|
||||
lead.owner,
|
||||
lead.company,
|
||||
(Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"),
|
||||
address.state,
|
||||
address.pincode,
|
||||
address.city,
|
||||
address.state,
|
||||
address.country,
|
||||
)
|
||||
.where(lead.company == filters.company)
|
||||
|
||||
@@ -69,23 +69,7 @@ class PlaidConnector:
|
||||
else:
|
||||
return response["link_token"]
|
||||
|
||||
def auth(self):
|
||||
try:
|
||||
self.client.Auth.get(self.access_token)
|
||||
except ItemError as e:
|
||||
if e.code == "ITEM_LOGIN_REQUIRED":
|
||||
pass
|
||||
except APIError as e:
|
||||
if e.code == "PLANNED_MAINTENANCE":
|
||||
pass
|
||||
except requests.Timeout:
|
||||
pass
|
||||
except Exception as e:
|
||||
frappe.log_error("Plaid: Authentication error")
|
||||
frappe.throw(_(str(e)), title=_("Authentication Failed"))
|
||||
|
||||
def get_transactions(self, start_date, end_date, account_id=None):
|
||||
self.auth()
|
||||
kwargs = dict(access_token=self.access_token, start_date=start_date, end_date=end_date)
|
||||
if account_id:
|
||||
kwargs.update(dict(account_ids=[account_id]))
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("QuickBooks Migrator", {
|
||||
connect: function (frm) {
|
||||
// OAuth requires user intervention to provide application access permissionsto requested scope
|
||||
// Here we open a new window and redirect user to the authorization url.
|
||||
// After user grants us permission to access. We will set authorization details on this doc which will force refresh.
|
||||
window.open(frm.doc.authorization_url);
|
||||
},
|
||||
fetch_data: function (frm) {
|
||||
frm.call("migrate");
|
||||
},
|
||||
onload: function (frm) {
|
||||
frm.trigger("set_indicator");
|
||||
var domain = frappe.urllib.get_base_url();
|
||||
var redirect_url = `${domain}/api/method/erpnext.erpnext_integrations.doctype.quickbooks_migrator.quickbooks_migrator.callback`;
|
||||
if (frm.doc.redirect_url != redirect_url) {
|
||||
frm.set_value("redirect_url", redirect_url);
|
||||
}
|
||||
// Instead of changing percentage width and message of single progress bar
|
||||
// Show a different porgress bar for every action after some time remove the finished progress bar
|
||||
// Former approach causes the progress bar to dance back and forth.
|
||||
frm.trigger("set_indicator");
|
||||
frappe.realtime.on("quickbooks_progress_update", function (data) {
|
||||
frm.dashboard.show_progress(data.message, (data.count / data.total) * 100, data.message);
|
||||
if (data.count == data.total) {
|
||||
window.setTimeout(
|
||||
function (message) {
|
||||
frm.dashboard.hide_progress(message);
|
||||
},
|
||||
1500,
|
||||
data.messsage
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
frm.trigger("set_indicator");
|
||||
if (!frm.doc.access_token) {
|
||||
// Unset access_token signifies that we don't have enough information to connect to quickbooks api and fetch data
|
||||
if (frm.doc.authorization_url) {
|
||||
frm.add_custom_button(__("Connect to Quickbooks"), function () {
|
||||
frm.trigger("connect");
|
||||
});
|
||||
}
|
||||
}
|
||||
if (frm.doc.access_token) {
|
||||
// If we have access_token that means we also have refresh_token we don't need user intervention anymore
|
||||
// All we need now is a Company from erpnext
|
||||
frm.remove_custom_button(__("Connect to Quickbooks"));
|
||||
|
||||
frm.toggle_display("company_settings", 1);
|
||||
frm.set_df_property("company", "reqd", 1);
|
||||
if (frm.doc.company) {
|
||||
frm.add_custom_button(__("Fetch Data"), function () {
|
||||
frm.trigger("fetch_data");
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
set_indicator: function (frm) {
|
||||
var indicator_map = {
|
||||
"Connecting to QuickBooks": [__("Connecting to QuickBooks"), "orange"],
|
||||
"Connected to QuickBooks": [__("Connected to QuickBooks"), "green"],
|
||||
"In Progress": [__("In Progress"), "orange"],
|
||||
Complete: [__("Complete"), "green"],
|
||||
Failed: [__("Failed"), "red"],
|
||||
};
|
||||
if (frm.doc.status) {
|
||||
var indicator = indicator_map[frm.doc.status];
|
||||
var label = indicator[0];
|
||||
var color = indicator[1];
|
||||
frm.page.set_indicator(label, color);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,213 +0,0 @@
|
||||
{
|
||||
"beta": 1,
|
||||
"creation": "2018-07-10 14:48:16.757030",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"status",
|
||||
"application_settings",
|
||||
"client_id",
|
||||
"redirect_url",
|
||||
"token_endpoint",
|
||||
"application_column_break",
|
||||
"client_secret",
|
||||
"scope",
|
||||
"api_endpoint",
|
||||
"authorization_settings",
|
||||
"authorization_endpoint",
|
||||
"refresh_token",
|
||||
"code",
|
||||
"authorization_column_break",
|
||||
"authorization_url",
|
||||
"access_token",
|
||||
"quickbooks_company_id",
|
||||
"company_settings",
|
||||
"company",
|
||||
"default_shipping_account",
|
||||
"default_warehouse",
|
||||
"company_column_break",
|
||||
"default_cost_center",
|
||||
"undeposited_funds_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Status",
|
||||
"options": "Connecting to QuickBooks\nConnected to QuickBooks\nIn Progress\nComplete\nFailed"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.client_id && doc.client_secret && doc.redirect_url",
|
||||
"fieldname": "application_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Application Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Client ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "redirect_url",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Redirect URL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
|
||||
"fieldname": "token_endpoint",
|
||||
"fieldtype": "Data",
|
||||
"label": "Token Endpoint",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "application_column_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_secret",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Client Secret",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "com.intuit.quickbooks.accounting",
|
||||
"fieldname": "scope",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Scope",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "https://quickbooks.api.intuit.com/v3",
|
||||
"fieldname": "api_endpoint",
|
||||
"fieldtype": "Data",
|
||||
"label": "API Endpoint",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "authorization_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Authorization Settings"
|
||||
},
|
||||
{
|
||||
"default": "https://appcenter.intuit.com/connect/oauth2",
|
||||
"fieldname": "authorization_endpoint",
|
||||
"fieldtype": "Data",
|
||||
"label": "Authorization Endpoint",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "refresh_token",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Refresh Token"
|
||||
},
|
||||
{
|
||||
"fieldname": "code",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "authorization_column_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "authorization_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Authorization URL",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Access Token"
|
||||
},
|
||||
{
|
||||
"fieldname": "quickbooks_company_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Quickbooks Company ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Company Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_shipping_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Default Shipping Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Default Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_column_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_cost_center",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Default Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "undeposited_funds_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Undeposited Funds Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"modified": "2019-08-07 15:26:00.653433",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "QuickBooks Migrator",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestQuickBooksMigrator(unittest.TestCase):
|
||||
pass
|
||||
@@ -562,6 +562,8 @@ accounting_dimension_doctypes = [
|
||||
"Payment Reconciliation",
|
||||
"Payment Reconciliation Allocation",
|
||||
"Payment Request",
|
||||
"Asset Movement Item",
|
||||
"Asset Depreciation Schedule",
|
||||
]
|
||||
|
||||
get_matching_queries = (
|
||||
|
||||
@@ -1371,6 +1371,64 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
|
||||
},
|
||||
)
|
||||
|
||||
def get_max_op_qty():
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Job Card")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.total_completed_qty).as_("qty"))
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.work_order == work_order.name)
|
||||
& (table.is_corrective_job_card == 0)
|
||||
)
|
||||
.groupby(table.operation)
|
||||
)
|
||||
return min([d.qty for d in query.run(as_dict=True)], default=0)
|
||||
|
||||
def get_utilised_cc():
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
subquery = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.work_order == work_order.name)
|
||||
& (table.purpose == "Manufacture")
|
||||
)
|
||||
)
|
||||
table = frappe.qb.DocType("Landed Cost Taxes and Charges")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.amount).as_("amount"))
|
||||
.where(table.parent.isin(subquery) & (table.has_corrective_cost == 1))
|
||||
)
|
||||
return query.run(as_dict=True)[0].amount or 0
|
||||
|
||||
if (
|
||||
work_order
|
||||
and work_order.corrective_operation_cost
|
||||
and cint(
|
||||
frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation"
|
||||
)
|
||||
)
|
||||
):
|
||||
max_qty = get_max_op_qty() - work_order.produced_qty
|
||||
remaining_cc = work_order.corrective_operation_cost - get_utilised_cc()
|
||||
stock_entry.append(
|
||||
"additional_costs",
|
||||
{
|
||||
"expense_account": expense_account,
|
||||
"description": "Corrective Operation Cost",
|
||||
"has_corrective_cost": 1,
|
||||
"amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bom_diff(bom1, bom2):
|
||||
|
||||
@@ -650,7 +650,7 @@ class JobCard(Document):
|
||||
)
|
||||
)
|
||||
|
||||
if self.get("operation") == d.operation:
|
||||
if self.get("operation") == d.operation or self.is_corrective_job_card:
|
||||
self.append(
|
||||
"items",
|
||||
{
|
||||
@@ -791,7 +791,7 @@ class JobCard(Document):
|
||||
fields=["total_time_in_mins", "hour_rate"],
|
||||
filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order},
|
||||
):
|
||||
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
|
||||
wo.corrective_operation_cost += (flt(row.total_time_in_mins) / 60) * flt(row.hour_rate)
|
||||
|
||||
wo.calculate_operating_cost()
|
||||
wo.flags.ignore_validate_update_after_submit = True
|
||||
@@ -982,7 +982,9 @@ class JobCard(Document):
|
||||
if self.time_logs:
|
||||
self.status = "Work In Progress"
|
||||
|
||||
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
|
||||
if self.docstatus == 1 and (
|
||||
self.for_quantity <= (self.total_completed_qty + self.process_loss_qty) or not self.items
|
||||
):
|
||||
self.status = "Completed"
|
||||
|
||||
if update_status:
|
||||
|
||||
@@ -424,12 +424,99 @@ class TestJobCard(FrappeTestCase):
|
||||
cost_after_cancel = self.work_order.total_operating_cost
|
||||
self.assertEqual(cost_after_cancel, original_cost)
|
||||
|
||||
@change_settings(
|
||||
"Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1}
|
||||
)
|
||||
def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self):
|
||||
wo = make_wo_order_test_record(
|
||||
item="_Test FG Item 2",
|
||||
qty=10,
|
||||
transfer_material_against=self.transfer_material_against,
|
||||
source_warehouse=self.source_warehouse,
|
||||
)
|
||||
self.generate_required_stock(wo)
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name})
|
||||
job_card.update({"for_quantity": 4})
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 4},
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
corrective_action = frappe.get_doc(
|
||||
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
|
||||
).insert()
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name, operation=corrective_action.name, for_operation=job_card.operation
|
||||
)
|
||||
corrective_job_card.hour_rate = 100
|
||||
corrective_job_card.insert()
|
||||
corrective_job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": add_to_date(now(), hours=2),
|
||||
"to_time": add_to_date(now(), hours=2, minutes=30),
|
||||
"completed_qty": 4,
|
||||
},
|
||||
)
|
||||
corrective_job_card.submit()
|
||||
wo.reload()
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
|
||||
stock_entry = make_stock_entry_for_wo(wo.name, "Manufacture", qty=3)
|
||||
self.assertEqual(stock_entry.additional_costs[1].amount, 37.5)
|
||||
frappe.get_doc(stock_entry).submit()
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||
|
||||
make_job_card(
|
||||
wo.name,
|
||||
[{"name": wo.operations[0].name, "operation": "_Test Operation 1", "qty": 3, "pending_qty": 3}],
|
||||
)
|
||||
workstation = job_card.workstation
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name})
|
||||
job_card.update({"for_quantity": 3})
|
||||
job_card.workstation = workstation
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": add_to_date(now(), hours=3),
|
||||
"to_time": add_to_date(now(), hours=4),
|
||||
"completed_qty": 3,
|
||||
},
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name, operation=corrective_action.name, for_operation=job_card.operation
|
||||
)
|
||||
corrective_job_card.hour_rate = 80
|
||||
corrective_job_card.insert()
|
||||
corrective_job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": add_to_date(now(), hours=4),
|
||||
"to_time": add_to_date(now(), hours=4, minutes=30),
|
||||
"completed_qty": 3,
|
||||
},
|
||||
)
|
||||
corrective_job_card.submit()
|
||||
wo.reload()
|
||||
|
||||
stock_entry = make_stock_entry_for_wo(wo.name, "Manufacture", qty=4)
|
||||
self.assertEqual(stock_entry.additional_costs[1].amount, 52.5)
|
||||
|
||||
def test_job_card_statuses(self):
|
||||
def assertStatus(status):
|
||||
jc.set_status()
|
||||
self.assertEqual(jc.status, status)
|
||||
|
||||
jc = frappe.new_doc("Job Card")
|
||||
jc.process_loss_qty = 0
|
||||
jc.for_quantity = 2
|
||||
jc.transferred_qty = 1
|
||||
jc.total_completed_qty = 0
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"bom_section",
|
||||
"update_bom_costs_automatically",
|
||||
"column_break_lhyt",
|
||||
"manufacture_sub_assembly_in_operation",
|
||||
"section_break_6",
|
||||
"default_wip_warehouse",
|
||||
"default_fg_warehouse",
|
||||
@@ -223,13 +222,6 @@
|
||||
"fieldname": "column_break_lhyt",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled then system will manufacture Sub-assembly against the Job Card (operation).",
|
||||
"fieldname": "manufacture_sub_assembly_in_operation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manufacture Sub-assembly in Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
|
||||
@@ -249,7 +241,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-13 12:07:03.089977",
|
||||
"modified": "2025-02-05 16:11:11.639916",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
|
||||
@@ -29,7 +29,6 @@ class ManufacturingSettings(Document):
|
||||
get_rm_cost_from_consumption_entry: DF.Check
|
||||
job_card_excess_transfer: DF.Check
|
||||
make_serial_no_batch_from_work_order: DF.Check
|
||||
manufacture_sub_assembly_in_operation: DF.Check
|
||||
material_consumption: DF.Check
|
||||
mins_between_operations: DF.Int
|
||||
overproduction_percentage_for_sales_order: DF.Percent
|
||||
|
||||
@@ -562,6 +562,28 @@ frappe.ui.form.on("Production Plan Sales Order", {
|
||||
frappe.ui.form.on("Production Plan Sub Assembly Item", {
|
||||
fg_warehouse(frm, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "sub_assembly_items", "fg_warehouse");
|
||||
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.fg_warehouse && row.production_item) {
|
||||
let child_row = {
|
||||
item_code: row.production_item,
|
||||
warehouse: row.fg_warehouse,
|
||||
};
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_bin_details",
|
||||
args: {
|
||||
row: child_row,
|
||||
company: frm.doc.company,
|
||||
for_warehouse: row.fg_warehouse,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message && r.message.length) {
|
||||
frappe.model.set_value(cdt, cdn, "actual_qty", r.message[0].actual_qty);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -937,8 +937,14 @@ class ProductionPlan(Document):
|
||||
|
||||
bom_data = []
|
||||
|
||||
warehouse = (self.sub_assembly_warehouse) if self.skip_available_sub_assembly_item else None
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
|
||||
get_sub_assembly_items(
|
||||
row.bom_no,
|
||||
bom_data,
|
||||
row.planned_qty,
|
||||
self.company,
|
||||
warehouse=self.sub_assembly_warehouse,
|
||||
skip_available_sub_assembly_item=self.skip_available_sub_assembly_item,
|
||||
)
|
||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||
sub_assembly_items_store.extend(bom_data)
|
||||
|
||||
@@ -1729,14 +1735,23 @@ def get_item_data(item_code):
|
||||
}
|
||||
|
||||
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=None, indent=0):
|
||||
def get_sub_assembly_items(
|
||||
bom_no,
|
||||
bom_data,
|
||||
to_produce_qty,
|
||||
company,
|
||||
warehouse=None,
|
||||
indent=0,
|
||||
skip_available_sub_assembly_item=False,
|
||||
):
|
||||
data = get_bom_children(parent=bom_no)
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
|
||||
if warehouse:
|
||||
bin_details = frappe._dict()
|
||||
if skip_available_sub_assembly_item:
|
||||
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
|
||||
|
||||
for _bin_dict in bin_details:
|
||||
@@ -1746,11 +1761,14 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=
|
||||
continue
|
||||
else:
|
||||
stock_qty = stock_qty - _bin_dict.projected_qty
|
||||
elif warehouse:
|
||||
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
|
||||
|
||||
if stock_qty > 0:
|
||||
bom_data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"actual_qty": bin_details[0].get("actual_qty", 0) if bin_details else 0,
|
||||
"parent_item_code": parent_item_code,
|
||||
"description": d.description,
|
||||
"production_item": d.item_code,
|
||||
@@ -1768,7 +1786,13 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=
|
||||
|
||||
if d.value:
|
||||
get_sub_assembly_items(
|
||||
d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1
|
||||
d.value,
|
||||
bom_data,
|
||||
stock_qty,
|
||||
company,
|
||||
warehouse,
|
||||
indent=indent + 1,
|
||||
skip_available_sub_assembly_item=skip_available_sub_assembly_item,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2367,6 +2367,59 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
stock_entry.submit()
|
||||
|
||||
def test_components_alternate_item_for_bom_based_manufacture_entry(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
|
||||
|
||||
fg_item = "Test FG Item For Component Validation for alternate item"
|
||||
source_warehouse = "Stores - _TC"
|
||||
raw_materials = ["Test Component Validation RM Item 112", "Test Component Validation RM Item 22"]
|
||||
alternate_item = ["Alternate Test Component Validation RM Item 1"]
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
for item in raw_materials + alternate_item:
|
||||
make_item(item, {"is_stock_item": 1, "allow_alternative_item": 1})
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item,
|
||||
target=source_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Alternative",
|
||||
"item_code": raw_materials[0],
|
||||
"alternative_item_code": alternate_item[0],
|
||||
"two_way": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=10,
|
||||
source_warehouse=source_warehouse,
|
||||
)
|
||||
|
||||
transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
|
||||
transfer_entry.save()
|
||||
transfer_entry.items[0].item_code = alternate_item[0]
|
||||
transfer_entry.items[0].original_item = raw_materials[0]
|
||||
transfer_entry.submit()
|
||||
|
||||
self.assertTrue(transfer_entry.docstatus == 1)
|
||||
|
||||
manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
|
||||
manufacture_entry.save()
|
||||
self.assertTrue(manufacture_entry.items[0].item_code == alternate_item[0])
|
||||
self.assertTrue(manufacture_entry.items[0].original_item == raw_materials[0])
|
||||
|
||||
manufacture_entry.submit()
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
|
||||
|
||||
def test_components_qty_for_bom_based_manufacture_entry(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
|
||||
|
||||
@@ -397,7 +397,10 @@ frappe.ui.form.on("Work Order", {
|
||||
message = title;
|
||||
// pending qty
|
||||
if (!frm.doc.skip_transfer) {
|
||||
var pending_complete = frm.doc.material_transferred_for_manufacturing - frm.doc.produced_qty;
|
||||
var pending_complete =
|
||||
frm.doc.material_transferred_for_manufacturing -
|
||||
frm.doc.produced_qty -
|
||||
frm.doc.process_loss_qty;
|
||||
if (pending_complete) {
|
||||
var width = (pending_complete / frm.doc.qty) * 100 - added_min;
|
||||
title = __("{0} items in progress", [pending_complete]);
|
||||
@@ -409,6 +412,16 @@ frappe.ui.form.on("Work Order", {
|
||||
message = message + ". " + title;
|
||||
}
|
||||
}
|
||||
if (frm.doc.process_loss_qty) {
|
||||
var process_loss_width = (frm.doc.process_loss_qty / frm.doc.qty) * 100;
|
||||
title = __("{0} items lost during process.", [frm.doc.process_loss_qty]);
|
||||
bars.push({
|
||||
title: title,
|
||||
width: process_loss_width + "%",
|
||||
progress_class: "progress-bar-danger",
|
||||
});
|
||||
message = message + ". " + title;
|
||||
}
|
||||
frm.dashboard.add_progress(__("Status"), bars, message);
|
||||
},
|
||||
|
||||
|
||||
@@ -348,8 +348,9 @@ class WorkOrder(Document):
|
||||
if flt(self.material_transferred_for_manufacturing) > 0:
|
||||
status = "In Process"
|
||||
|
||||
total_qty = flt(flt(self.produced_qty) + flt(self.process_loss_qty), self.precision("qty"))
|
||||
if flt(total_qty) >= flt(self.qty):
|
||||
precision = frappe.get_precision("Work Order", "produced_qty")
|
||||
total_qty = flt(self.produced_qty, precision) + flt(self.process_loss_qty, precision)
|
||||
if flt(total_qty, precision) >= flt(self.qty, precision):
|
||||
status = "Completed"
|
||||
else:
|
||||
status = "Cancelled"
|
||||
@@ -1518,7 +1519,9 @@ def close_work_order(work_order, status):
|
||||
work_order = frappe.get_doc("Work Order", work_order)
|
||||
if work_order.get("operations"):
|
||||
job_cards = frappe.get_list(
|
||||
"Job Card", filters={"work_order": work_order.name, "status": "Work In Progress"}, pluck="name"
|
||||
"Job Card",
|
||||
filters={"work_order": work_order.name, "status": "Work In Progress", "docstatus": 1},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if job_cards:
|
||||
|
||||
@@ -390,3 +390,6 @@ erpnext.patches.v15_0.update_asset_status_to_work_in_progress
|
||||
erpnext.patches.v15_0.rename_manufacturing_settings_field
|
||||
erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect
|
||||
erpnext.patches.v15_0.sync_auto_reconcile_config
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
|
||||
|
||||
5
erpnext/patches/v14_0/disable_add_row_in_gross_profit.py
Normal file
5
erpnext/patches/v14_0/disable_add_row_in_gross_profit.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.set_value("Report", "Gross Profit", "add_total_row", 0)
|
||||
@@ -0,0 +1,10 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||
|
||||
frappe.qb.update(AssetValueAdjustment).set(
|
||||
AssetValueAdjustment.difference_amount,
|
||||
AssetValueAdjustment.new_asset_value - AssetValueAdjustment.current_asset_value,
|
||||
).where(AssetValueAdjustment.docstatus != 2).run()
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"project_name": "_Test Project",
|
||||
"status": "Open"
|
||||
"status": "Open",
|
||||
"company": "_Test Company"
|
||||
}
|
||||
]
|
||||
@@ -822,7 +822,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
}
|
||||
|
||||
set_total_amount_to_default_mop() {
|
||||
async set_total_amount_to_default_mop() {
|
||||
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
||||
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
|
||||
|
||||
@@ -844,6 +844,45 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
During returns, if an user select mode of payment other than
|
||||
default mode of payment, it should retain the user selection
|
||||
instead resetting it to default mode of payment.
|
||||
*/
|
||||
|
||||
let payment_amount = 0;
|
||||
this.frm.doc.payments.forEach(payment => {
|
||||
payment_amount += payment.amount
|
||||
});
|
||||
|
||||
if (payment_amount == total_amount_to_pay) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
For partial return, if the payment was made using single mode of payment
|
||||
it should set the return to that mode of payment only.
|
||||
*/
|
||||
|
||||
let return_against_mop = await frappe.call({
|
||||
method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data',
|
||||
args: {
|
||||
invoice: this.frm.doc.return_against
|
||||
}
|
||||
});
|
||||
|
||||
if (return_against_mop.message.length === 1) {
|
||||
this.frm.doc.payments.forEach(payment => {
|
||||
if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) {
|
||||
payment.amount = total_amount_to_pay;
|
||||
} else {
|
||||
payment.amount = 0;
|
||||
}
|
||||
});
|
||||
this.frm.refresh_fields();
|
||||
return;
|
||||
}
|
||||
|
||||
this.frm.doc.payments.find(payment => {
|
||||
if (payment.default) {
|
||||
payment.amount = total_amount_to_pay;
|
||||
|
||||
@@ -1658,7 +1658,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc && r.message) {
|
||||
me.remove_pricing_rule(r.message, removed_pricing_rule);
|
||||
me.remove_pricing_rule(r.message, removed_pricing_rule, item.name);
|
||||
me.calculate_taxes_and_totals();
|
||||
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
|
||||
}
|
||||
@@ -1751,7 +1751,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"serial_no": d.serial_no,
|
||||
"batch_no": d.batch_no,
|
||||
"price_list_rate": d.price_list_rate,
|
||||
"conversion_factor": d.conversion_factor || 1.0
|
||||
"conversion_factor": d.conversion_factor || 1.0,
|
||||
"discount_percentage" : d.discount_percentage,
|
||||
"discount_amount" : d.discount_amount,
|
||||
});
|
||||
|
||||
// if doctype is Quotation Item / Sales Order Iten then add Margin Type and rate in item_list
|
||||
@@ -1935,7 +1937,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
});
|
||||
}
|
||||
|
||||
remove_pricing_rule(item, removed_pricing_rule) {
|
||||
remove_pricing_rule(item, removed_pricing_rule, row_name) {
|
||||
let me = this;
|
||||
const fields = ["discount_percentage",
|
||||
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
|
||||
@@ -1974,6 +1976,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
me.trigger_price_list_rate();
|
||||
}
|
||||
else if(!item.is_free_item && row_name){
|
||||
me.frm.doc.items.forEach(d => {
|
||||
if (d.name != row_name) return;
|
||||
|
||||
Object.assign(d, item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
trigger_price_list_rate() {
|
||||
@@ -2358,29 +2367,39 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
primary_action_label: __("Create")
|
||||
});
|
||||
|
||||
this.frm.doc.items.forEach(item => {
|
||||
if (this.has_inspection_required(item)) {
|
||||
let dialog_items = dialog.fields_dict.items;
|
||||
dialog_items.df.data.push({
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"description": item.description,
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no,
|
||||
"sample_size": item.sample_quantity,
|
||||
"child_row_reference": item.name,
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.stock_controller.check_item_quality_inspection",
|
||||
args: {
|
||||
doctype: this.frm.doc.doctype,
|
||||
items: this.frm.doc.items
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
r.message.forEach(item => {
|
||||
if (me.has_inspection_required(item)) {
|
||||
let dialog_items = dialog.fields_dict.items;
|
||||
dialog_items.df.data.push({
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"description": item.description,
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no,
|
||||
"sample_size": item.sample_quantity,
|
||||
"child_row_reference": item.name,
|
||||
});
|
||||
dialog_items.grid.refresh();
|
||||
}
|
||||
});
|
||||
dialog_items.grid.refresh();
|
||||
|
||||
data = dialog.fields_dict.items.df.data;
|
||||
if (!data.length) {
|
||||
frappe.msgprint(__("All items in this document already have a linked Quality Inspection."));
|
||||
} else {
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
data = dialog.fields_dict.items.df.data;
|
||||
if (!data.length) {
|
||||
frappe.msgprint(__("All items in this document already have a linked Quality Inspection."));
|
||||
} else {
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
has_inspection_required(item) {
|
||||
|
||||
@@ -28,9 +28,13 @@ $.extend(erpnext.queries, {
|
||||
|
||||
customer_filter: function (doc) {
|
||||
if (!doc.customer) {
|
||||
frappe.throw(
|
||||
__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "customer", doc.name))])
|
||||
);
|
||||
cur_frm.scroll_to_field("customer");
|
||||
frappe.show_alert({
|
||||
message: __("Please set {0} first.", [
|
||||
__(frappe.meta.get_label(doc.doctype, "customer", doc.name)),
|
||||
]),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
|
||||
return { filters: { customer: doc.customer } };
|
||||
@@ -39,11 +43,13 @@ $.extend(erpnext.queries, {
|
||||
contact_query: function (doc) {
|
||||
if (frappe.dynamic_link) {
|
||||
if (!doc[frappe.dynamic_link.fieldname]) {
|
||||
frappe.throw(
|
||||
__("Please set {0}", [
|
||||
cur_frm.scroll_to_field(frappe.dynamic_link.fieldname);
|
||||
frappe.show_alert({
|
||||
message: __("Please set {0} first.", [
|
||||
__(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name)),
|
||||
])
|
||||
);
|
||||
]),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -70,11 +76,13 @@ $.extend(erpnext.queries, {
|
||||
address_query: function (doc) {
|
||||
if (frappe.dynamic_link) {
|
||||
if (!doc[frappe.dynamic_link.fieldname]) {
|
||||
frappe.throw(
|
||||
__("Please set {0}", [
|
||||
cur_frm.scroll_to_field(frappe.dynamic_link.fieldname);
|
||||
frappe.show_alert({
|
||||
message: __("Please set {0} first.", [
|
||||
__(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name)),
|
||||
])
|
||||
);
|
||||
]),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -89,7 +97,13 @@ $.extend(erpnext.queries, {
|
||||
|
||||
company_address_query: function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))]));
|
||||
cur_frm.scroll_to_field("company");
|
||||
frappe.show_alert({
|
||||
message: __("Please set {0} first.", [
|
||||
__(frappe.meta.get_label(doc.doctype, "company", doc.name)),
|
||||
]),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -110,9 +124,13 @@ $.extend(erpnext.queries, {
|
||||
|
||||
supplier_filter: function (doc) {
|
||||
if (!doc.supplier) {
|
||||
frappe.throw(
|
||||
__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "supplier", doc.name))])
|
||||
);
|
||||
cur_frm.scroll_to_field("supplier");
|
||||
frappe.show_alert({
|
||||
message: __("Please set {0} first.", [
|
||||
__(frappe.meta.get_label(doc.doctype, "supplier", doc.name)),
|
||||
]),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
|
||||
return { filters: { supplier: doc.supplier } };
|
||||
@@ -120,9 +138,13 @@ $.extend(erpnext.queries, {
|
||||
|
||||
lead_filter: function (doc) {
|
||||
if (!doc.lead) {
|
||||
frappe.throw(
|
||||
__("Please specify a {0}", [__(frappe.meta.get_label(doc.doctype, "lead", doc.name))])
|
||||
);
|
||||
cur_frm.scroll_to_field("lead");
|
||||
frappe.show_alert({
|
||||
message: __("Please specify a {0} first.", [
|
||||
__(frappe.meta.get_label(doc.doctype, "lead", doc.name)),
|
||||
]),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
|
||||
return { filters: { lead: doc.lead } };
|
||||
|
||||
@@ -654,6 +654,62 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
filters: filters,
|
||||
};
|
||||
},
|
||||
onchange: function () {
|
||||
const me = this;
|
||||
|
||||
frm.call({
|
||||
method: "erpnext.stock.get_item_details.get_item_details",
|
||||
args: {
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
item_code: this.value,
|
||||
set_warehouse: frm.doc.set_warehouse,
|
||||
customer: frm.doc.customer || frm.doc.party_name,
|
||||
quotation_to: frm.doc.quotation_to,
|
||||
supplier: frm.doc.supplier,
|
||||
currency: frm.doc.currency,
|
||||
is_internal_supplier: frm.doc.is_internal_supplier,
|
||||
is_internal_customer: frm.doc.is_internal_customer,
|
||||
conversion_rate: frm.doc.conversion_rate,
|
||||
price_list: frm.doc.selling_price_list || frm.doc.buying_price_list,
|
||||
price_list_currency: frm.doc.price_list_currency,
|
||||
plc_conversion_rate: frm.doc.plc_conversion_rate,
|
||||
company: frm.doc.company,
|
||||
order_type: frm.doc.order_type,
|
||||
is_pos: cint(frm.doc.is_pos),
|
||||
is_return: cint(frm.doc.is_return),
|
||||
is_subcontracted: frm.doc.is_subcontracted,
|
||||
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
|
||||
doctype: frm.doc.doctype,
|
||||
name: frm.doc.name,
|
||||
qty: me.doc.qty || 1,
|
||||
uom: me.doc.uom,
|
||||
pos_profile: cint(frm.doc.is_pos) ? frm.doc.pos_profile : "",
|
||||
tax_category: frm.doc.tax_category,
|
||||
child_doctype: frm.doc.doctype + " Item",
|
||||
is_old_subcontracting_flow: frm.doc.is_old_subcontracting_flow,
|
||||
},
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
const { qty, price_list_rate: rate, uom, conversion_factor } = r.message;
|
||||
|
||||
const row = dialog.fields_dict.trans_items.df.data.find(
|
||||
(doc) => doc.idx == me.doc.idx
|
||||
);
|
||||
if (row) {
|
||||
Object.assign(row, {
|
||||
conversion_factor: me.doc.conversion_factor || conversion_factor,
|
||||
uom: me.doc.uom || uom,
|
||||
qty: me.doc.qty || qty,
|
||||
rate: me.doc.rate || rate,
|
||||
});
|
||||
dialog.fields_dict.trans_items.grid.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
gap: var(--margin-md);
|
||||
padding: 1%;
|
||||
|
||||
section {
|
||||
min-height: 45rem;
|
||||
|
||||
@@ -12,13 +12,17 @@ frappe.listview_settings["Quotation"] = {
|
||||
};
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Sales Order"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order");
|
||||
});
|
||||
if (frappe.model.can_create("Sales Order")) {
|
||||
listview.page.add_action_item(__("Sales Order"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Sales Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice");
|
||||
});
|
||||
if (frappe.model.can_create("Sales Invoice")) {
|
||||
listview.page.add_action_item(__("Sales Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
get_indicator: function (doc) {
|
||||
|
||||
@@ -872,7 +872,11 @@ def make_material_request(source_name, target_doc=None):
|
||||
},
|
||||
"Sales Order Item": {
|
||||
"doctype": "Material Request Item",
|
||||
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
|
||||
"field_map": {
|
||||
"name": "sales_order_item",
|
||||
"parent": "sales_order",
|
||||
"delivery_date": "required_by",
|
||||
},
|
||||
"condition": lambda item: not frappe.db.exists(
|
||||
"Product Bundle", {"name": item.item_code, "disabled": 0}
|
||||
)
|
||||
@@ -1357,7 +1361,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.ordered_qty < doc.stock_qty
|
||||
and doc.supplier == supplier
|
||||
and doc.item_code in items_to_map,
|
||||
and doc.item_code in items_to_map
|
||||
and doc.delivered_by_supplier == 1,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
@@ -1501,6 +1506,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
)
|
||||
|
||||
set_delivery_date(doc.items, source_name)
|
||||
doc.set_onload("load_after_mapping", False)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@@ -60,16 +60,22 @@ frappe.listview_settings["Sales Order"] = {
|
||||
listview.call_for_selected_items(method, { status: "Submitted" });
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Sales Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice");
|
||||
});
|
||||
if (frappe.model.can_create("Sales Invoice")) {
|
||||
listview.page.add_action_item(__("Sales Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note");
|
||||
});
|
||||
if (frappe.model.can_create("Delivery Note")) {
|
||||
listview.page.add_action_item(__("Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Advance Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry");
|
||||
});
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
listview.page.add_action_item(__("Advance Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -155,7 +155,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.page.set_title_sub(
|
||||
`<span class="indicator orange">
|
||||
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}">
|
||||
Opened at ${moment(this.pos_opening_time).format("Do MMMM, h:mma")}
|
||||
Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)}
|
||||
</a>
|
||||
</span>`
|
||||
);
|
||||
@@ -165,6 +165,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.prepare_dom();
|
||||
this.prepare_components();
|
||||
this.prepare_menu();
|
||||
this.prepare_fullscreen_btn();
|
||||
this.make_new_invoice();
|
||||
}
|
||||
|
||||
@@ -200,6 +201,39 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.page.add_menu_item(__("Close the POS"), this.close_pos.bind(this), false, "Shift+Ctrl+C");
|
||||
}
|
||||
|
||||
prepare_fullscreen_btn() {
|
||||
this.page.page_actions.find(".custom-actions").empty();
|
||||
|
||||
this.page.add_button(__("Full Screen"), null, { btn_class: "btn-default fullscreen-btn" });
|
||||
|
||||
this.bind_fullscreen_events();
|
||||
}
|
||||
|
||||
bind_fullscreen_events() {
|
||||
this.$fullscreen_btn = this.page.page_actions.find(".fullscreen-btn");
|
||||
|
||||
this.$fullscreen_btn.on("click", function () {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("fullscreenchange", this.handle_fullscreen_change_event.bind(this));
|
||||
}
|
||||
|
||||
handle_fullscreen_change_event() {
|
||||
let enable_fullscreen_label = __("Full Screen");
|
||||
let exit_fullscreen_label = __("Exit Full Screen");
|
||||
|
||||
if (document.fullscreenElement) {
|
||||
this.$fullscreen_btn[0].innerText = exit_fullscreen_label;
|
||||
} else {
|
||||
this.$fullscreen_btn[0].innerText = enable_fullscreen_label;
|
||||
}
|
||||
}
|
||||
|
||||
open_form_view() {
|
||||
frappe.model.sync(this.frm.doc);
|
||||
frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name);
|
||||
@@ -451,6 +485,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
]);
|
||||
},
|
||||
},
|
||||
pos_profile: this.pos_profile,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -647,7 +682,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
i.item_code === item_code &&
|
||||
(!has_batch_no || (has_batch_no && i.batch_no === batch_no)) &&
|
||||
i.uom === uom &&
|
||||
i.rate === flt(rate)
|
||||
i.price_list_rate === flt(rate)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -988,8 +988,8 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
.html(`${__("Last transacted")} ${__(elapsed_time)}`);
|
||||
|
||||
res.forEach((invoice) => {
|
||||
const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format(
|
||||
"Do MMMM, h:mma"
|
||||
const posting_datetime = frappe.datetime.str_to_user(
|
||||
invoice.posting_date + " " + invoice.posting_time
|
||||
);
|
||||
let indicator_color = {
|
||||
Paid: "green",
|
||||
|
||||
@@ -96,8 +96,8 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
}
|
||||
|
||||
get_invoice_html(invoice) {
|
||||
const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format(
|
||||
"Do MMMM, h:mma"
|
||||
const posting_datetime = frappe.datetime.str_to_user(
|
||||
invoice.posting_date + " " + invoice.posting_time
|
||||
);
|
||||
return `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
|
||||
<div class="invoice-name-date">
|
||||
@@ -110,7 +110,7 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-total-status">
|
||||
<div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency, 0) || 0}</div>
|
||||
<div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency) || 0}</div>
|
||||
<div class="invoice-date">${posting_datetime}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
erpnext.PointOfSale.PastOrderSummary = class {
|
||||
constructor({ wrapper, events }) {
|
||||
constructor({ wrapper, events, pos_profile }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
this.pos_profile = pos_profile;
|
||||
|
||||
this.init_component();
|
||||
}
|
||||
@@ -355,6 +356,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
const condition_btns_map = this.get_condition_btn_map(after_submission);
|
||||
|
||||
this.add_summary_btns(condition_btns_map);
|
||||
|
||||
if (after_submission) {
|
||||
this.print_receipt_on_order_complete();
|
||||
}
|
||||
}
|
||||
|
||||
attach_document_info(doc) {
|
||||
@@ -421,4 +426,16 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
toggle_component(show) {
|
||||
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
|
||||
}
|
||||
|
||||
async print_receipt_on_order_complete() {
|
||||
const res = await frappe.db.get_value(
|
||||
"POS Profile",
|
||||
this.pos_profile,
|
||||
"print_receipt_on_order_complete"
|
||||
);
|
||||
|
||||
if (res.message.print_receipt_on_order_complete) {
|
||||
this.print_receipt();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -462,7 +462,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
this.$payment_modes.find(".cash-shortcuts").remove();
|
||||
let shortcuts_html = shortcuts
|
||||
.map((s) => {
|
||||
return `<div class="shortcut" data-value="${s}">${format_currency(s, currency, 0)}</div>`;
|
||||
return `<div class="shortcut" data-value="${s}">${format_currency(s, currency)}</div>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ def get_columns(filters):
|
||||
f"{frappe.unscrub(str(party_type_value))}::150",
|
||||
"Address Line 1",
|
||||
"Address Line 2",
|
||||
"Postal Code",
|
||||
"City",
|
||||
"State",
|
||||
"Postal Code",
|
||||
"Country",
|
||||
"Is Primary Address:Check",
|
||||
"First Name",
|
||||
|
||||
@@ -871,5 +871,6 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "employee_name"
|
||||
}
|
||||
"title_field": "employee_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ class Employee(NestedSet):
|
||||
self.validate_email()
|
||||
self.validate_status()
|
||||
self.validate_reports_to()
|
||||
self.set_preferred_email()
|
||||
self.validate_preferred_email()
|
||||
|
||||
if self.user_id:
|
||||
@@ -160,9 +161,7 @@ class Employee(NestedSet):
|
||||
|
||||
def set_preferred_email(self):
|
||||
preferred_email_field = frappe.scrub(self.prefered_contact_email)
|
||||
if preferred_email_field:
|
||||
preferred_email = self.get(preferred_email_field)
|
||||
self.prefered_email = preferred_email
|
||||
self.prefered_email = self.get(preferred_email_field) if preferred_email_field else None
|
||||
|
||||
def validate_status(self):
|
||||
if self.status == "Left":
|
||||
|
||||
@@ -169,7 +169,7 @@ class DeprecatedBatchNoValuation:
|
||||
if not self.non_batchwise_balance_qty:
|
||||
continue
|
||||
|
||||
if self.non_batchwise_balance_qty.get(batch_no) == 0:
|
||||
if not self.non_batchwise_balance_qty.get(batch_no):
|
||||
self.batch_avg_rate[batch_no] = 0.0
|
||||
self.stock_value_differece[batch_no] = 0.0
|
||||
else:
|
||||
@@ -245,8 +245,9 @@ class DeprecatedBatchNoValuation:
|
||||
|
||||
last_sle = self.get_last_sle_for_non_batch()
|
||||
for d in batch_data:
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
|
||||
if self.available_qty.get(d.batch_no):
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
|
||||
|
||||
def get_last_sle_for_non_batch(self):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
@@ -292,6 +293,58 @@ class DeprecatedBatchNoValuation:
|
||||
data = query.run(as_dict=True)
|
||||
return data[0] if data else {}
|
||||
|
||||
@deprecated
|
||||
def get_last_sle_for_sabb_no_batchwise_valuation(self):
|
||||
sabb = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
sabb_entry = frappe.qb.DocType("Serial and Batch Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
posting_datetime = CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
timestamp_condition = CombineDatetime(sabb.posting_date, sabb.posting_time) < posting_datetime
|
||||
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (
|
||||
CombineDatetime(sabb.posting_date, sabb.posting_time) == posting_datetime
|
||||
) & (sabb.creation < self.sle.creation)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sabb)
|
||||
.inner_join(sabb_entry)
|
||||
.on(sabb.name == sabb_entry.parent)
|
||||
.inner_join(batch)
|
||||
.on(sabb_entry.batch_no == batch.name)
|
||||
.select(sabb.name)
|
||||
.where(
|
||||
(sabb.item_code == self.sle.item_code)
|
||||
& (sabb.warehouse == self.sle.warehouse)
|
||||
& (sabb_entry.batch_no.isnotnull())
|
||||
& (batch.use_batchwise_valuation == 0)
|
||||
& (sabb.is_cancelled == 0)
|
||||
& (sabb.docstatus == 1)
|
||||
)
|
||||
.where(timestamp_condition)
|
||||
.orderby(sabb.posting_date, order=Order.desc)
|
||||
.orderby(sabb.posting_time, order=Order.desc)
|
||||
.orderby(sabb.creation, order=Order.desc)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if self.sle.voucher_detail_no:
|
||||
query = query.where(sabb.voucher_detail_no != self.sle.voucher_detail_no)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"serial_and_batch_bundle": data[0].name},
|
||||
["stock_value", "qty_after_transaction"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return sle if sle else {}
|
||||
|
||||
@deprecated
|
||||
def set_balance_value_from_bundle(self) -> None:
|
||||
bundle = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
@@ -338,7 +391,14 @@ class DeprecatedBatchNoValuation:
|
||||
|
||||
query = query.where(bundle.voucher_type != "Pick List")
|
||||
|
||||
for d in query.run(as_dict=True):
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
|
||||
batch_data = query.run(as_dict=True)
|
||||
for d in batch_data:
|
||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||
|
||||
last_sle = self.get_last_sle_for_sabb_no_batchwise_valuation()
|
||||
if not last_sle:
|
||||
return
|
||||
|
||||
for batch_no in self.available_qty:
|
||||
self.non_batchwise_balance_value[batch_no] = flt(last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[batch_no] = flt(last_sle.qty_after_transaction)
|
||||
|
||||
@@ -54,7 +54,12 @@ frappe.ui.form.on("Batch", {
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
|
||||
args: { batch_no: frm.doc.name, item_code: frm.doc.item, for_stock_levels: for_stock_levels },
|
||||
args: {
|
||||
batch_no: frm.doc.name,
|
||||
item_code: frm.doc.item,
|
||||
for_stock_levels: for_stock_levels,
|
||||
consider_negative_batches: 1,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.message) {
|
||||
return;
|
||||
@@ -71,7 +76,7 @@ frappe.ui.form.on("Batch", {
|
||||
|
||||
// show
|
||||
(r.message || []).forEach(function (d) {
|
||||
if (d.qty > 0) {
|
||||
if (d.qty != 0) {
|
||||
$(`<div class='row' style='margin-bottom: 10px;'>
|
||||
<div class='col-sm-3 small' style='padding-top: 3px;'>${d.warehouse}</div>
|
||||
<div class='col-sm-3 small text-right' style='padding-top: 3px;'>${d.qty}</div>
|
||||
|
||||
@@ -218,6 +218,7 @@ def get_batch_qty(
|
||||
posting_time=None,
|
||||
ignore_voucher_nos=None,
|
||||
for_stock_levels=False,
|
||||
consider_negative_batches=False,
|
||||
):
|
||||
"""Returns batch actual qty if warehouse is passed,
|
||||
or returns dict of qty by warehouse if warehouse is None
|
||||
@@ -243,6 +244,7 @@ def get_batch_qty(
|
||||
"batch_no": batch_no,
|
||||
"ignore_voucher_nos": ignore_voucher_nos,
|
||||
"for_stock_levels": for_stock_levels,
|
||||
"consider_negative_batches": consider_negative_batches,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -453,10 +455,14 @@ def get_available_batches(kwargs):
|
||||
|
||||
batches = get_auto_batch_nos(kwargs)
|
||||
for batch in batches:
|
||||
if batch.get("batch_no") not in batchwise_qty:
|
||||
batchwise_qty[batch.get("batch_no")] = batch.get("qty")
|
||||
key = batch.get("batch_no")
|
||||
if kwargs.get("based_on_warehouse"):
|
||||
key = (batch.get("batch_no"), batch.get("warehouse"))
|
||||
|
||||
if key not in batchwise_qty:
|
||||
batchwise_qty[key] = batch.get("qty")
|
||||
else:
|
||||
batchwise_qty[batch.get("batch_no")] += batch.get("qty")
|
||||
batchwise_qty[key] += batch.get("qty")
|
||||
|
||||
return batchwise_qty
|
||||
|
||||
|
||||
@@ -116,7 +116,6 @@ class ClosingStockBalance(Document):
|
||||
"item_group": self.item_group,
|
||||
"warehouse_type": self.warehouse_type,
|
||||
"include_uom": self.include_uom,
|
||||
"ignore_closing_balance": 1,
|
||||
"show_variant_attributes": 1,
|
||||
"show_stock_ageing_data": 1,
|
||||
}
|
||||
|
||||
@@ -1132,7 +1132,7 @@ def make_packing_slip(source_name, target_doc=None):
|
||||
"batch_no": "batch_no",
|
||||
"description": "description",
|
||||
"qty": "qty",
|
||||
"stock_uom": "stock_uom",
|
||||
"uom": "stock_uom",
|
||||
"name": "dn_detail",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
|
||||
@@ -63,16 +63,20 @@ frappe.listview_settings["Delivery Note"] = {
|
||||
}
|
||||
};
|
||||
|
||||
// doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
|
||||
if (frappe.model.can_create("Delivery Trip")) {
|
||||
doclist.page.add_action_item(__("Create Delivery Trip"), action);
|
||||
}
|
||||
|
||||
doclist.page.add_action_item(__("Create Delivery Trip"), action);
|
||||
if (frappe.model.can_create("Sales Invoice")) {
|
||||
doclist.page.add_action_item(__("Sales Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice");
|
||||
});
|
||||
}
|
||||
|
||||
doclist.page.add_action_item(__("Sales Invoice"), () => {
|
||||
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice");
|
||||
});
|
||||
|
||||
doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip");
|
||||
});
|
||||
if (frappe.model.can_create("Packing Slip")) {
|
||||
doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user