mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-05 10:43:04 +00:00
Compare commits
173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9e3dee5b2 | ||
|
|
a8cd49112d | ||
|
|
8baef24541 | ||
|
|
20d481de5e | ||
|
|
7568af67e9 | ||
|
|
5dbdcb1158 | ||
|
|
473aaf4e5b | ||
|
|
5812577854 | ||
|
|
ebb8d90b4d | ||
|
|
11ebbf2a9c | ||
|
|
c9cde259ec | ||
|
|
b15ec238c9 | ||
|
|
09c39face8 | ||
|
|
c3cc363648 | ||
|
|
e55fd7204a | ||
|
|
b7cbafae14 | ||
|
|
706a6c1ad7 | ||
|
|
28e8bb7085 | ||
|
|
8b3ffc9949 | ||
|
|
b0aef9e42b | ||
|
|
d61dab8569 | ||
|
|
a45f8ca5fd | ||
|
|
d9e62fef21 | ||
|
|
3b4d39766f | ||
|
|
61c0ce6ca8 | ||
|
|
bff99d89b9 | ||
|
|
b63eab8cbb | ||
|
|
7fcb0f578a | ||
|
|
317cc0358c | ||
|
|
58e18e2b1f | ||
|
|
97c49b93b6 | ||
|
|
c3f5a494f3 | ||
|
|
a0e06a4ba5 | ||
|
|
deaeb103d5 | ||
|
|
3c58e0af50 | ||
|
|
cb703ff17c | ||
|
|
6a0111c7db | ||
|
|
e00348fd52 | ||
|
|
4e74257ba9 | ||
|
|
fb76daaf9e | ||
|
|
0df706f14d | ||
|
|
d310074222 | ||
|
|
760b2e24f2 | ||
|
|
1f3374fcdf | ||
|
|
c47dcc0eb7 | ||
|
|
b738b0fe9f | ||
|
|
d396c18689 | ||
|
|
460acc3e22 | ||
|
|
8a91bf3154 | ||
|
|
b7eabc3112 | ||
|
|
482832f3c2 | ||
|
|
3536a754ff | ||
|
|
b29435744f | ||
|
|
cf4d4ba3e9 | ||
|
|
c27f272f06 | ||
|
|
63b26e679b | ||
|
|
f2feeaf264 | ||
|
|
a981633d94 | ||
|
|
e278fc683f | ||
|
|
068de08bbb | ||
|
|
f6be19cb7c | ||
|
|
838cc5b72a | ||
|
|
68b318a94b | ||
|
|
6a0ab23c87 | ||
|
|
068ae87b8d | ||
|
|
fe9dffb271 | ||
|
|
521cfb3d4e | ||
|
|
bc6cbb2656 | ||
|
|
f52f726e06 | ||
|
|
f3aa885488 | ||
|
|
c45ce75f57 | ||
|
|
6dbe820416 | ||
|
|
3b15708f18 | ||
|
|
a1ebd16284 | ||
|
|
d1679d4663 | ||
|
|
2bd10d388f | ||
|
|
545d0b9730 | ||
|
|
4bde345399 | ||
|
|
78ad3f6cdc | ||
|
|
88e1c28e7d | ||
|
|
259d7cde39 | ||
|
|
6ebd4ba2cc | ||
|
|
47071cec5d | ||
|
|
5d2f296ca8 | ||
|
|
199a64937b | ||
|
|
a535933a09 | ||
|
|
b59c91a341 | ||
|
|
e99ef572a2 | ||
|
|
ea9095c824 | ||
|
|
6f3a6ca2ca | ||
|
|
44c16713ba | ||
|
|
8d299d1495 | ||
|
|
f9574366b5 | ||
|
|
7b5d5043c5 | ||
|
|
ca343f12d8 | ||
|
|
9515b96049 | ||
|
|
9945a90b3f | ||
|
|
a41577a1cd | ||
|
|
657daf9a43 | ||
|
|
77f4199e2a | ||
|
|
7b322e7437 | ||
|
|
21802396ce | ||
|
|
9bad219f0a | ||
|
|
ddbf9317a1 | ||
|
|
48dc24b9bf | ||
|
|
c10b123a81 | ||
|
|
fa6a479ed9 | ||
|
|
a60879fec0 | ||
|
|
cd1db37c74 | ||
|
|
e3608f285f | ||
|
|
c110a9f2ad | ||
|
|
24ee765e6a | ||
|
|
5663432bd8 | ||
|
|
d64432c3f4 | ||
|
|
7cc5579eca | ||
|
|
60b16a4b1e | ||
|
|
ff8027b8eb | ||
|
|
794ade832f | ||
|
|
75edb7708f | ||
|
|
f4852d472a | ||
|
|
db7fb7d7b1 | ||
|
|
ae0725d55a | ||
|
|
ceafa0ce28 | ||
|
|
a43c181673 | ||
|
|
ae83236b7c | ||
|
|
98c8feded0 | ||
|
|
5a2005395b | ||
|
|
ecb889707d | ||
|
|
e3539ac2de | ||
|
|
c159e3b06f | ||
|
|
7e9f6aee58 | ||
|
|
084658e8ff | ||
|
|
21596c6c23 | ||
|
|
858058c9e1 | ||
|
|
aaee02835b | ||
|
|
125d97a182 | ||
|
|
4551b4b638 | ||
|
|
9771bbdb75 | ||
|
|
b64b4330a3 | ||
|
|
b577d95ecd | ||
|
|
248e9cac71 | ||
|
|
4c11f2bff2 | ||
|
|
8618402829 | ||
|
|
c7c9d33954 | ||
|
|
83ccb32be6 | ||
|
|
69de359728 | ||
|
|
5ea2cdcaad | ||
|
|
ac2e760d81 | ||
|
|
164973a42c | ||
|
|
9cb7c279a4 | ||
|
|
9ccfbe1bf5 | ||
|
|
5004d95f59 | ||
|
|
2990b4ec85 | ||
|
|
3bc5076dfc | ||
|
|
b34191e81f | ||
|
|
59be55fd8a | ||
|
|
a89b4f49fe | ||
|
|
2f5533c676 | ||
|
|
04f0c96af7 | ||
|
|
00b7c49b4b | ||
|
|
6e9ad4ddd2 | ||
|
|
6c57971abc | ||
|
|
2c9b3908dd | ||
|
|
c04ba3864e | ||
|
|
da15df1f28 | ||
|
|
94ef2eabb1 | ||
|
|
337082512e | ||
|
|
764235b390 | ||
|
|
7193633773 | ||
|
|
ef2a6c788e | ||
|
|
67d1709ef8 | ||
|
|
cef6d0d74d | ||
|
|
a0011c5b52 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.27.4"
|
||||
__version__ = "15.29.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Rate at which this tax is applied",
|
||||
@@ -190,7 +191,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-20 18:18:44.405723",
|
||||
"modified": "2024-06-27 16:23:04.444354",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -251,4 +252,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,531 +0,0 @@
|
||||
{
|
||||
"country_code": "tr",
|
||||
"name": "Turkey - Tek D\u00fczen Hesap Plan\u0131",
|
||||
"tree": {
|
||||
"Duran Varl\u0131klar": {
|
||||
"Di\u011fer Alacaklar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klardan Alacaklar": {},
|
||||
"Di\u011fer Alacak Senetleri Reeskontu(-)": {},
|
||||
"Di\u011fer \u00c7e\u015fitli Alacaklar": {},
|
||||
"Ortaklardan Alacaklar": {},
|
||||
"Personelden Alacaklar": {},
|
||||
"\u0130\u015ftiraklerden Alacaklar": {},
|
||||
"\u015e\u00fcpheli Di\u011fer Alacaklar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {}
|
||||
},
|
||||
"Di\u011fer Duran Varl\u0131klar": {
|
||||
"Birikmi\u015f Amortismanlar(-)": {},
|
||||
"Di\u011fer KDV": {},
|
||||
"Di\u011fer \u00c7e\u015fitli Duran Varl\u0131klar": {},
|
||||
"Elden \u00c7\u0131kar\u0131lacak Stoklar Ve Maddi Duran Varl\u0131klar": {},
|
||||
"Gelecek Y\u0131llar \u0130htiyac\u0131 Stoklar": {},
|
||||
"Gelecek Y\u0131llarda \u0130ndirilecek KDV": {},
|
||||
"Pe\u015fin \u00d6denen Vergi Ve Fonlar": {},
|
||||
"Stok De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {}
|
||||
},
|
||||
"Gelecek Y\u0131llara Ait Giderler ve Gelir Tahakkuklar\u0131": {
|
||||
"Gelecek Y\u0131llara Ait Giderler": {},
|
||||
"Gelir Tahakkuklar\u0131": {}
|
||||
},
|
||||
"Maddi Duran Varl\u0131klar": {
|
||||
"Arazi Ve Arsalar": {},
|
||||
"Binalar": {},
|
||||
"Birikmi\u015f Amortismanlar(-)": {},
|
||||
"Demirba\u015flar": {},
|
||||
"Di\u011fer Maddi Duran Varl\u0131klar": {},
|
||||
"Ta\u015f\u0131tlar": {},
|
||||
"Tesis, Makine Ve Cihazlar": {},
|
||||
"Verilen Avanslar": {},
|
||||
"Yap\u0131lmakta Olan Yat\u0131r\u0131mlar": {},
|
||||
"Yer Alt\u0131 Ve Yer \u00dcst\u00fc D\u00fczenleri": {}
|
||||
},
|
||||
"Maddi Olmayan Duran Varl\u0131klar": {
|
||||
"Ara\u015ft\u0131rma Ve Geli\u015ftirme Giderleri": {},
|
||||
"Birikmi\u015f Amortismanlar(-)": {},
|
||||
"Di\u011fer Maddi Olmayan Duran Varl\u0131klar": {},
|
||||
"Haklar": {},
|
||||
"Kurulu\u015f Ve \u00d6rg\u00fctlenme Giderleri": {},
|
||||
"Verilen Avanslar": {},
|
||||
"\u00d6zel Maliyetler": {},
|
||||
"\u015eerefiye": {}
|
||||
},
|
||||
"Mali Duran Varl\u0131klar": {
|
||||
"Ba\u011fl\u0131 Menkul K\u0131ymetler": {},
|
||||
"Ba\u011fl\u0131 Menkul K\u0131ymetler De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klar": {},
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klar Sermaye Paylar\u0131 De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klara Sermaye Taahh\u00fctleri(-)": {},
|
||||
"Di\u011fer Mali Duran Varl\u0131klar": {},
|
||||
"Di\u011fer Mali Duran Varl\u0131klar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"\u0130\u015ftirakler": {},
|
||||
"\u0130\u015ftirakler Sermaye Paylar\u0131 De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"\u0130\u015ftiraklere Sermaye Taahh\u00fctleri(-)": {}
|
||||
},
|
||||
"Ticari Alacaklar": {
|
||||
"Alacak Senetleri": {},
|
||||
"Alacak Senetleri Reeskontu(-)": {},
|
||||
"Al\u0131c\u0131lar": {},
|
||||
"Kazaqn\u0131lmam\u0131\u015f Finansal Kiralama Faiz Gelirleri(-)": {},
|
||||
"Verilen Depozito Ve Teminatlar": {},
|
||||
"\u015e\u00fcpheli Ticari Alacaklar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {}
|
||||
},
|
||||
"root_type": "",
|
||||
"\u00d6zel T\u00fckenmeye Tabi Varl\u0131klar": {
|
||||
"Arama Giderleri": {},
|
||||
"Birikmi\u015f T\u00fckenme Paylar\u0131(-)": {},
|
||||
"Di\u011fer \u00d6zel T\u00fckenmeye Tabi Varl\u0131klar": {},
|
||||
"Haz\u0131rl\u0131k Ve Geli\u015ftirme Giderleri": {},
|
||||
"Verilen Avanslar": {}
|
||||
}
|
||||
},
|
||||
"D\u00f6nen Varl\u0131klar": {
|
||||
"Di\u011fer Alacaklar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klardan Alacaklar": {},
|
||||
"Di\u011fer Alacak Senetleri Reeskontu(-)": {},
|
||||
"Di\u011fer \u00c7e\u015fitli Alacaklar": {},
|
||||
"Ortaklardan Alacaklar": {},
|
||||
"Personelden Alacaklar": {},
|
||||
"\u0130\u015ftiraklerden Alacaklar": {},
|
||||
"\u015e\u00fcpheli Di\u011fer Alacaklar": {},
|
||||
"\u015e\u00fcpheli Di\u011fer Alacaklar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {}
|
||||
},
|
||||
"Di\u011fer D\u00f6nen Varl\u0131klar": {
|
||||
"Devreden KDV": {},
|
||||
"Di\u011fer D\u00f6nen Varl\u0131klar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"Di\u011fer KDV": {},
|
||||
"Di\u011fer \u00c7e\u015fitli D\u00f6nen Varl\u0131klar": {},
|
||||
"Personel Avanslar\u0131": {},
|
||||
"Pe\u015fin \u00d6denen Vergiler Ve Fonlar": {},
|
||||
"Say\u0131m Ve Tesell\u00fcm Noksanlar\u0131": {},
|
||||
"\u0130ndirilecek KDV": {},
|
||||
"\u0130\u015f Avanslar\u0131": {}
|
||||
},
|
||||
"Gelecek Aylara Ait Giderler ve Gelir Tahakkuklar\u0131": {
|
||||
"Gelecek Aylara Ait Giderler": {},
|
||||
"Gelir Tahakkuklar\u0131": {}
|
||||
},
|
||||
"Haz\u0131r De\u011ferler": {
|
||||
"Al\u0131nan \u00c7ekler": {},
|
||||
"Bankalar": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Di\u011fer Haz\u0131r De\u011ferler": {},
|
||||
"Kasa": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Verilen \u00c7ekler ve \u00d6deme Emirleri(-)": {}
|
||||
},
|
||||
"Menkul K\u0131ymetler": {
|
||||
"Di\u011fer Menkul K\u0131ymetler": {},
|
||||
"Hisse Senetleri": {},
|
||||
"Kamu Kesimi Tahvil, Senet ve Bonolar\u0131": {},
|
||||
"Menkul K\u0131ymetler De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"\u00d6zel Kesim Tahvil Senet Ve Bonolar\u0131": {}
|
||||
},
|
||||
"Stoklar": {
|
||||
"Mamuller": {},
|
||||
"Stok De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"Ticari Mallar": {},
|
||||
"Verilen Sipari\u015f Avanslar\u0131": {},
|
||||
"Yar\u0131 Mamuller": {},
|
||||
"\u0130lk Madde Malzeme": {}
|
||||
},
|
||||
"Ticari Alacaklar": {
|
||||
"Alacak Senetleri": {},
|
||||
"Alacak Senetleri Reeskontu(-)": {},
|
||||
"Al\u0131c\u0131lar": {},
|
||||
"Di\u011fer Ticari Alacaklar": {},
|
||||
"Kazan\u0131lmam\u0131\u015f Finansal Kiralama Faiz Gelirleri(-)": {},
|
||||
"Verilen Depozito ve Teminatlar": {},
|
||||
"\u015e\u00fcpheli Ticari Alacaklar": {},
|
||||
"\u015e\u00fcpheli Ticari Alacaklar Kar\u015f\u0131l\u0131\u011f\u0131": {}
|
||||
},
|
||||
"Y\u0131llara Yayg\u0131n \u0130n\u015faat ve Onar\u0131m Maliyetleri": {
|
||||
"Ta\u015feronlara Verilen Avanslar": {},
|
||||
"Y\u0131llara Yayg\u0131n \u0130n\u015faat Ve Onar\u0131m Maliyetleri": {}
|
||||
},
|
||||
"root_type": ""
|
||||
},
|
||||
"Gelir Tablosu Hesaplar\u0131": {
|
||||
"Br\u00fct Sat\u0131\u015flar": {
|
||||
"Di\u011fer Gelirler": {},
|
||||
"Yurt D\u0131\u015f\u0131 Sat\u0131\u015flar": {},
|
||||
"Yurt \u0130\u00e7i Sat\u0131\u015flar": {}
|
||||
},
|
||||
"Di\u011fer Faaliyetlerden Olu\u015fan Gelir ve K\u00e2rlar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klardan Temett\u00fc Gelirleri": {},
|
||||
"Di\u011fer Ola\u011fan Gelir Ve K\u00e2rlar": {},
|
||||
"Enflasyon D\u00fczeltme K\u00e2rlar\u0131": {},
|
||||
"Faiz Gelirleri": {},
|
||||
"Kambiyo K\u00e2rlar\u0131": {},
|
||||
"Komisyon Gelirleri": {},
|
||||
"Konusu Kalmayan Kar\u015f\u0131l\u0131klar": {},
|
||||
"Menkul K\u0131ymet Sat\u0131\u015f K\u00e2rlar\u0131": {},
|
||||
"Reeskont Faiz Gelirleri": {},
|
||||
"\u0130\u015ftiraklerden Temett\u00fc Gelirleri": {}
|
||||
},
|
||||
"Di\u011fer Faaliyetlerden Olu\u015fan Gider ve Zararlar (-)": {
|
||||
"Di\u011fer Ola\u011fan Gider Ve Zararlar(-)": {},
|
||||
"Enflasyon D\u00fczeltmesi Zararlar\u0131(-)": {},
|
||||
"Kambiyo Zararlar\u0131(-)": {},
|
||||
"Kar\u015f\u0131l\u0131k Giderleri(-)": {},
|
||||
"Komisyon Giderleri(-)": {},
|
||||
"Menkul K\u0131ymet Sat\u0131\u015f Zararlar\u0131(-)": {},
|
||||
"Reeskont Faiz Giderleri(-)": {}
|
||||
},
|
||||
"D\u00f6nem Net K\u00e2r\u0131 Ve Zarar\u0131": {
|
||||
"D\u00f6nem K\u00e2r\u0131 Vergi Ve Di\u011fer Yasal Y\u00fck\u00fcml\u00fcl\u00fck Kar\u015f\u0131l\u0131klar\u0131(-)": {},
|
||||
"D\u00f6nem K\u00e2r\u0131 Veya Zarar\u0131": {},
|
||||
"D\u00f6nem Net K\u00e2r\u0131 Veya Zarar\u0131": {},
|
||||
"Enflasyon D\u00fczeltme Hesab\u0131": {},
|
||||
"Y\u0131llara Yayg\u0131n \u0130n\u015faat Ve Enflasyon D\u00fczeltme Hesab\u0131": {}
|
||||
},
|
||||
"Faaliyet Giderleri(-)": {
|
||||
"Ara\u015ft\u0131rma Ve Geli\u015ftirme Giderleri(-)": {},
|
||||
"Genel Y\u00f6netim Giderleri(-)": {},
|
||||
"Pazarlama Sat\u0131\u015f Ve Da\u011f\u0131t\u0131m Giderleri(-)": {}
|
||||
},
|
||||
"Finansman Giderleri": {
|
||||
"K\u0131sa Vadeli Bor\u00e7lanma Giderleri(-)": {},
|
||||
"Uzun Vadeli Bor\u00e7lanma Giderleri(-)": {}
|
||||
},
|
||||
"Ola\u011fan D\u0131\u015f\u0131 Gelir Ve K\u00e2rlar": {
|
||||
"Di\u011fer Ola\u011fan D\u0131\u015f\u0131 Gelir Ve K\u00e2rlar": {},
|
||||
"\u00d6nceki D\u00f6nem Gelir Ve K\u00e2rlar\u0131": {}
|
||||
},
|
||||
"Ola\u011fan D\u0131\u015f\u0131 Gider Ve Zaralar(-)": {
|
||||
"Di\u011fer Ola\u011fan D\u0131\u015f\u0131 Gider Ve Zararlar(-)": {},
|
||||
"\u00c7al\u0131\u015fmayan K\u0131s\u0131m Gider Ve Zararlar\u0131(-)": {},
|
||||
"\u00d6nceki D\u00f6nem Gider Ve Zararlar\u0131(-)": {}
|
||||
},
|
||||
"Sat\u0131\u015f \u0130ndirimleri (-)": {
|
||||
"Di\u011fer \u0130ndirimler": {},
|
||||
"Sat\u0131\u015f \u0130ndirimleri(-)": {},
|
||||
"Sat\u0131\u015ftan \u0130adeler(-)": {}
|
||||
},
|
||||
"Sat\u0131\u015flar\u0131n Maliyeti(-)": {
|
||||
"Di\u011fer Sat\u0131\u015flar\u0131n Maliyeti(-)": {},
|
||||
"Sat\u0131lan Hizmet Maliyeti(-)": {},
|
||||
"Sat\u0131lan Mamuller Maliyeti(-)": {},
|
||||
"Sat\u0131lan Ticari Mallar Maliyeti(-)": {}
|
||||
},
|
||||
"root_type": ""
|
||||
},
|
||||
"K\u0131sa Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"Al\u0131nan Avanslar": {
|
||||
"Al\u0131nan Di\u011fer Avanslar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Al\u0131nan Sipari\u015f Avanslar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 ve Gider Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"Di\u011fer Bor\u00e7 Ve Gider Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"D\u00f6nem K\u00e2r\u0131 Vergi Ve Di\u011fer Yasal Y\u00fck\u00fcml\u00fcl\u00fck Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"D\u00f6nem K\u00e2r\u0131n\u0131n Pe\u015fin \u00d6denen Vergi Ve Di\u011fer Y\u00fck\u00fcml\u00fcl\u00fckler(-)": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"K\u0131dem Tazminat\u0131 Kar\u015f\u0131l\u0131\u011f\u0131": {},
|
||||
"Maliyet Giderleri Kar\u015f\u0131l\u0131\u011f\u0131": {},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Bor\u00e7lar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klara Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Bor\u00e7 Senetleri Reeskontu(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer \u00c7e\u015fitli Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Ortaklara Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Personele Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable",
|
||||
"\u0130\u015ftiraklere Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Di\u011fer K\u0131sa Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"Di\u011fer KDV": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Di\u011fer \u00c7e\u015fitli Yabanc\u0131 Kaynaklar": {},
|
||||
"Hesaplanan KDV": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Merkez Ve \u015eubeler Cari Hesab\u0131": {},
|
||||
"Say\u0131m Ve Tesell\u00fcm Fazlalar\u0131": {},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Gelecek Aylara Ait Gelirler Ve Gider Tahakkuklar\u0131": {
|
||||
"Gelecek Aylara Ait Gelirler": {},
|
||||
"Gider Tahakkuklar\u0131": {}
|
||||
},
|
||||
"Mali Bor\u00e7lar": {
|
||||
"Banka Kredileri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Mali Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Ertelenmi\u015f Finansal Kiralama Bor\u00e7lanma Maliyetleri(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Finansal Kiralama \u0130\u015flemlerinden Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Menkul K\u0131ymetler \u0130hra\u00e7 Fark\u0131(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Tahvil Anapara Bor\u00e7, Taksit Ve Faizleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Uzun Vadeli Kredilerin Anapara Taksitleri Ve Faizleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable",
|
||||
"\u00c7\u0131kar\u0131lan Bonolar Ve Senetler": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"\u00c7\u0131kar\u0131lm\u0131\u015f Di\u011fer Menkul K\u0131ymetler": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Ticari Bor\u00e7lar": {
|
||||
"Al\u0131nan Depozito Ve Teminatlar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Senetleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Senetleri Reeskontu(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Ticari Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Sat\u0131c\u0131lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Y\u0131llara Yayg\u0131n \u0130n\u015faat Ve Onar\u0131m Hakedi\u015fleri": {
|
||||
"350 Y\u0131llara Yayg\u0131n \u0130n\u015faat Ve Onar\u0131m Hakedi\u015fleri Bedelleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"root_type": "",
|
||||
"\u00d6denecek Vergi ve Di\u011fer Y\u00fck\u00fcml\u00fcl\u00fckler": {
|
||||
"Vadesi Ge\u00e7mi\u015f, Ertelenmi\u015f Veya Taksitlendirilmi\u015f Vergi Ve Di\u011fer Y\u00fck\u00fcml\u00fcl\u00fckler": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_type": "Tax",
|
||||
"\u00d6denecek Di\u011fer Y\u00fck\u00fcml\u00fcl\u00fckler": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"\u00d6denecek Sosyal G\u00fcvenl\u00fck Kesintileri": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"\u00d6denecek Vergi Ve Fonlar": {
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Maliyet Hesaplar\u0131": {
|
||||
"Ara\u015ft\u0131rma Ve Geli\u015ftirme Giderleri": {},
|
||||
"Direkt \u0130lk Madde Ve Malzeme Giderleri": {
|
||||
"Direk \u0130lk Madde Ve Malzeme Giderleri Hesab\u0131": {},
|
||||
"Direkt \u0130lk Madde Ve Malzeme Fiyat Fark\u0131": {},
|
||||
"Direkt \u0130lk Madde Ve Malzeme Miktar Fark\u0131": {},
|
||||
"Direkt \u0130lk Madde Ve Malzeme Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Direkt \u0130\u015f\u00e7ilik Giderleri": {
|
||||
"Direkt \u0130\u015f\u00e7ilik Giderleri": {},
|
||||
"Direkt \u0130\u015f\u00e7ilik Giderleri Yans\u0131tma Hesab\u0131": {},
|
||||
"Direkt \u0130\u015f\u00e7ilik S\u00fcre Farklar\u0131": {},
|
||||
"Direkt \u0130\u015f\u00e7ilik \u00dccret Farklar\u0131": {}
|
||||
},
|
||||
"Finansman Giderleri": {
|
||||
"Finansman Giderleri": {},
|
||||
"Finansman Giderleri Fark Hesab\u0131": {},
|
||||
"Finansman Giderleri Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Genel Y\u00f6netim Giderleri": {
|
||||
"Genel Y\u00f6netim Gider Farklar\u0131 Hesab\u0131": {},
|
||||
"Genel Y\u00f6netim Giderleri": {},
|
||||
"Genel Y\u00f6netim Giderleri Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Genel \u00dcretim Giderleri": {
|
||||
"Genel \u00dcretim Giderleri": {},
|
||||
"Genel \u00dcretim Giderleri B\u00fct\u00e7e Farklar\u0131": {},
|
||||
"Genel \u00dcretim Giderleri Kapasite Farklar\u0131": {},
|
||||
"Genel \u00dcretim Giderleri Verimlilik Giderleri": {},
|
||||
"Genel \u00dcretim Giderleri Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Hizmet \u00dcretim Maliyeti": {
|
||||
"Hizmet \u00dcretim Maliyeti": {},
|
||||
"Hizmet \u00dcretim Maliyeti Fark Hesaplar\u0131": {},
|
||||
"Hizmet \u00dcretim Maliyeti Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Maliyet Muhasebesi Ba\u011flant\u0131 Hesaplar\u0131": {
|
||||
"Maliyet Muhasebesi Ba\u011flant\u0131 Hesab\u0131": {},
|
||||
"Maliyet Muhasebesi Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Pazarlama, Sat\u0131\u015f Ve Da\u011f\u0131t\u0131m Giderleri": {
|
||||
"Atra\u015ft\u0131rma Ve Geli\u015ftirme Giderleri": {},
|
||||
"Pazarlama Sat\u0131\u015f Ve Dag\u0131t\u0131m Giderleri Yans\u0131tma Hesab\u0131": {},
|
||||
"Pazarlama Sat\u0131\u015f Ve Da\u011f\u0131t\u0131m Giderleri Fark Hesab\u0131": {}
|
||||
},
|
||||
"root_type": ""
|
||||
},
|
||||
"Naz\u0131m Hesaplar": {
|
||||
"root_type": ""
|
||||
},
|
||||
"Serbest Hesaplar": {
|
||||
"root_type": ""
|
||||
},
|
||||
"Uzun Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"Al\u0131nan Avanslar": {
|
||||
"Al\u0131nan Di\u011fer Avanslar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Al\u0131nan Sipari\u015f Avanslar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Ve Gider Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"Di\u011fer Bor\u00e7 Ve Gider Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"K\u0131dem Tazminat\u0131 Kar\u015f\u0131l\u0131\u011f\u0131": {},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Bor\u00e7lar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klara Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Bor\u00e7 Senetleri Reeskontu(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer \u00c7e\u015fitli Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Kamuya Olan Ertelenmi\u015f Veya Taksitlendirilmi\u015f Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Ortaklara Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable",
|
||||
"\u0130\u015ftiraklere Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Di\u011fer Uzun Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"Di\u011fer \u00c7e\u015fitli Uzun Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Gelecek Y\u0131llara Ertelenmi\u015f Veya Terkin Edilecek KDV": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Tesise Kat\u0131lma Paylar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Gelecek Y\u0131llara Ait Gelirler Ve Gider Tahakkuklar\u0131": {
|
||||
"Gelecek Y\u0131llara Ait Gelirler": {},
|
||||
"Gider Tahakkuklar\u0131": {}
|
||||
},
|
||||
"Mali Bor\u00e7lar": {
|
||||
"Banka Kredileri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Mali Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Ertelenmi\u015f Finansal Kiralama Bor\u00e7lanma Maliyetleri(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Finansal Kiralama \u0130\u015flemlerinden Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Menkul K\u0131ymetler \u0130hra\u00e7 Fark\u0131(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable",
|
||||
"\u00c7\u0131kar\u0131lm\u0131\u015f Di\u011fer Menkul K\u0131ymetler": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"\u00c7\u0131kar\u0131lm\u0131\u015f Tahviller": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Ticari Bor\u00e7lar": {
|
||||
"Al\u0131nan Depozito Ve Teminatlar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Senetleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Senetleri Reeskontu(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Ticari Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Sat\u0131c\u0131lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"root_type": ""
|
||||
},
|
||||
"\u00d6z Kaynaklar": {
|
||||
"D\u00f6nem Net K\u00e2r\u0131 (Zarar\u0131)": {
|
||||
"D\u00f6nem Net K\u00e2r\u0131": {},
|
||||
"D\u00f6nem Net Zarar\u0131(-)": {}
|
||||
},
|
||||
"Ge\u00e7mi\u015f Y\u0131llar K\u00e2rlar\u0131": {
|
||||
"Ge\u00e7mi\u015f Y\u0131llar K\u00e2rlar\u0131": {}
|
||||
},
|
||||
"Ge\u00e7mi\u015f Y\u0131llar Zararlar\u0131(-)": {
|
||||
"Ge\u00e7mi\u015f Y\u0131llar Zararlar\u0131(-)": {}
|
||||
},
|
||||
"K\u00e2r Yedekleri": {
|
||||
"Di\u011fer K\u00e2r Yedekleri": {},
|
||||
"Ola\u011fan\u00fcst\u00fc Yedekler": {},
|
||||
"Stat\u00fc Yedekleri": {},
|
||||
"Yasal Yedekler": {},
|
||||
"\u00d6zel Fonlar": {}
|
||||
},
|
||||
"Sermaye Yedekleri": {
|
||||
"Di\u011fer Sermaye Yedekleri": {},
|
||||
"Hisse Senedi \u0130ptal K\u00e2rlar\u0131": {},
|
||||
"Hisse Senetleri \u0130hra\u00e7 Primleri": {},
|
||||
"Maddi Duran Varl\u0131k Yeniden De\u011ferlenme Art\u0131\u015flar\u0131": {},
|
||||
"Maliyet Art\u0131\u015flar\u0131 Fonu": {},
|
||||
"\u0130\u015ftirakler Yeniden De\u011ferleme Art\u0131\u015flar\u0131": {}
|
||||
},
|
||||
"root_type": "",
|
||||
"\u00d6denmi\u015f Sermaye": {
|
||||
"Sermaye": {},
|
||||
"\u00d6denmi\u015f Sermaye(-)": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -255,14 +255,16 @@ def get_accounting_dimensions(as_list=True, filters=None):
|
||||
|
||||
|
||||
def get_checks_for_pl_and_bs_accounts():
|
||||
dimensions = frappe.db.sql(
|
||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
||||
WHERE p.name = c.parent""",
|
||||
as_dict=1,
|
||||
)
|
||||
if frappe.flags.accounting_dimensions_details is None:
|
||||
# nosemgrep
|
||||
frappe.flags.accounting_dimensions_details = frappe.db.sql(
|
||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
||||
WHERE p.name = c.parent""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return dimensions
|
||||
return frappe.flags.accounting_dimensions_details
|
||||
|
||||
|
||||
def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
@@ -78,6 +78,8 @@ class TestAccountingDimension(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
disable_dimension()
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
frappe.flags.dimension_filter_map = None
|
||||
|
||||
|
||||
def create_dimension():
|
||||
|
||||
@@ -66,37 +66,41 @@ class AccountingDimensionFilter(Document):
|
||||
|
||||
|
||||
def get_dimension_filter_map():
|
||||
filters = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
|
||||
WHERE
|
||||
p.name = a.parent
|
||||
AND p.disabled = 0
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
dimension_filter_map = {}
|
||||
|
||||
for f in filters:
|
||||
f.fieldname = scrub(f.accounting_dimension)
|
||||
|
||||
build_map(
|
||||
dimension_filter_map,
|
||||
f.fieldname,
|
||||
f.applicable_on_account,
|
||||
f.dimension_value,
|
||||
f.allow_or_restrict,
|
||||
f.is_mandatory,
|
||||
if not frappe.flags.get("dimension_filter_map"):
|
||||
# nosemgrep
|
||||
filters = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
||||
`tabAccounting Dimension Filter` p
|
||||
WHERE
|
||||
p.name = a.parent
|
||||
AND p.disabled = 0
|
||||
AND p.name = d.parent
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return dimension_filter_map
|
||||
dimension_filter_map = {}
|
||||
|
||||
for f in filters:
|
||||
f.fieldname = scrub(f.accounting_dimension)
|
||||
|
||||
build_map(
|
||||
dimension_filter_map,
|
||||
f.fieldname,
|
||||
f.applicable_on_account,
|
||||
f.dimension_value,
|
||||
f.allow_or_restrict,
|
||||
f.is_mandatory,
|
||||
)
|
||||
|
||||
frappe.flags.dimension_filter_map = dimension_filter_map
|
||||
|
||||
return frappe.flags.dimension_filter_map
|
||||
|
||||
|
||||
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
|
||||
|
||||
@@ -47,6 +47,8 @@ class TestAccountingDimensionFilter(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
disable_dimension_filter()
|
||||
disable_dimension()
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
frappe.flags.dimension_filter_map = None
|
||||
|
||||
for si in self.invoice_list:
|
||||
si.load_from_db()
|
||||
|
||||
@@ -142,6 +142,8 @@ class Budget(Document):
|
||||
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
if not frappe.get_all("Budget", limit=1):
|
||||
return
|
||||
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
@@ -149,6 +151,9 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec
|
||||
return
|
||||
|
||||
if not args.account:
|
||||
args.account = args.get("expense_account")
|
||||
|
||||
@@ -175,12 +180,12 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
if (
|
||||
args.get(budget_against)
|
||||
and args.account
|
||||
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
|
||||
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
|
||||
):
|
||||
doctype = dimension.get("document_type")
|
||||
|
||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
condition = f"""and exists(select name from `tab{doctype}`
|
||||
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
||||
args.is_tree = True
|
||||
|
||||
@@ -1,94 +1,42 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2016-05-16 11:54:09.286135",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2016-05-16 11:54:09.286135",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account",
|
||||
"budget_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "budget_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Budget Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company:company:default_currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "budget_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Budget Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-01-02 17:02:53.339420",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Account",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-04 15:43:27.016947",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -179,7 +179,8 @@
|
||||
"fieldname": "voucher_detail_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher Detail No",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
@@ -290,7 +291,7 @@
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-18 15:38:14.006208",
|
||||
"modified": "2024-07-02 14:31:51.496466",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
@@ -322,7 +323,7 @@
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "modified",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,6 @@ class GLEntry(Document):
|
||||
account: DF.Link | None
|
||||
account_currency: DF.Link | None
|
||||
against: DF.Text | None
|
||||
against_link: DF.DynamicLink | None
|
||||
against_type: DF.Link | None
|
||||
against_voucher: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
company: DF.Link | None
|
||||
@@ -328,7 +326,7 @@ def update_outstanding_amt(
|
||||
party_condition = ""
|
||||
|
||||
if against_voucher_type == "Sales Invoice":
|
||||
party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to")
|
||||
party_account = frappe.get_cached_value(against_voucher_type, against_voucher, "debit_to")
|
||||
account_condition = f"and account in ({frappe.db.escape(account)}, {frappe.db.escape(party_account)})"
|
||||
else:
|
||||
account_condition = f" and account = {frappe.db.escape(account)}"
|
||||
@@ -392,7 +390,9 @@ def update_outstanding_amt(
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", None, "frozen_accounts_modifier")
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if not frozen_accounts_modifier:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
|
||||
@@ -194,6 +194,7 @@ class JournalEntry(AccountsController):
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
@@ -225,6 +226,7 @@ class JournalEntry(AccountsController):
|
||||
self.unlink_inter_company_jv()
|
||||
self.unlink_asset_adjustment_entry()
|
||||
self.update_invoice_discounting()
|
||||
self.update_booked_depreciation(1)
|
||||
|
||||
def get_title(self):
|
||||
return self.pay_to_recd_from or self.accounts[0].account
|
||||
@@ -439,6 +441,25 @@ class JournalEntry(AccountsController):
|
||||
if status:
|
||||
inv_disc_doc.set_status(status=status)
|
||||
|
||||
def update_booked_depreciation(self, cancel=0):
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
if cancel:
|
||||
fb_row.total_number_of_booked_depreciations -= 1
|
||||
else:
|
||||
fb_row.total_number_of_booked_depreciations += 1
|
||||
fb_row.db_update()
|
||||
break
|
||||
|
||||
def unlink_advance_entry_reference(self):
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
|
||||
@@ -76,7 +76,6 @@ class PaymentEntry(AccountsController):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.set_liability_account()
|
||||
self.validate_advance_account_currency()
|
||||
self.set_missing_ref_details(force=True)
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
@@ -163,22 +162,6 @@ class PaymentEntry(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
|
||||
def validate_advance_account_currency(self):
|
||||
if self.book_advance_payments_in_separate_party_account is True:
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if self.payment_type == "Receive" and self.paid_from_account_currency != company_currency:
|
||||
frappe.throw(
|
||||
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||
frappe.bold(self.paid_from), frappe.bold(self.paid_from_account_currency)
|
||||
)
|
||||
)
|
||||
if self.payment_type == "Pay" and self.paid_to_account_currency != company_currency:
|
||||
frappe.throw(
|
||||
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||
frappe.bold(self.paid_to), frappe.bold(self.paid_to_account_currency)
|
||||
)
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
@@ -1234,13 +1217,21 @@ class PaymentEntry(AccountsController):
|
||||
if reference.reference_doctype == "Sales Invoice":
|
||||
return "credit", reference.account
|
||||
|
||||
if reference.reference_doctype == "Purchase Invoice":
|
||||
return "debit", reference.account
|
||||
|
||||
if reference.reference_doctype == "Payment Entry":
|
||||
# reference.account_type and reference.payment_type is only available for Reverse payments
|
||||
if reference.account_type == "Receivable" and reference.payment_type == "Pay":
|
||||
return "credit", self.party_account
|
||||
else:
|
||||
return "debit", self.party_account
|
||||
|
||||
return "debit", reference.account
|
||||
if reference.reference_doctype == "Journal Entry":
|
||||
if self.party_type == "Customer" and self.payment_type == "Receive":
|
||||
return "credit", reference.account
|
||||
else:
|
||||
return "debit", reference.account
|
||||
|
||||
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
||||
args_dict = {
|
||||
@@ -1266,7 +1257,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
args_dict["account"] = account
|
||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict.update(
|
||||
{
|
||||
@@ -1285,7 +1276,7 @@ class PaymentEntry(AccountsController):
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = 0
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
args_dict["account"] = self.party_account
|
||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict.update(
|
||||
{
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]]
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
@@ -161,11 +161,12 @@ class PaymentLedgerEntry(Document):
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost:
|
||||
self.validate_account_details()
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
self.validate_allowed_dimensions()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
if not self.delinked:
|
||||
self.validate_account_details()
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
self.validate_allowed_dimensions()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
|
||||
# update outstanding amount
|
||||
if (
|
||||
|
||||
@@ -509,7 +509,11 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
|
||||
{
|
||||
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||
"delete_linked_ledger_entries": 1,
|
||||
"unlink_advance_payment_on_cancelation_of_order": 1,
|
||||
},
|
||||
)
|
||||
def test_advance_payment_unlink_on_order_cancellation(self):
|
||||
transaction_date = nowdate()
|
||||
|
||||
@@ -109,6 +109,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
"account_currency": "INR",
|
||||
"account_type": "Payable",
|
||||
},
|
||||
# 'Receivable' account for capturing advance received, under 'Liabilities' group
|
||||
{
|
||||
"attribute": "advance_receivable_account",
|
||||
"account_name": "Advance Received",
|
||||
"parent_account": "Current Liabilities - _PR",
|
||||
"account_currency": "INR",
|
||||
"account_type": "Receivable",
|
||||
},
|
||||
]
|
||||
|
||||
for x in accounts:
|
||||
@@ -1574,6 +1582,229 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
def test_advance_payment_reconciliation_against_journal_for_customer(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_received_account": self.advance_receivable_account,
|
||||
"reconcile_on_advance_payment_date": 0,
|
||||
},
|
||||
)
|
||||
amount = 200.0
|
||||
je = self.create_journal_entry(self.debit_to, self.bank, amount)
|
||||
je.accounts[0].cost_center = self.main_cc.name
|
||||
je.accounts[0].party_type = "Customer"
|
||||
je.accounts[0].party = self.customer
|
||||
je.accounts[1].cost_center = self.main_cc.name
|
||||
je = je.save().submit()
|
||||
|
||||
pe = self.create_payment_entry(amount=amount).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.default_advance_account = self.advance_receivable_account
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 4)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name, "delinked": 0},
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||
order_by="account, against_voucher, debit",
|
||||
)
|
||||
expected_gle = [
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": je.name,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
{
|
||||
"account": self.bank,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": None,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(gl_entries, expected_gle)
|
||||
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name},
|
||||
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||
order_by="account, against_voucher_no, amount",
|
||||
)
|
||||
expected_ple = [
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": amount,
|
||||
},
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": je.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
def test_advance_payment_reconciliation_against_journal_for_supplier(self):
|
||||
self.supplier = make_supplier("_Test Supplier")
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconcile_on_advance_payment_date": 0,
|
||||
},
|
||||
)
|
||||
amount = 200.0
|
||||
je = self.create_journal_entry(self.creditors, self.bank, -amount)
|
||||
je.accounts[0].cost_center = self.main_cc.name
|
||||
je.accounts[0].party_type = "Supplier"
|
||||
je.accounts[0].party = self.supplier
|
||||
je.accounts[1].cost_center = self.main_cc.name
|
||||
je = je.save().submit()
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.payment_type = "Pay"
|
||||
pe.party_type = "Supplier"
|
||||
pe.paid_from = self.bank
|
||||
pe.paid_to = self.creditors
|
||||
pe.party = self.supplier
|
||||
pe.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 4)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name, "delinked": 0},
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||
order_by="account, against_voucher, debit",
|
||||
)
|
||||
expected_gle = [
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": self.creditors,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": je.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": self.bank,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": None,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(gl_entries, expected_gle)
|
||||
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name},
|
||||
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||
order_by="account, against_voucher_no, amount",
|
||||
)
|
||||
expected_ple = [
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": amount,
|
||||
},
|
||||
{
|
||||
"account": self.creditors,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": je.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -1237,6 +1237,68 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_pricing_rules_with_and_without_apply_multiple(self):
|
||||
item = make_item("PR Item 99")
|
||||
|
||||
test_records = [
|
||||
{
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test discount on item group",
|
||||
"name": "_Test discount on item group",
|
||||
"apply_on": "Item Group",
|
||||
"item_groups": [
|
||||
{
|
||||
"item_group": "Products",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 60,
|
||||
"has_priority": 1,
|
||||
"company": "_Test Company",
|
||||
"apply_multiple_pricing_rules": True,
|
||||
},
|
||||
{
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test fixed rate on item code",
|
||||
"name": "_Test fixed rate on item code",
|
||||
"apply_on": "Item Code",
|
||||
"items": [
|
||||
{
|
||||
"item_code": item.name,
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 25,
|
||||
"has_priority": 1,
|
||||
"company": "_Test Company",
|
||||
"apply_multiple_pricing_rules": False,
|
||||
},
|
||||
]
|
||||
|
||||
for item_group_priority, item_code_priority in [(2, 4), (4, 2)]:
|
||||
item_group_rule = frappe.get_doc(test_records[0].copy())
|
||||
item_group_rule.priority = item_group_priority
|
||||
item_group_rule.insert()
|
||||
|
||||
item_code_rule = frappe.get_doc(test_records[1].copy())
|
||||
item_code_rule.priority = item_code_priority
|
||||
item_code_rule.insert()
|
||||
|
||||
si = create_sales_invoice(qty=5, customer="_Test Customer 1", item=item.name, do_not_submit=True)
|
||||
si.save()
|
||||
self.assertEqual(len(si.pricing_rules), 1)
|
||||
# Item Code rule should've applied as it has higher priority
|
||||
expected_rule = item_group_rule if item_group_priority > item_code_priority else item_code_rule
|
||||
self.assertEqual(si.pricing_rules[0].pricing_rule, expected_rule.name)
|
||||
|
||||
si.delete()
|
||||
item_group_rule.delete()
|
||||
item_code_rule.delete()
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
|
||||
@@ -174,12 +174,9 @@ def _get_pricing_rules(apply_on, args, values):
|
||||
|
||||
|
||||
def apply_multiple_pricing_rules(pricing_rules):
|
||||
apply_multiple_rule = [
|
||||
d.apply_multiple_pricing_rules for d in pricing_rules if d.apply_multiple_pricing_rules
|
||||
]
|
||||
|
||||
if not apply_multiple_rule:
|
||||
return False
|
||||
for d in pricing_rules:
|
||||
if not d.apply_multiple_pricing_rules:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -549,6 +549,21 @@ class PurchaseInvoice(BuyingController):
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset:
|
||||
account = None
|
||||
if not item.pr_detail and item.po_detail:
|
||||
receipt_item = frappe.get_cached_value(
|
||||
"Purchase Receipt Item",
|
||||
{
|
||||
"purchase_order": item.purchase_order,
|
||||
"purchase_order_item": item.po_detail,
|
||||
"docstatus": 1,
|
||||
},
|
||||
["name", "parent"],
|
||||
as_dict=1,
|
||||
)
|
||||
if receipt_item:
|
||||
item.pr_detail = receipt_item.name
|
||||
item.purchase_receipt = receipt_item.parent
|
||||
|
||||
if item.pr_detail:
|
||||
if not self.asset_received_but_not_billed:
|
||||
self.asset_received_but_not_billed = self.get_company_default(
|
||||
@@ -593,7 +608,7 @@ class PurchaseInvoice(BuyingController):
|
||||
for item in self.get("items"):
|
||||
validate_account_head(item.idx, item.expense_account, self.company, "Expense")
|
||||
|
||||
def set_against_expense_account(self):
|
||||
def set_against_expense_account(self, force=False):
|
||||
against_accounts = []
|
||||
for item in self.get("items"):
|
||||
if item.expense_account and (item.expense_account not in against_accounts):
|
||||
@@ -601,6 +616,10 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.against_expense_account = ",".join(against_accounts)
|
||||
|
||||
def force_set_against_expense_account(self):
|
||||
self.set_against_expense_account()
|
||||
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
||||
|
||||
def po_required(self):
|
||||
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
|
||||
if frappe.get_value(
|
||||
@@ -683,6 +702,19 @@ class PurchaseInvoice(BuyingController):
|
||||
where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""",
|
||||
}
|
||||
)
|
||||
self.status_updater.append(
|
||||
{
|
||||
"source_dt": "Purchase Invoice Item",
|
||||
"target_dt": "Material Request Item",
|
||||
"join_field": "material_request_item",
|
||||
"target_field": "received_qty",
|
||||
"target_parent_dt": "Material Request",
|
||||
"target_parent_field": "per_received",
|
||||
"target_ref_field": "stock_qty",
|
||||
"source_field": "stock_qty",
|
||||
"percent_join_field": "material_request",
|
||||
}
|
||||
)
|
||||
if cint(self.is_return):
|
||||
self.status_updater.append(
|
||||
{
|
||||
@@ -778,13 +810,12 @@ class PurchaseInvoice(BuyingController):
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries()
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
if self.docstatus == 1:
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries()
|
||||
|
||||
if gl_entries:
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
|
||||
if self.docstatus == 1:
|
||||
if gl_entries:
|
||||
make_gl_entries(
|
||||
gl_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
@@ -792,32 +823,43 @@ class PurchaseInvoice(BuyingController):
|
||||
from_repost=from_repost,
|
||||
)
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
if provisional_entries:
|
||||
for entry in provisional_entries:
|
||||
frappe.db.set_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_detail_no": entry.voucher_detail_no,
|
||||
},
|
||||
"is_cancelled",
|
||||
1,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock:
|
||||
elif self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
self.cancel_provisional_entries()
|
||||
|
||||
self.update_supplier_outstanding(update_outstanding)
|
||||
|
||||
def cancel_provisional_entries(self):
|
||||
rows = set()
|
||||
purchase_receipts = set()
|
||||
for d in self.items:
|
||||
if d.purchase_receipt:
|
||||
purchase_receipts.add(d.purchase_receipt)
|
||||
rows.add(d.name)
|
||||
|
||||
if rows:
|
||||
# cancel gl entries
|
||||
gle = qb.DocType("GL Entry")
|
||||
gle_update_query = (
|
||||
qb.update(gle)
|
||||
.set(gle.is_cancelled, 1)
|
||||
.where(
|
||||
(gle.voucher_type == "Purchase Receipt")
|
||||
& (gle.voucher_no.isin(purchase_receipts))
|
||||
& (gle.voucher_detail_no.isin(rows))
|
||||
)
|
||||
)
|
||||
gle_update_query.run()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None):
|
||||
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
||||
@@ -930,8 +972,8 @@ class PurchaseInvoice(BuyingController):
|
||||
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
||||
)
|
||||
)
|
||||
|
||||
purchase_receipt_doc_map = {}
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.get_provisional_accounts()
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
@@ -1070,49 +1112,7 @@ class PurchaseInvoice(BuyingController):
|
||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
provisional_account, pr_qty, pr_base_rate, pr_rate = frappe.get_cached_value(
|
||||
"Purchase Receipt Item",
|
||||
item.pr_detail,
|
||||
["provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
provisional_account = provisional_account or self.get_company_default(
|
||||
"default_provisional_account"
|
||||
)
|
||||
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
|
||||
|
||||
if not purchase_receipt_doc:
|
||||
purchase_receipt_doc = frappe.get_doc(
|
||||
"Purchase Receipt", item.purchase_receipt
|
||||
)
|
||||
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
|
||||
|
||||
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
|
||||
expense_booked_in_pr = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"is_cancelled": 0,
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": item.purchase_receipt,
|
||||
"voucher_detail_no": item.pr_detail,
|
||||
"account": provisional_account,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
|
||||
if expense_booked_in_pr:
|
||||
# Intentionally passing purchase invoice item to handle partial billing
|
||||
purchase_receipt_doc.add_provisional_gl_entry(
|
||||
item,
|
||||
gl_entries,
|
||||
self.posting_date,
|
||||
provisional_account,
|
||||
reverse=1,
|
||||
item_amount=(
|
||||
(min(item.qty, pr_qty) * pr_rate)
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
self.make_provisional_gl_entry(gl_entries, item)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
@@ -1208,6 +1208,59 @@ class PurchaseInvoice(BuyingController):
|
||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||
self.update_gross_purchase_amount_for_linked_assets(item)
|
||||
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate"],
|
||||
)
|
||||
default_provisional_account = self.get_company_default("default_provisional_account")
|
||||
provisional_accounts = set(
|
||||
[
|
||||
d.provisional_expense_account
|
||||
if d.provisional_expense_account
|
||||
else default_provisional_account
|
||||
for d in pr_items
|
||||
]
|
||||
)
|
||||
|
||||
provisional_gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": ("in", linked_purchase_receipts),
|
||||
"account": ("in", provisional_accounts),
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
fields=["voucher_detail_no"],
|
||||
)
|
||||
rows_with_provisional_entries = [d.voucher_detail_no for d in provisional_gl_entries]
|
||||
for item in pr_items:
|
||||
self.provisional_accounts[item.name] = {
|
||||
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||
"qty": item.qty,
|
||||
"base_rate": item.base_rate,
|
||||
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||
}
|
||||
|
||||
def make_provisional_gl_entry(self, gl_entries, item):
|
||||
if item.purchase_receipt:
|
||||
pr_item = self.provisional_accounts.get(item.pr_detail, {})
|
||||
if pr_item.get("has_provisional_entry"):
|
||||
purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt)
|
||||
|
||||
# Intentionally passing purchase invoice item to handle partial billing
|
||||
purchase_receipt_doc.add_provisional_gl_entry(
|
||||
item,
|
||||
gl_entries,
|
||||
self.posting_date,
|
||||
pr_item.get("provisional_account"),
|
||||
reverse=1,
|
||||
item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
|
||||
)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
|
||||
@@ -10,13 +10,19 @@ import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice as make_pi_from_po
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.test_material_request import make_material_request
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
||||
)
|
||||
@@ -72,6 +78,31 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
# teardown
|
||||
pi.delete()
|
||||
|
||||
def test_update_received_qty_in_material_request(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
"""
|
||||
Test if the received_qty in Material Request is updated correctly when
|
||||
a Purchase Invoice with update_stock=True is submitted.
|
||||
"""
|
||||
mr = make_material_request(item_code="_Test Item", qty=10)
|
||||
mr.save()
|
||||
mr.submit()
|
||||
po = make_purchase_order(mr.name)
|
||||
po.supplier = "_Test Supplier"
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
# Create a Purchase Invoice with update_stock=True
|
||||
pi = make_purchase_invoice(po.name)
|
||||
pi.update_stock = True
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# Check if the received quantity is updated in Material Request
|
||||
mr.reload()
|
||||
self.assertEqual(mr.items[0].received_qty, 10)
|
||||
|
||||
def test_gl_entries_without_perpetual_inventory(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||
pi = frappe.copy_doc(test_records[0])
|
||||
@@ -2158,6 +2189,56 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
|
||||
self.assertEqual(row.rejected_serial_no, serial_nos[2])
|
||||
|
||||
def test_make_pr_and_pi_from_po(self):
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_category
|
||||
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
create_asset_category()
|
||||
|
||||
item = create_item(
|
||||
item_code="_Test_Item", is_stock_item=0, is_fixed_asset=1, asset_category="Computers"
|
||||
)
|
||||
po = create_purchase_order(item_code=item.item_code)
|
||||
pr = create_pr_against_po(po.name, 10)
|
||||
pi = make_pi_from_po(po.name)
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
pr_gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no=%s
|
||||
order by account asc""",
|
||||
pr.name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pr_expected_values = [
|
||||
["Asset Received But Not Billed - _TC", 0, 5000],
|
||||
["CWIP Account - _TC", 5000, 0],
|
||||
]
|
||||
|
||||
for i, gle in enumerate(pr_gl_entries):
|
||||
self.assertEqual(pr_expected_values[i][0], gle.account)
|
||||
self.assertEqual(pr_expected_values[i][1], gle.debit)
|
||||
self.assertEqual(pr_expected_values[i][2], gle.credit)
|
||||
|
||||
pi_gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
|
||||
order by account asc""",
|
||||
pi.name,
|
||||
as_dict=1,
|
||||
)
|
||||
pi_expected_values = [
|
||||
["Asset Received But Not Billed - _TC", 5000, 0],
|
||||
["Creditors - _TC", 0, 5000],
|
||||
]
|
||||
|
||||
for i, gle in enumerate(pi_gl_entries):
|
||||
self.assertEqual(pi_expected_values[i][0], gle.account)
|
||||
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
||||
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"purchase_receipt",
|
||||
"pr_detail",
|
||||
"sales_invoice_item",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"item_weight_details",
|
||||
"weight_per_unit",
|
||||
"total_weight",
|
||||
@@ -934,12 +936,34 @@
|
||||
{
|
||||
"fieldname": "column_break_vbbb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "material_request",
|
||||
"fieldtype": "Link",
|
||||
"label": "Material Request",
|
||||
"no_copy": 1,
|
||||
"options": "Material Request",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "material_request_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Material Request Item",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "pr_detail",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-19 19:09:47.210965",
|
||||
"modified": "2024-06-14 11:57:07.171700",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
@@ -949,4 +973,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ class PurchaseInvoiceItem(Document):
|
||||
manufacturer_part_no: DF.Data | None
|
||||
margin_rate_or_amount: DF.Float
|
||||
margin_type: DF.Literal["", "Percentage", "Amount"]
|
||||
material_request: DF.Link | None
|
||||
material_request_item: DF.Data | None
|
||||
net_amount: DF.Currency
|
||||
net_rate: DF.Currency
|
||||
page_break: DF.Check
|
||||
|
||||
@@ -167,6 +167,10 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
if doc.doctype == "Sales Invoice":
|
||||
doc.force_set_against_income_account()
|
||||
else:
|
||||
doc.force_set_against_expense_account()
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
|
||||
@@ -943,6 +943,10 @@ class SalesInvoice(SellingController):
|
||||
against_acc.append(d.income_account)
|
||||
self.against_income_account = ",".join(against_acc)
|
||||
|
||||
def force_set_against_income_account(self):
|
||||
self.set_against_income_account()
|
||||
frappe.db.set_value(self.doctype, self.name, "against_income_account", self.against_income_account)
|
||||
|
||||
def add_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.po_no and self.po_date:
|
||||
@@ -2678,6 +2682,10 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.closing_text = letter_text.get("closing_text")
|
||||
target.language = letter_text.get("language")
|
||||
|
||||
# update outstanding
|
||||
if source.payment_schedule and len(source.payment_schedule) == 1:
|
||||
target.overdue_payments[0].outstanding = source.get("outstanding_amount")
|
||||
|
||||
target.validate()
|
||||
|
||||
return get_mapped_doc(
|
||||
|
||||
@@ -2202,13 +2202,14 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
|
||||
expected_values = [
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["Round Off - _TC", 0.01, 0.01],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
]
|
||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||
expected_values = {
|
||||
"_Test Account Service Tax - _TC": [0.0, 114.41],
|
||||
"_Test Account VAT - _TC": [0.0, 114.41],
|
||||
si.debit_to: [1500, 0.0],
|
||||
round_off_account: [0.01, 0.01],
|
||||
"Sales - _TC": [0.0, 1271.18],
|
||||
}
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
@@ -2219,10 +2220,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[i][0], gle.account)
|
||||
self.assertEqual(expected_values[i][1], gle.debit)
|
||||
self.assertEqual(expected_values[i][2], gle.credit)
|
||||
for gle in gl_entries:
|
||||
expected_account_values = expected_values[gle.account]
|
||||
self.assertEqual(expected_account_values[0], gle.debit)
|
||||
self.assertEqual(expected_account_values[1], gle.credit)
|
||||
|
||||
def test_rounding_adjustment_3(self):
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
@@ -2270,6 +2271,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||
|
||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
@@ -2277,7 +2279,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
["Round Off - _TC", 0.02, 0.01],
|
||||
[round_off_account, 0.02, 0.01],
|
||||
]
|
||||
)
|
||||
|
||||
@@ -2306,8 +2308,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||
self.assertEqual(round_off_gle.location, "Block 1")
|
||||
if round_off_gle:
|
||||
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||
self.assertEqual(round_off_gle.location, "Block 1")
|
||||
|
||||
disable_dimension()
|
||||
|
||||
|
||||
@@ -476,7 +476,7 @@ class TestSubscription(FrappeTestCase):
|
||||
start_date="2021-01-01",
|
||||
submit_invoice=0,
|
||||
generate_new_invoices_past_due_date=1,
|
||||
party="_Test Subscription Customer",
|
||||
party="_Test Subscription Customer John Doe",
|
||||
)
|
||||
|
||||
# create invoices for the first two moths
|
||||
@@ -565,10 +565,16 @@ def create_parties():
|
||||
if not frappe.db.exists("Customer", "_Test Subscription Customer"):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Subscription Customer"
|
||||
customer.billing_currency = "USD"
|
||||
customer.default_currency = "USD"
|
||||
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
|
||||
customer.insert()
|
||||
|
||||
if not frappe.db.exists("Customer", "_Test Subscription Customer John Doe"):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Subscription Customer John Doe"
|
||||
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable - _TC"})
|
||||
customer.insert()
|
||||
|
||||
|
||||
def reset_settings():
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
|
||||
@@ -107,7 +107,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
def test_02_unreconcile_one_payment_from_multi_payments(self):
|
||||
def test_02_unreconcile_one_payment_among_multi_payments(self):
|
||||
"""
|
||||
Scenario: 2 payments, both split against 2 different invoices
|
||||
Unreconcile only one payment from one invoice
|
||||
|
||||
@@ -7,7 +7,7 @@ import copy
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, cstr, flt, formatdate, getdate, now
|
||||
from frappe.utils import cint, flt, formatdate, getdate, now
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -228,11 +228,13 @@ def get_cost_center_allocation_data(company, posting_date):
|
||||
def merge_similar_entries(gl_map, precision=None):
|
||||
merged_gl_map = []
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
merge_properties = get_merge_properties(accounting_dimensions)
|
||||
|
||||
for entry in gl_map:
|
||||
entry.merge_key = get_merge_key(entry, merge_properties)
|
||||
# if there is already an entry in this account then just add it
|
||||
# to that entry
|
||||
same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions)
|
||||
same_head = check_if_in_list(entry, merged_gl_map)
|
||||
if same_head:
|
||||
same_head.debit = flt(same_head.debit) + flt(entry.debit)
|
||||
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
||||
@@ -273,34 +275,35 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
return merged_gl_map
|
||||
|
||||
|
||||
def check_if_in_list(gle, gl_map, dimensions=None):
|
||||
account_head_fieldnames = [
|
||||
"voucher_detail_no",
|
||||
"party",
|
||||
"against_voucher",
|
||||
def get_merge_properties(dimensions=None):
|
||||
merge_properties = [
|
||||
"account",
|
||||
"cost_center",
|
||||
"against_voucher_type",
|
||||
"party",
|
||||
"party_type",
|
||||
"voucher_detail_no",
|
||||
"against_voucher",
|
||||
"against_voucher_type",
|
||||
"project",
|
||||
"finance_book",
|
||||
"voucher_no",
|
||||
]
|
||||
|
||||
if dimensions:
|
||||
account_head_fieldnames = account_head_fieldnames + dimensions
|
||||
merge_properties.extend(dimensions)
|
||||
return merge_properties
|
||||
|
||||
|
||||
def get_merge_key(entry, merge_properties):
|
||||
merge_key = []
|
||||
for fieldname in merge_properties:
|
||||
merge_key.append(entry.get(fieldname, ""))
|
||||
|
||||
return tuple(merge_key)
|
||||
|
||||
|
||||
def check_if_in_list(gle, gl_map):
|
||||
for e in gl_map:
|
||||
same_head = True
|
||||
if e.account != gle.account:
|
||||
same_head = False
|
||||
continue
|
||||
|
||||
for fieldname in account_head_fieldnames:
|
||||
if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
|
||||
same_head = False
|
||||
break
|
||||
|
||||
if same_head:
|
||||
if e.merge_key == gle.merge_key:
|
||||
return e
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
label: __("Group By"),
|
||||
fieldtype: "Select",
|
||||
options:
|
||||
"Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term",
|
||||
"Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nCost Center\nMonthly\nPayment Term",
|
||||
default: "Invoice",
|
||||
},
|
||||
{
|
||||
@@ -63,6 +63,26 @@ frappe.query_reports["Gross Profit"] = {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
tree: true,
|
||||
name_field: "parent",
|
||||
@@ -85,3 +105,5 @@ frappe.query_reports["Gross Profit"] = {
|
||||
return value;
|
||||
},
|
||||
};
|
||||
|
||||
erpnext.utils.add_dimensions("Gross Profit", 15);
|
||||
|
||||
@@ -8,6 +8,11 @@ from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Order
|
||||
from frappe.utils import cint, flt, formatdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
@@ -120,6 +125,13 @@ def execute(filters=None):
|
||||
"gross_profit_percent",
|
||||
],
|
||||
"project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"],
|
||||
"cost_center": [
|
||||
"cost_center",
|
||||
"base_amount",
|
||||
"buying_amount",
|
||||
"gross_profit",
|
||||
"gross_profit_percent",
|
||||
],
|
||||
"territory": [
|
||||
"territory",
|
||||
"base_amount",
|
||||
@@ -299,7 +311,14 @@ def get_columns(group_wise_columns, filters):
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"options": "Project",
|
||||
"width": 100,
|
||||
"width": 140,
|
||||
},
|
||||
"cost_center": {
|
||||
"label": _("Cost Center"),
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"options": "Cost Center",
|
||||
"width": 140,
|
||||
},
|
||||
"sales_person": {
|
||||
"label": _("Sales Person"),
|
||||
@@ -787,6 +806,31 @@ class GrossProfitGenerator:
|
||||
if self.filters.get("item_code"):
|
||||
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s"
|
||||
|
||||
if self.filters.get("cost_center"):
|
||||
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
|
||||
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
|
||||
conditions += " and `tabSales Invoice Item`.cost_center in %(cost_center)s"
|
||||
|
||||
if self.filters.get("project"):
|
||||
self.filters.project = frappe.parse_json(self.filters.get("project"))
|
||||
conditions += " and `tabSales Invoice Item`.project in %(project)s"
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
if self.filters.get(dimension.fieldname):
|
||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, self.filters.get(dimension.fieldname)
|
||||
)
|
||||
conditions += (
|
||||
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
|
||||
)
|
||||
else:
|
||||
conditions += (
|
||||
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
|
||||
)
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
|
||||
@@ -52,7 +52,7 @@ frappe.query_reports["Item-wise Purchase Register"] = {
|
||||
label: __("Group By"),
|
||||
fieldname: "group_by",
|
||||
fieldtype: "Select",
|
||||
options: ["Supplier", "Item Group", "Item", "Invoice"],
|
||||
options: ["", "Supplier", "Item Group", "Item", "Invoice"],
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -289,7 +289,7 @@ def get_columns(additional_table_columns, filters):
|
||||
|
||||
|
||||
def apply_conditions(query, pi, pii, filters):
|
||||
for opts in ("company", "supplier", "item_code", "mode_of_payment"):
|
||||
for opts in ("company", "supplier", "mode_of_payment"):
|
||||
if filters.get(opts):
|
||||
query = query.where(pi[opts] == filters[opts])
|
||||
|
||||
@@ -299,6 +299,9 @@ def apply_conditions(query, pi, pii, filters):
|
||||
if filters.get("to_date"):
|
||||
query = query.where(pi.posting_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("item_code"):
|
||||
query = query.where(pii.item_code == filters.get("item_code"))
|
||||
|
||||
if filters.get("item_group"):
|
||||
query = query.where(pii.item_group == filters.get("item_group"))
|
||||
|
||||
@@ -306,7 +309,7 @@ def apply_conditions(query, pi, pii, filters):
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(filters, "Purchase Invoice")
|
||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return query
|
||||
|
||||
@@ -322,7 +325,7 @@ def get_items(filters, additional_table_columns):
|
||||
.left_join(Item)
|
||||
.on(pii.item_code == Item.name)
|
||||
.select(
|
||||
pii.name.as_("pii_name"),
|
||||
pii.name,
|
||||
pii.parent,
|
||||
pi.posting_date,
|
||||
pi.credit_to,
|
||||
|
||||
@@ -54,6 +54,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
fieldtype: "Link",
|
||||
options: "Brand",
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
@@ -64,7 +70,7 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
label: __("Group By"),
|
||||
fieldname: "group_by",
|
||||
fieldtype: "Select",
|
||||
options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"],
|
||||
options: ["", "Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"],
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -342,7 +342,7 @@ def get_columns(additional_table_columns, filters):
|
||||
|
||||
|
||||
def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
for opts in ("company", "customer", "item_code"):
|
||||
for opts in ("company", "customer"):
|
||||
if filters.get(opts):
|
||||
query = query.where(si[opts] == filters[opts])
|
||||
|
||||
@@ -371,6 +371,9 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
if filters.get("brand"):
|
||||
query = query.where(sii.brand == filters.get("brand"))
|
||||
|
||||
if filters.get("item_code"):
|
||||
query = query.where(sii.item_code == filters.get("item_code"))
|
||||
|
||||
if filters.get("item_group"):
|
||||
query = query.where(sii.item_group == filters.get("item_group"))
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ def _execute(filters, additional_table_columns=None):
|
||||
delivery_note = list(set(invoice_so_dn_map.get(inv.name, {}).get("delivery_note", [])))
|
||||
cost_center = list(set(invoice_cc_wh_map.get(inv.name, {}).get("cost_center", [])))
|
||||
warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", [])))
|
||||
inv_customer_details = customer_details.get(inv.customer, {})
|
||||
|
||||
row = {
|
||||
"voucher_type": inv.doctype,
|
||||
@@ -88,9 +89,9 @@ def _execute(filters, additional_table_columns=None):
|
||||
"customer": inv.customer,
|
||||
"customer_name": inv.customer_name,
|
||||
**get_values_for_columns(additional_table_columns, inv),
|
||||
"customer_group": customer_details.get(inv.customer).get("customer_group"),
|
||||
"territory": customer_details.get(inv.customer).get("territory"),
|
||||
"tax_id": customer_details.get(inv.customer).get("tax_id"),
|
||||
"customer_group": inv_customer_details.get("customer_group"),
|
||||
"territory": inv_customer_details.get("territory"),
|
||||
"tax_id": inv_customer_details.get("tax_id"),
|
||||
"receivable_account": inv.debit_to,
|
||||
"mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])),
|
||||
"project": inv.project,
|
||||
|
||||
@@ -10,7 +10,7 @@ import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Count, Sum
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
@@ -1492,24 +1492,39 @@ def get_stock_accounts(company, voucher_type=None, voucher_no=None):
|
||||
)
|
||||
]
|
||||
|
||||
return stock_accounts
|
||||
return list(set(stock_accounts))
|
||||
|
||||
|
||||
def get_stock_and_account_balance(account=None, posting_date=None, company=None):
|
||||
if not posting_date:
|
||||
posting_date = nowdate()
|
||||
|
||||
warehouse_account = get_warehouse_account_map(company)
|
||||
|
||||
account_balance = get_balance_on(
|
||||
account, posting_date, in_account_currency=False, ignore_account_permission=True
|
||||
)
|
||||
|
||||
related_warehouses = [
|
||||
wh
|
||||
for wh, wh_details in warehouse_account.items()
|
||||
if wh_details.account == account and not wh_details.is_group
|
||||
]
|
||||
account_table = frappe.qb.DocType("Account")
|
||||
query = (
|
||||
frappe.qb.from_(account_table)
|
||||
.select(Count(account_table.name))
|
||||
.where(
|
||||
(account_table.account_type == "Stock")
|
||||
& (account_table.company == company)
|
||||
& (account_table.is_group == 0)
|
||||
)
|
||||
)
|
||||
|
||||
no_of_stock_accounts = cint(query.run()[0][0])
|
||||
|
||||
related_warehouses = []
|
||||
if no_of_stock_accounts > 1:
|
||||
warehouse_account = get_warehouse_account_map(company)
|
||||
|
||||
related_warehouses = [
|
||||
wh
|
||||
for wh, wh_details in warehouse_account.items()
|
||||
if wh_details.account == account and not wh_details.is_group
|
||||
]
|
||||
|
||||
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"calculate_depreciation",
|
||||
"column_break_33",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
"opening_number_of_booked_depreciations",
|
||||
"is_fully_depreciated",
|
||||
"section_break_36",
|
||||
"finance_books",
|
||||
@@ -257,12 +257,6 @@
|
||||
"label": "Opening Accumulated Depreciation",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "number_of_depreciations_booked",
|
||||
"fieldtype": "Int",
|
||||
"label": "Number of Depreciations Booked"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset",
|
||||
@@ -546,6 +540,12 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Opening Number of Booked Depreciations"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -589,7 +589,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-18 16:45:47.306032",
|
||||
"modified": "2024-05-21 13:46:21.066483",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -89,8 +89,8 @@ class Asset(AccountsController):
|
||||
maintenance_required: DF.Check
|
||||
naming_series: DF.Literal["ACC-ASS-.YYYY.-"]
|
||||
next_depreciation_date: DF.Date | None
|
||||
number_of_depreciations_booked: DF.Int
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
policy_number: DF.Data | None
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
@@ -145,7 +145,7 @@ class Asset(AccountsController):
|
||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||
).format(asset_depr_schedules_links)
|
||||
)
|
||||
|
||||
self.set_total_booked_depreciations()
|
||||
self.total_asset_cost = self.gross_purchase_amount
|
||||
self.status = self.get_status()
|
||||
|
||||
@@ -417,7 +417,7 @@ class Asset(AccountsController):
|
||||
|
||||
if not self.is_existing_asset:
|
||||
self.opening_accumulated_depreciation = 0
|
||||
self.number_of_depreciations_booked = 0
|
||||
self.opening_number_of_booked_depreciations = 0
|
||||
else:
|
||||
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
@@ -428,15 +428,15 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
if self.opening_accumulated_depreciation:
|
||||
if not self.number_of_depreciations_booked:
|
||||
frappe.throw(_("Please set Number of Depreciations Booked"))
|
||||
if not self.opening_number_of_booked_depreciations:
|
||||
frappe.throw(_("Please set Opening Number of Booked Depreciations"))
|
||||
else:
|
||||
self.number_of_depreciations_booked = 0
|
||||
self.opening_number_of_booked_depreciations = 0
|
||||
|
||||
if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
|
||||
if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked"
|
||||
"Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
|
||||
).format(row.idx),
|
||||
title=_("Invalid Schedule"),
|
||||
)
|
||||
@@ -457,6 +457,17 @@ class Asset(AccountsController):
|
||||
).format(row.idx)
|
||||
)
|
||||
|
||||
def set_total_booked_depreciations(self):
|
||||
# set value of total number of booked depreciations field
|
||||
for fb_row in self.get("finance_books"):
|
||||
total_number_of_booked_depreciations = self.opening_number_of_booked_depreciations
|
||||
depr_schedule = get_depr_schedule(self.name, "Active", fb_row.finance_book)
|
||||
if depr_schedule:
|
||||
for je in depr_schedule:
|
||||
if je.journal_entry:
|
||||
total_number_of_booked_depreciations += 1
|
||||
fb_row.db_set("total_number_of_booked_depreciations", total_number_of_booked_depreciations)
|
||||
|
||||
def validate_expected_value_after_useful_life(self):
|
||||
for row in self.get("finance_books"):
|
||||
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
||||
|
||||
@@ -323,6 +323,7 @@ def _make_journal_entry_for_depreciation(
|
||||
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
asset.reload()
|
||||
idx = cint(asset_depr_schedule_doc.finance_book_id)
|
||||
row = asset.get("finance_books")[idx - 1]
|
||||
row.value_after_depreciation -= depr_schedule.depreciation_amount
|
||||
|
||||
@@ -355,7 +355,7 @@ class TestAsset(AssetSetup):
|
||||
purchase_date="2020-04-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=5,
|
||||
number_of_depreciations_booked=2,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
@@ -453,7 +453,7 @@ class TestAsset(AssetSetup):
|
||||
purchase_date="2020-01-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=6,
|
||||
number_of_depreciations_booked=1,
|
||||
opening_number_of_booked_depreciations=1,
|
||||
frequency_of_depreciation=10,
|
||||
depreciation_start_date="2021-01-01",
|
||||
opening_accumulated_depreciation=20000,
|
||||
@@ -739,7 +739,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2030-06-06",
|
||||
is_existing_asset=1,
|
||||
number_of_depreciations_booked=2,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
opening_accumulated_depreciation=47095.89,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2032-12-31",
|
||||
@@ -789,7 +789,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
available_for_use_date="2030-01-01",
|
||||
is_existing_asset=1,
|
||||
depreciation_method="Double Declining Balance",
|
||||
number_of_depreciations_booked=1,
|
||||
opening_number_of_booked_depreciations=1,
|
||||
opening_accumulated_depreciation=50000,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2031-12-31",
|
||||
@@ -1123,8 +1123,8 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
def test_number_of_depreciations_booked(self):
|
||||
"""Tests if an error is raised when number_of_depreciations_booked is not specified when opening_accumulated_depreciation is."""
|
||||
def test_opening_booked_depreciations(self):
|
||||
"""Tests if an error is raised when opening_number_of_booked_depreciations is not specified when opening_accumulated_depreciation is."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
@@ -1140,9 +1140,9 @@ class TestDepreciationBasics(AssetSetup):
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
def test_number_of_depreciations(self):
|
||||
"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
|
||||
"""Tests if an error is raised when opening_number_of_booked_depreciations >= total_number_of_depreciations."""
|
||||
|
||||
# number_of_depreciations_booked > total_number_of_depreciations
|
||||
# opening_number_of_booked_depreciations > total_number_of_depreciations
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
@@ -1151,13 +1151,13 @@ class TestDepreciationBasics(AssetSetup):
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2020-07-01",
|
||||
opening_accumulated_depreciation=10000,
|
||||
number_of_depreciations_booked=5,
|
||||
opening_number_of_booked_depreciations=5,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
# number_of_depreciations_booked = total_number_of_depreciations
|
||||
# opening_number_of_booked_depreciations = total_number_of_depreciations
|
||||
asset_2 = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
@@ -1166,7 +1166,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2020-07-01",
|
||||
opening_accumulated_depreciation=10000,
|
||||
number_of_depreciations_booked=5,
|
||||
opening_number_of_booked_depreciations=5,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
@@ -1501,19 +1501,17 @@ class TestDepreciationBasics(AssetSetup):
|
||||
"""
|
||||
|
||||
asset = create_asset(calculate_depreciation=1)
|
||||
asset.opening_accumulated_depreciation = 2000
|
||||
asset.number_of_depreciations_booked = 1
|
||||
|
||||
asset.finance_books[0].expected_value_after_useful_life = 100
|
||||
asset.save()
|
||||
asset.reload()
|
||||
self.assertEqual(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||
self.assertEqual(asset.finance_books[0].value_after_depreciation, 100000.0)
|
||||
|
||||
# changing expected_value_after_useful_life shouldn't affect value_after_depreciation
|
||||
asset.finance_books[0].expected_value_after_useful_life = 200
|
||||
asset.save()
|
||||
asset.reload()
|
||||
self.assertEqual(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||
self.assertEqual(asset.finance_books[0].value_after_depreciation, 100000.0)
|
||||
|
||||
def test_asset_cost_center(self):
|
||||
asset = create_asset(is_existing_asset=1, do_not_save=1)
|
||||
@@ -1696,7 +1694,7 @@ def create_asset(**args):
|
||||
"purchase_date": args.purchase_date or "2015-01-01",
|
||||
"calculate_depreciation": args.calculate_depreciation or 0,
|
||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
||||
"opening_number_of_booked_depreciations": args.opening_number_of_booked_depreciations or 0,
|
||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||
"purchase_amount": args.purchase_amount or 100000,
|
||||
"maintenance_required": args.maintenance_required or 0,
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Serial No",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -178,7 +177,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-05 11:22:57.346889",
|
||||
"modified": "2024-06-26 17:06:22.564438",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization Stock Item",
|
||||
@@ -188,4 +187,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"column_break_2",
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
"opening_number_of_booked_depreciations",
|
||||
"finance_book",
|
||||
"finance_book_id",
|
||||
"depreciation_details_section",
|
||||
@@ -171,10 +171,10 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "number_of_depreciations_booked",
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Number of Depreciations Booked",
|
||||
"label": "Opening Number of Booked Depreciations",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ class AssetDepreciationSchedule(Document):
|
||||
gross_purchase_amount: DF.Currency
|
||||
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
|
||||
notes: DF.SmallText | None
|
||||
number_of_depreciations_booked: DF.Int
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
rate_of_depreciation: DF.Percent
|
||||
shift_based: DF.Check
|
||||
@@ -161,7 +161,7 @@ class AssetDepreciationSchedule(Document):
|
||||
return (
|
||||
asset_doc.gross_purchase_amount != self.gross_purchase_amount
|
||||
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
|
||||
or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
|
||||
or asset_doc.opening_number_of_booked_depreciations != self.opening_number_of_booked_depreciations
|
||||
)
|
||||
|
||||
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
|
||||
@@ -194,7 +194,7 @@ class AssetDepreciationSchedule(Document):
|
||||
self.finance_book = row.finance_book
|
||||
self.finance_book_id = row.idx
|
||||
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
|
||||
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
|
||||
self.opening_number_of_booked_depreciations = asset_doc.opening_number_of_booked_depreciations or 0
|
||||
self.gross_purchase_amount = asset_doc.gross_purchase_amount
|
||||
self.depreciation_method = row.depreciation_method
|
||||
self.total_number_of_depreciations = row.total_number_of_depreciations
|
||||
@@ -263,7 +263,7 @@ class AssetDepreciationSchedule(Document):
|
||||
row.db_update()
|
||||
|
||||
final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||
self.number_of_depreciations_booked
|
||||
self.opening_number_of_booked_depreciations
|
||||
)
|
||||
|
||||
has_pro_rata = _check_is_pro_rata(asset_doc, row)
|
||||
@@ -328,7 +328,7 @@ class AssetDepreciationSchedule(Document):
|
||||
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
|
||||
from_date = add_months(
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||
)
|
||||
if self.depreciation_schedule:
|
||||
from_date = self.depreciation_schedule[-1].schedule_date
|
||||
@@ -378,13 +378,16 @@ class AssetDepreciationSchedule(Document):
|
||||
from_date = get_last_day(
|
||||
add_months(
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation),
|
||||
(
|
||||
(self.opening_number_of_booked_depreciations - 1)
|
||||
* row.frequency_of_depreciation
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
from_date = add_months(
|
||||
getdate(add_days(asset_doc.available_for_use_date, -1)),
|
||||
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
(self.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||
)
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
@@ -400,7 +403,8 @@ class AssetDepreciationSchedule(Document):
|
||||
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
|
||||
asset_doc.to_date = add_months(
|
||||
asset_doc.available_for_use_date,
|
||||
(n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
|
||||
(n + self.opening_number_of_booked_depreciations)
|
||||
* cint(row.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
@@ -546,34 +550,47 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
|
||||
has_pro_rata = False
|
||||
|
||||
# if not existing asset, from_date = available_for_use_date
|
||||
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# from_date = 01/01/2022
|
||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
|
||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
|
||||
if wdv_or_dd_non_yearly:
|
||||
total_days = get_total_days(row.depreciation_start_date, 12)
|
||||
else:
|
||||
# if frequency_of_depreciation is 12 months, total_days = 365
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
if days <= 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""Error: This asset already has {0} depreciation periods booked.
|
||||
The `depreciation start` date must be at least {1} periods after the `available for use` date.
|
||||
Please correct the dates accordingly."""
|
||||
).format(
|
||||
asset_doc.opening_number_of_booked_depreciations,
|
||||
asset_doc.opening_number_of_booked_depreciations,
|
||||
)
|
||||
)
|
||||
if days < total_days:
|
||||
has_pro_rata = True
|
||||
|
||||
return has_pro_rata
|
||||
|
||||
|
||||
def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False):
|
||||
if wdv_or_dd_non_yearly:
|
||||
return add_months(
|
||||
"""
|
||||
if Asset has opening booked depreciations = 9,
|
||||
available for use date = 17-07-2023,
|
||||
depreciation start date = 30-04-2024
|
||||
then from date should be 01-04-2024
|
||||
"""
|
||||
if asset_doc.opening_number_of_booked_depreciations > 0:
|
||||
from_date = add_months(
|
||||
asset_doc.available_for_use_date,
|
||||
(asset_doc.number_of_depreciations_booked * 12),
|
||||
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation) - 1,
|
||||
)
|
||||
if is_last_day_of_the_month(row.depreciation_start_date):
|
||||
return add_days(get_last_day(from_date), 1)
|
||||
|
||||
# get from date when depreciation start date is not last day of the month
|
||||
months_difference = month_diff(row.depreciation_start_date, from_date) - 1
|
||||
return add_days(add_months(row.depreciation_start_date, -1 * months_difference), 1)
|
||||
else:
|
||||
return add_months(
|
||||
asset_doc.available_for_use_date,
|
||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
)
|
||||
return asset_doc.available_for_use_date
|
||||
|
||||
|
||||
def _get_pro_rata_amt(
|
||||
@@ -678,7 +695,7 @@ def get_straight_line_or_manual_depr_amount(
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
|
||||
|
||||
|
||||
def get_daily_prorata_based_straight_line_depr(
|
||||
@@ -704,7 +721,7 @@ def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
|
||||
|
||||
asset_shift_factors_map = get_asset_shift_factors_map()
|
||||
shift = (
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
post_depreciation_entries,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
@@ -28,7 +31,7 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
||||
|
||||
def test_daily_prorata_based_depr_on_sl_methond(self):
|
||||
def test_daily_prorata_based_depr_on_sl_method(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
@@ -160,3 +163,69 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_update_total_number_of_booked_depreciations(self):
|
||||
# check if updates total number of booked depreciations when depreciation gets booked
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
opening_accumulated_depreciation=2000,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2020-01-01",
|
||||
depreciation_start_date="2020-03-31",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=24,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
post_depreciation_entries(date="2021-03-31")
|
||||
asset.reload()
|
||||
"""
|
||||
opening_number_of_booked_depreciations = 2
|
||||
number_of_booked_depreciations till 2021-03-31 = 13
|
||||
total_number_of_booked_depreciations = 15
|
||||
"""
|
||||
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 15)
|
||||
|
||||
# cancel depreciation entry
|
||||
depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
|
||||
|
||||
frappe.get_doc("Journal Entry", depr_entry).cancel()
|
||||
asset.reload()
|
||||
|
||||
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 14)
|
||||
|
||||
def test_schedule_for_wdv_method_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
available_for_use_date="2020-07-17",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
opening_accumulated_depreciation=11666.67,
|
||||
depreciation_start_date="2021-04-30",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=3,
|
||||
gross_purchase_amount=50000,
|
||||
rate_of_depreciation=40,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
expected_schedules = [
|
||||
["2021-04-30", 3833.33, 15500.0],
|
||||
["2021-07-31", 3833.33, 19333.33],
|
||||
["2021-10-31", 3833.33, 23166.66],
|
||||
["2022-01-31", 3833.33, 26999.99],
|
||||
["2022-04-30", 2300.0, 29299.99],
|
||||
["2022-07-31", 2300.0, 31599.99],
|
||||
["2022-10-31", 2300.0, 33899.99],
|
||||
["2023-01-31", 2300.0, 36199.99],
|
||||
["2023-04-30", 1380.0, 37579.99],
|
||||
["2023-07-31", 12420.01, 50000.0],
|
||||
]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"finance_book",
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"total_number_of_booked_depreciations",
|
||||
"daily_prorata_based",
|
||||
"shift_based",
|
||||
"column_break_5",
|
||||
@@ -104,12 +105,19 @@
|
||||
"fieldname": "shift_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on shifts"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "total_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Total Number of Booked Depreciations ",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-29 08:49:39.876439",
|
||||
"modified": "2024-05-21 15:48:20.907250",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
||||
@@ -28,6 +28,7 @@ class AssetFinanceBook(Document):
|
||||
rate_of_depreciation: DF.Percent
|
||||
salvage_value_percentage: DF.Percent
|
||||
shift_based: DF.Check
|
||||
total_number_of_booked_depreciations: DF.Int
|
||||
total_number_of_depreciations: DF.Int
|
||||
value_after_depreciation: DF.Currency
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -18,9 +18,7 @@ class AssetMaintenance(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.assets.doctype.asset_maintenance_task.asset_maintenance_task import (
|
||||
AssetMaintenanceTask,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_maintenance_task.asset_maintenance_task import AssetMaintenanceTask
|
||||
|
||||
asset_category: DF.ReadOnly | None
|
||||
asset_maintenance_tasks: DF.Table[AssetMaintenanceTask]
|
||||
@@ -47,6 +45,11 @@ class AssetMaintenance(Document):
|
||||
assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date)
|
||||
self.sync_maintenance_tasks()
|
||||
|
||||
def after_delete(self):
|
||||
asset = frappe.get_doc("Asset", self.asset_name)
|
||||
if asset.status == "In Maintenance":
|
||||
asset.set_status()
|
||||
|
||||
def sync_maintenance_tasks(self):
|
||||
tasks_names = []
|
||||
for task in self.get("asset_maintenance_tasks"):
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate, nowdate
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import getdate, nowdate, today
|
||||
|
||||
from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate_next_due_date
|
||||
|
||||
@@ -75,6 +76,17 @@ class AssetMaintenanceLog(Document):
|
||||
asset_maintenance_doc.save()
|
||||
|
||||
|
||||
def update_asset_maintenance_log_status():
|
||||
AssetMaintenanceLog = DocType("Asset Maintenance Log")
|
||||
(
|
||||
frappe.qb.update(AssetMaintenanceLog)
|
||||
.set(AssetMaintenanceLog.maintenance_status, "Overdue")
|
||||
.where(
|
||||
(AssetMaintenanceLog.maintenance_status == "Planned") & (AssetMaintenanceLog.due_date < today())
|
||||
)
|
||||
).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_maintenance_tasks(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@@ -20,14 +20,14 @@ frappe.ui.form.on("Asset Repair", {
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict.warehouse.get_query = function (doc) {
|
||||
frm.set_query("warehouse", "stock_items", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("serial_and_batch_bundle", "stock_items", (doc, cdt, cdn) => {
|
||||
let row = locals[cdt][cdn];
|
||||
@@ -79,7 +79,7 @@ frappe.ui.form.on("Asset Repair", {
|
||||
});
|
||||
}
|
||||
|
||||
if (frm.doc.repair_status == "Completed") {
|
||||
if (frm.doc.repair_status == "Completed" && !frm.doc.completion_date) {
|
||||
frm.set_value("completion_date", frappe.datetime.now_datetime());
|
||||
}
|
||||
},
|
||||
@@ -87,15 +87,48 @@ frappe.ui.form.on("Asset Repair", {
|
||||
stock_items_on_form_rendered() {
|
||||
erpnext.setup_serial_or_batch_no();
|
||||
},
|
||||
|
||||
stock_consumption: function (frm) {
|
||||
if (!frm.doc.stock_consumption) {
|
||||
frm.clear_table("stock_items");
|
||||
frm.refresh_field("stock_items");
|
||||
}
|
||||
},
|
||||
|
||||
purchase_invoice: function (frm) {
|
||||
if (frm.doc.purchase_invoice) {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: "Purchase Invoice",
|
||||
fieldname: "base_net_total",
|
||||
filters: { name: frm.doc.purchase_invoice },
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("repair_cost", r.message.base_net_total);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
frm.set_value("repair_cost", 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||
item_code: function (frm, cdt, cdn) {
|
||||
warehouse: function (frm, cdt, cdn) {
|
||||
var item = locals[cdt][cdn];
|
||||
|
||||
if (!item.item_code) {
|
||||
frappe.msgprint(__("Please select an item code before setting the warehouse."));
|
||||
frappe.model.set_value(cdt, cdn, "warehouse", "");
|
||||
return;
|
||||
}
|
||||
|
||||
let item_args = {
|
||||
item_code: item.item_code,
|
||||
warehouse: frm.doc.warehouse,
|
||||
warehouse: item.warehouse,
|
||||
qty: item.consumed_quantity,
|
||||
serial_no: item.serial_no,
|
||||
company: frm.doc.company,
|
||||
|
||||
@@ -22,16 +22,14 @@
|
||||
"column_break_14",
|
||||
"project",
|
||||
"accounting_details",
|
||||
"repair_cost",
|
||||
"purchase_invoice",
|
||||
"capitalize_repair_cost",
|
||||
"stock_consumption",
|
||||
"column_break_8",
|
||||
"purchase_invoice",
|
||||
"repair_cost",
|
||||
"stock_consumption_details_section",
|
||||
"warehouse",
|
||||
"stock_items",
|
||||
"total_repair_cost",
|
||||
"stock_entry",
|
||||
"asset_depreciation_details_section",
|
||||
"increase_in_asset_life",
|
||||
"section_break_9",
|
||||
@@ -122,7 +120,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "repair_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Repair Cost"
|
||||
"label": "Repair Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
@@ -218,13 +217,6 @@
|
||||
"label": "Total Repair Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "stock_consumption",
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "capitalize_repair_cost",
|
||||
"fieldname": "asset_depreciation_details_section",
|
||||
@@ -251,20 +243,12 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_entry",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock Entry",
|
||||
"no_copy": 1,
|
||||
"options": "Stock Entry",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-16 15:55:25.023471",
|
||||
"modified": "2024-06-13 16:14:14.398356",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
|
||||
@@ -47,20 +47,25 @@ class AssetRepair(AccountsController):
|
||||
repair_cost: DF.Currency
|
||||
repair_status: DF.Literal["Pending", "Completed", "Cancelled"]
|
||||
stock_consumption: DF.Check
|
||||
stock_entry: DF.Link | None
|
||||
stock_items: DF.Table[AssetRepairConsumedItem]
|
||||
total_repair_cost: DF.Currency
|
||||
warehouse: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
self.validate_dates()
|
||||
self.update_status()
|
||||
|
||||
if self.get("stock_items"):
|
||||
self.set_stock_items_cost()
|
||||
self.calculate_total_repair_cost()
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (self.failure_date > self.completion_date):
|
||||
frappe.throw(
|
||||
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
|
||||
)
|
||||
|
||||
def update_status(self):
|
||||
if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
|
||||
frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
|
||||
@@ -105,22 +110,22 @@ class AssetRepair(AccountsController):
|
||||
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||
self.modify_depreciation_schedule()
|
||||
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
|
||||
).format(
|
||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||
self.asset_doc.save()
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
|
||||
).format(
|
||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||
self.asset_doc.save()
|
||||
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset updated after completion of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset updated after completion of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
def before_cancel(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
@@ -136,29 +141,28 @@ class AssetRepair(AccountsController):
|
||||
self.asset_doc.total_asset_cost -= self.repair_cost
|
||||
self.asset_doc.additional_asset_cost -= self.repair_cost
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
self.increase_stock_quantity()
|
||||
if self.get("capitalize_repair_cost"):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.make_gl_entries(cancel=True)
|
||||
self.db_set("stock_entry", None)
|
||||
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||
self.revert_depreciation_schedule_on_cancellation()
|
||||
|
||||
notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format(
|
||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||
self.asset_doc.save()
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0}'s Asset Repair {1} was cancelled."
|
||||
).format(
|
||||
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||
self.asset_doc.save()
|
||||
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset updated after cancellation of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset updated after cancellation of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
def after_delete(self):
|
||||
frappe.get_doc("Asset", self.asset).set_status()
|
||||
@@ -170,11 +174,6 @@ class AssetRepair(AccountsController):
|
||||
def check_for_stock_items_and_warehouse(self):
|
||||
if not self.get("stock_items"):
|
||||
frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
|
||||
if not self.warehouse:
|
||||
frappe.throw(
|
||||
_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."),
|
||||
title=_("Missing Warehouse"),
|
||||
)
|
||||
|
||||
def increase_asset_value(self):
|
||||
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
|
||||
@@ -208,6 +207,7 @@ class AssetRepair(AccountsController):
|
||||
stock_entry = frappe.get_doc(
|
||||
{"doctype": "Stock Entry", "stock_entry_type": "Material Issue", "company": self.company}
|
||||
)
|
||||
stock_entry.asset_repair = self.name
|
||||
|
||||
for stock_item in self.get("stock_items"):
|
||||
self.validate_serial_no(stock_item)
|
||||
@@ -215,7 +215,7 @@ class AssetRepair(AccountsController):
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"s_warehouse": self.warehouse,
|
||||
"s_warehouse": stock_item.warehouse,
|
||||
"item_code": stock_item.item_code,
|
||||
"qty": stock_item.consumed_quantity,
|
||||
"basic_rate": stock_item.valuation_rate,
|
||||
@@ -228,8 +228,6 @@ class AssetRepair(AccountsController):
|
||||
stock_entry.insert()
|
||||
stock_entry.submit()
|
||||
|
||||
self.db_set("stock_entry", stock_entry.name)
|
||||
|
||||
def validate_serial_no(self, stock_item):
|
||||
if not stock_item.serial_and_batch_bundle and frappe.get_cached_value(
|
||||
"Item", stock_item.item_code, "has_serial_no"
|
||||
@@ -247,12 +245,6 @@ class AssetRepair(AccountsController):
|
||||
"Serial and Batch Bundle", stock_item.serial_and_batch_bundle, values_to_update
|
||||
)
|
||||
|
||||
def increase_stock_quantity(self):
|
||||
if self.stock_entry:
|
||||
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)
|
||||
stock_entry.flags.ignore_links = True
|
||||
stock_entry.cancel()
|
||||
|
||||
def make_gl_entries(self, cancel=False):
|
||||
if flt(self.total_repair_cost) > 0:
|
||||
gl_entries = self.get_gl_entries()
|
||||
@@ -316,7 +308,7 @@ class AssetRepair(AccountsController):
|
||||
return
|
||||
|
||||
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
|
||||
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)
|
||||
stock_entry = frappe.get_doc("Stock Entry", {"asset_repair": self.name})
|
||||
|
||||
default_expense_account = None
|
||||
if not erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
@@ -357,7 +349,7 @@ class AssetRepair(AccountsController):
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"against_voucher_type": "Stock Entry",
|
||||
"against_voucher": self.stock_entry,
|
||||
"against_voucher": stock_entry.name,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -377,7 +369,7 @@ class AssetRepair(AccountsController):
|
||||
def calculate_last_schedule_date(self, asset, row, extra_months):
|
||||
asset.flags.increase_in_asset_life = True
|
||||
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||
asset.number_of_depreciations_booked
|
||||
asset.opening_number_of_booked_depreciations
|
||||
)
|
||||
|
||||
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
||||
@@ -410,7 +402,7 @@ class AssetRepair(AccountsController):
|
||||
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
|
||||
asset.flags.increase_in_asset_life = True
|
||||
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||
asset.number_of_depreciations_booked
|
||||
asset.opening_number_of_booked_depreciations
|
||||
)
|
||||
|
||||
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
||||
|
||||
@@ -76,14 +76,14 @@ class TestAssetRepair(unittest.TestCase):
|
||||
def test_warehouse(self):
|
||||
asset_repair = create_asset_repair(stock_consumption=1)
|
||||
self.assertTrue(asset_repair.stock_consumption)
|
||||
self.assertTrue(asset_repair.warehouse)
|
||||
self.assertTrue(asset_repair.stock_items[0].warehouse)
|
||||
|
||||
def test_decrease_stock_quantity(self):
|
||||
asset_repair = create_asset_repair(stock_consumption=1, submit=1)
|
||||
stock_entry = frappe.get_last_doc("Stock Entry")
|
||||
|
||||
self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
|
||||
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
|
||||
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.stock_items[0].warehouse)
|
||||
self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code)
|
||||
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
|
||||
|
||||
@@ -114,14 +114,14 @@ class TestAssetRepair(unittest.TestCase):
|
||||
asset_repair.repair_status = "Completed"
|
||||
self.assertRaises(frappe.ValidationError, asset_repair.submit)
|
||||
|
||||
def test_increase_in_asset_value_due_to_stock_consumption(self):
|
||||
def test_no_increase_in_asset_value_when_not_capitalized(self):
|
||||
asset = create_asset(calculate_depreciation=1, submit=1)
|
||||
initial_asset_value = get_asset_value_after_depreciation(asset.name)
|
||||
asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1)
|
||||
create_asset_repair(asset=asset, stock_consumption=1, submit=1)
|
||||
asset.reload()
|
||||
|
||||
increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value
|
||||
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
|
||||
self.assertEqual(increase_in_asset_value, 0)
|
||||
|
||||
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
|
||||
asset = create_asset(calculate_depreciation=1, submit=1)
|
||||
@@ -185,7 +185,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
frappe.get_doc("Purchase Invoice", asset_repair.purchase_invoice).items[0].expense_account
|
||||
)
|
||||
stock_entry_expense_account = (
|
||||
frappe.get_doc("Stock Entry", asset_repair.stock_entry).get("items")[0].expense_account
|
||||
frappe.get_doc("Stock Entry", {"asset_repair": asset_repair.name}).get("items")[0].expense_account
|
||||
)
|
||||
|
||||
expected_values = {
|
||||
@@ -260,6 +260,12 @@ class TestAssetRepair(unittest.TestCase):
|
||||
asset.finance_books[0].value_after_depreciation,
|
||||
)
|
||||
|
||||
def test_asset_repiar_link_in_stock_entry(self):
|
||||
asset = create_asset(calculate_depreciation=1, submit=1)
|
||||
asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1)
|
||||
stock_entry = frappe.get_last_doc("Stock Entry")
|
||||
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
|
||||
|
||||
|
||||
def num_of_depreciations(asset):
|
||||
return asset.finance_books[0].total_number_of_depreciations
|
||||
@@ -289,7 +295,7 @@ def create_asset_repair(**args):
|
||||
|
||||
if args.stock_consumption:
|
||||
asset_repair.stock_consumption = 1
|
||||
asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company=asset.company)
|
||||
warehouse = args.warehouse or create_warehouse("Test Warehouse", company=asset.company)
|
||||
|
||||
bundle = None
|
||||
if args.serial_no:
|
||||
@@ -297,8 +303,8 @@ def create_asset_repair(**args):
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": args.item_code,
|
||||
"warehouse": asset_repair.warehouse,
|
||||
"company": frappe.get_cached_value("Warehouse", asset_repair.warehouse, "company"),
|
||||
"warehouse": warehouse,
|
||||
"company": frappe.get_cached_value("Warehouse", warehouse, "company"),
|
||||
"qty": (flt(args.stock_qty) or 1) * -1,
|
||||
"voucher_type": "Asset Repair",
|
||||
"type_of_transaction": "Asset Repair",
|
||||
@@ -314,6 +320,7 @@ def create_asset_repair(**args):
|
||||
"stock_items",
|
||||
{
|
||||
"item_code": args.item_code or "_Test Stock Item",
|
||||
"warehouse": warehouse,
|
||||
"valuation_rate": args.rate if args.get("rate") is not None else 100,
|
||||
"consumed_quantity": args.qty or 1,
|
||||
"serial_and_batch_bundle": bundle,
|
||||
@@ -333,7 +340,7 @@ def create_asset_repair(**args):
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"t_warehouse": asset_repair.warehouse,
|
||||
"t_warehouse": asset_repair.stock_items[0].warehouse,
|
||||
"item_code": asset_repair.stock_items[0].item_code,
|
||||
"qty": asset_repair.stock_items[0].consumed_quantity,
|
||||
"basic_rate": args.rate if args.get("rate") is not None else 100,
|
||||
@@ -351,7 +358,7 @@ def create_asset_repair(**args):
|
||||
company=asset.company,
|
||||
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
|
||||
cost_center=asset_repair.cost_center,
|
||||
warehouse=asset_repair.warehouse,
|
||||
warehouse=args.warehouse or create_warehouse("Test Warehouse", company=asset.company),
|
||||
)
|
||||
asset_repair.purchase_invoice = pi.name
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"warehouse",
|
||||
"valuation_rate",
|
||||
"consumed_quantity",
|
||||
"total_value",
|
||||
@@ -44,19 +45,28 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item",
|
||||
"options": "Item"
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
"options": "Serial and Batch Bundle"
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-06 02:24:20.375870",
|
||||
"modified": "2024-06-13 12:01:47.147333",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair Consumed Item",
|
||||
|
||||
@@ -15,7 +15,7 @@ class AssetRepairConsumedItem(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
consumed_quantity: DF.Data | None
|
||||
item_code: DF.Link | None
|
||||
item_code: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
@@ -23,6 +23,7 @@ class AssetRepairConsumedItem(Document):
|
||||
serial_no: DF.SmallText | None
|
||||
total_value: DF.Currency
|
||||
valuation_rate: DF.Currency
|
||||
warehouse: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -125,9 +125,10 @@ def get_data(filters):
|
||||
if assets_linked_to_fb and asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(
|
||||
asset.asset_id, finance_book
|
||||
) or get_asset_value_after_depreciation(asset.asset_id)
|
||||
depreciation_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
asset_value = (
|
||||
asset.gross_purchase_amount - asset.opening_accumulated_depreciation - depreciation_amount
|
||||
)
|
||||
|
||||
row = {
|
||||
"asset_id": asset.asset_id,
|
||||
@@ -139,7 +140,7 @@ def get_data(filters):
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||
"depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
|
||||
"depreciated_amount": depreciation_amount,
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
"location": asset.location,
|
||||
"asset_category": asset.asset_category,
|
||||
@@ -185,11 +186,12 @@ def prepare_chart_data(data, filters):
|
||||
)
|
||||
|
||||
for d in data:
|
||||
date = d.get(date_field)
|
||||
belongs_to_month = formatdate(date, "MMM YYYY")
|
||||
if d.get(date_field):
|
||||
date = d.get(date_field)
|
||||
belongs_to_month = formatdate(date, "MMM YYYY")
|
||||
|
||||
labels_values_map[belongs_to_month].asset_value += d.get("asset_value")
|
||||
labels_values_map[belongs_to_month].depreciated_amount += d.get("depreciated_amount")
|
||||
labels_values_map[belongs_to_month].asset_value += d.get("asset_value")
|
||||
labels_values_map[belongs_to_month].depreciated_amount += d.get("depreciated_amount")
|
||||
|
||||
return {
|
||||
"data": {
|
||||
|
||||
@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
|
||||
|
||||
frappe.ui.form.on("Purchase Order", {
|
||||
setup: function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
frm.set_query("reserve_warehouse", "supplied_items", function () {
|
||||
return {
|
||||
|
||||
@@ -484,7 +484,13 @@ class PurchaseOrder(BuyingController):
|
||||
self.auto_create_subcontracting_order()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
|
||||
super().on_cancel()
|
||||
|
||||
if self.is_against_so():
|
||||
@@ -794,6 +800,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
"field_map": {
|
||||
"name": "po_detail",
|
||||
"parent": "purchase_order",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
|
||||
@@ -138,6 +138,7 @@ class Supplier(TransactionBase):
|
||||
validate_party_accounts(self)
|
||||
self.validate_internal_supplier()
|
||||
self.add_role_for_user()
|
||||
self.validate_currency_for_receivable_payable_and_advance_account()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_supplier_group_details(self):
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"doctype": "Supplier",
|
||||
"supplier_name": "_Test Supplier USD",
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
"default_currency": "USD",
|
||||
"accounts": [{
|
||||
"company": "_Test Company",
|
||||
"account": "_Test Payable USD - _TC"
|
||||
|
||||
@@ -1430,10 +1430,13 @@ class AccountsController(TransactionBase):
|
||||
if d.exchange_gain_loss and (
|
||||
(d.reference_doctype, d.reference_name, str(d.idx)) not in booked
|
||||
):
|
||||
if self.payment_type == "Receive":
|
||||
party_account = self.paid_from
|
||||
elif self.payment_type == "Pay":
|
||||
party_account = self.paid_to
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
party_account = d.account
|
||||
else:
|
||||
if self.payment_type == "Receive":
|
||||
party_account = self.paid_from
|
||||
elif self.payment_type == "Pay":
|
||||
party_account = self.paid_to
|
||||
|
||||
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
||||
|
||||
@@ -1761,8 +1764,8 @@ class AccountsController(TransactionBase):
|
||||
item_allowance = {}
|
||||
global_qty_allowance, global_amount_allowance = None, None
|
||||
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value(
|
||||
"Accounts Settings", "role_allowed_to_over_bill"
|
||||
role_allowed_to_over_bill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
user_roles = frappe.get_roles()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.contacts.doctype.address.address import render_address
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
@@ -332,6 +333,8 @@ class BuyingController(SubcontractingController):
|
||||
else:
|
||||
item.valuation_rate = 0.0
|
||||
|
||||
update_regional_item_valuation_rate(self)
|
||||
|
||||
def set_incoming_rate(self):
|
||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
|
||||
return
|
||||
@@ -656,10 +659,7 @@ class BuyingController(SubcontractingController):
|
||||
return
|
||||
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
field = "purchase_invoice" if self.doctype == "Purchase Invoice" else "purchase_receipt"
|
||||
|
||||
self.process_fixed_asset()
|
||||
self.update_fixed_asset(field)
|
||||
|
||||
if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
|
||||
"Buying Settings", "disable_last_purchase_rate"
|
||||
@@ -769,7 +769,7 @@ class BuyingController(SubcontractingController):
|
||||
if not row.asset_location:
|
||||
frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code))
|
||||
|
||||
item_data = frappe.db.get_value(
|
||||
item_data = frappe.get_cached_value(
|
||||
"Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1
|
||||
)
|
||||
asset_quantity = row.qty if is_grouped_asset else 1
|
||||
@@ -798,7 +798,7 @@ class BuyingController(SubcontractingController):
|
||||
asset.flags.ignore_validate = True
|
||||
asset.flags.ignore_mandatory = True
|
||||
asset.set_missing_values()
|
||||
asset.insert()
|
||||
asset.db_insert()
|
||||
|
||||
return asset.name
|
||||
|
||||
@@ -824,11 +824,7 @@ class BuyingController(SubcontractingController):
|
||||
frappe.delete_doc("Asset", asset.name, force=1)
|
||||
continue
|
||||
|
||||
if self.docstatus in [0, 1] and not asset.get(field):
|
||||
asset.set(field, self.name)
|
||||
asset.purchase_date = self.posting_date
|
||||
asset.supplier = self.supplier
|
||||
elif self.docstatus == 2:
|
||||
if self.docstatus == 2:
|
||||
if asset.docstatus == 2:
|
||||
continue
|
||||
if asset.docstatus == 0:
|
||||
@@ -935,3 +931,8 @@ def validate_item_type(doc, fieldname, message):
|
||||
).format(items, message)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def update_regional_item_valuation_rate(doc):
|
||||
pass
|
||||
|
||||
@@ -352,6 +352,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Batch"
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
page_len = 30
|
||||
|
||||
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
||||
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
|
||||
@@ -422,7 +423,7 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
||||
& (stock_ledger_entry.batch_no.isnotnull())
|
||||
)
|
||||
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
||||
.having(Sum(stock_ledger_entry.actual_qty) > 0)
|
||||
.having(Sum(stock_ledger_entry.actual_qty) != 0)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
@@ -473,7 +474,7 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
||||
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
||||
)
|
||||
.groupby(bundle.batch_no, bundle.warehouse)
|
||||
.having(Sum(bundle.qty) > 0)
|
||||
.having(Sum(bundle.qty) != 0)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
@@ -146,7 +146,10 @@ def validate_returned_items(doc):
|
||||
def validate_quantity(doc, args, ref, valid_items, already_returned_items):
|
||||
fields = ["stock_qty"]
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
|
||||
fields.extend(["received_qty", "rejected_qty"])
|
||||
if not args.get("return_qty_from_rejected_warehouse"):
|
||||
fields.extend(["received_qty", "rejected_qty"])
|
||||
else:
|
||||
fields.extend(["received_qty"])
|
||||
|
||||
already_returned_data = already_returned_items.get(args.item_code) or {}
|
||||
|
||||
@@ -158,9 +161,12 @@ def validate_quantity(doc, 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
|
||||
|
||||
if column == "stock_qty":
|
||||
if column == "stock_qty" and not args.get("return_qty_from_rejected_warehouse"):
|
||||
reference_qty = ref.get(column)
|
||||
current_stock_qty = args.get(column)
|
||||
elif args.get("return_qty_from_rejected_warehouse"):
|
||||
reference_qty = ref.get("rejected_qty") * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
else:
|
||||
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
@@ -883,6 +889,9 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||
|
||||
if row.voucher_type in ["Sales Invoice", "Delivery Note"]:
|
||||
row.qty = -1 * row.qty
|
||||
|
||||
if key not in available_dict:
|
||||
available_dict[key] = frappe._dict(
|
||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||
@@ -936,6 +945,7 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
if is_rejected:
|
||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||
|
||||
del filters["rejected_serial_and_batch_bundle"]
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
@@ -943,6 +953,9 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if not d.get("serial_and_batch_bundle") and not d.get("rejected_serial_and_batch_bundle"):
|
||||
continue
|
||||
|
||||
if is_rejected:
|
||||
if d.get("return_qty_from_rejected_warehouse"):
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
@@ -1027,7 +1040,7 @@ def get_available_batch_qty(parent_doc, batch_no, warehouse):
|
||||
)
|
||||
|
||||
|
||||
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None):
|
||||
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None, qty_field=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
type_of_transaction = "Outward"
|
||||
@@ -1037,11 +1050,21 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
warehouse = child_doc.get("target_warehouse")
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
if not child_doc.get(qty_field):
|
||||
frappe.throw(
|
||||
_("For the {0}, the quantity is required to make the return entry").format(
|
||||
frappe.bold(child_doc.item_code)
|
||||
)
|
||||
)
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
@@ -1054,7 +1077,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
"qty": child_doc.qty,
|
||||
"qty": child_doc.get(qty_field),
|
||||
"company": parent_doc.company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
|
||||
@@ -554,6 +554,7 @@ class StatusUpdater(Document):
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_allowance_for(
|
||||
item_code,
|
||||
item_allowance=None,
|
||||
@@ -583,20 +584,20 @@ def get_allowance_for(
|
||||
global_amount_allowance,
|
||||
)
|
||||
|
||||
qty_allowance, over_billing_allowance = frappe.db.get_value(
|
||||
qty_allowance, over_billing_allowance = frappe.get_cached_value(
|
||||
"Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"]
|
||||
)
|
||||
|
||||
if qty_or_amount == "qty" and not qty_allowance:
|
||||
if global_qty_allowance is None:
|
||||
global_qty_allowance = flt(
|
||||
frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")
|
||||
frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance")
|
||||
)
|
||||
qty_allowance = global_qty_allowance
|
||||
elif qty_or_amount == "amount" and not over_billing_allowance:
|
||||
if global_amount_allowance is None:
|
||||
global_amount_allowance = flt(
|
||||
frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
frappe.get_cached_value("Accounts Settings", None, "over_billing_allowance")
|
||||
)
|
||||
over_billing_allowance = global_amount_allowance
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ class StockController(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False, via_landed_cost_voucher=False):
|
||||
if self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
@@ -118,7 +118,11 @@ class StockController(AccountsController):
|
||||
|
||||
if self.docstatus == 1:
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries(warehouse_account)
|
||||
gl_entries = (
|
||||
self.get_gl_entries(warehouse_account, via_landed_cost_voucher)
|
||||
if self.doctype == "Purchase Receipt"
|
||||
else self.get_gl_entries(warehouse_account)
|
||||
)
|
||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||
|
||||
def validate_serialized_batch(self):
|
||||
@@ -255,10 +259,17 @@ class StockController(AccountsController):
|
||||
qty_field = "qty"
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
if not data.get("qty"):
|
||||
frappe.throw(
|
||||
_("For the {0}, no stock is available for the return in the warehouse {1}.").format(
|
||||
frappe.bold(row.item_code), row.get(warehouse_field)
|
||||
)
|
||||
)
|
||||
|
||||
data = filter_serial_batches(
|
||||
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
|
||||
)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field, qty_field)
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
row.db_set(
|
||||
{
|
||||
|
||||
@@ -294,16 +294,23 @@ class SubcontractingController(StockController):
|
||||
receipt_items = {item.name: item.get(self.subcontract_data.order_field) for item in receipt_items}
|
||||
consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys())
|
||||
|
||||
voucher_nos = [d.voucher_no for d in consumed_materials if d.voucher_no]
|
||||
voucher_bundle_data = get_voucher_wise_serial_batch_from_bundle(
|
||||
voucher_no=voucher_nos,
|
||||
is_outward=1,
|
||||
get_subcontracted_item=("Subcontracting Receipt Supplied Item", "main_item_code"),
|
||||
)
|
||||
|
||||
if return_consumed_items:
|
||||
return (consumed_materials, receipt_items)
|
||||
|
||||
if not consumed_materials:
|
||||
return
|
||||
|
||||
voucher_nos = [d.voucher_no for d in consumed_materials if d.voucher_no]
|
||||
voucher_bundle_data = (
|
||||
get_voucher_wise_serial_batch_from_bundle(
|
||||
voucher_no=voucher_nos,
|
||||
is_outward=1,
|
||||
get_subcontracted_item=("Subcontracting Receipt Supplied Item", "main_item_code"),
|
||||
)
|
||||
if voucher_nos
|
||||
else {}
|
||||
)
|
||||
|
||||
for row in consumed_materials:
|
||||
key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name))
|
||||
if not self.available_materials.get(key):
|
||||
@@ -350,10 +357,14 @@ class SubcontractingController(StockController):
|
||||
transferred_items = self.__get_transferred_items()
|
||||
|
||||
voucher_nos = [row.voucher_no for row in transferred_items]
|
||||
voucher_bundle_data = get_voucher_wise_serial_batch_from_bundle(
|
||||
voucher_no=voucher_nos,
|
||||
is_outward=0,
|
||||
get_subcontracted_item=("Stock Entry Detail", "subcontracted_item"),
|
||||
voucher_bundle_data = (
|
||||
get_voucher_wise_serial_batch_from_bundle(
|
||||
voucher_no=voucher_nos,
|
||||
is_outward=0,
|
||||
get_subcontracted_item=("Stock Entry Detail", "subcontracted_item"),
|
||||
)
|
||||
if voucher_nos
|
||||
else {}
|
||||
)
|
||||
|
||||
for row in transferred_items:
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.utils import add_days, getdate, nowdate
|
||||
|
||||
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
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
@@ -55,6 +56,7 @@ class TestAccountsController(FrappeTestCase):
|
||||
40 series - Company default Cost center is unset
|
||||
50 series - Journals against Journals
|
||||
60 series - Journals against Payment Entries
|
||||
70 series - Advances in Separate party account. Both Party and Advance account are in Foreign currency.
|
||||
90 series - Dimension inheritence
|
||||
"""
|
||||
|
||||
@@ -114,47 +116,102 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.supplier = make_supplier("_Test MC Supplier USD", "USD")
|
||||
|
||||
def create_account(self):
|
||||
account_name = "Debtors USD"
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": account_name, "company": self.company}
|
||||
):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = account_name
|
||||
acc.parent_account = "Accounts Receivable - " + self.company_abbr
|
||||
acc.company = self.company
|
||||
acc.account_currency = "USD"
|
||||
acc.account_type = "Receivable"
|
||||
acc.insert()
|
||||
else:
|
||||
name = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": account_name, "company": self.company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
acc = frappe.get_doc("Account", name)
|
||||
self.debtors_usd = acc.name
|
||||
accounts = [
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "debtors_usd",
|
||||
"name": "Debtors USD",
|
||||
"account_type": "Receivable",
|
||||
"account_currency": "USD",
|
||||
"parent_account": "Accounts Receivable - " + self.company_abbr,
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "creditors_usd",
|
||||
"name": "Creditors USD",
|
||||
"account_type": "Payable",
|
||||
"account_currency": "USD",
|
||||
"parent_account": "Accounts Payable - " + self.company_abbr,
|
||||
}
|
||||
),
|
||||
# Advance accounts under Asset and Liability header
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "advance_received_usd",
|
||||
"name": "Advance Received USD",
|
||||
"account_type": "Receivable",
|
||||
"account_currency": "USD",
|
||||
"parent_account": "Current Liabilities - " + self.company_abbr,
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "advance_paid_usd",
|
||||
"name": "Advance Paid USD",
|
||||
"account_type": "Payable",
|
||||
"account_currency": "USD",
|
||||
"parent_account": "Current Assets - " + self.company_abbr,
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
account_name = "Creditors USD"
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": account_name, "company": self.company}
|
||||
):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = account_name
|
||||
acc.parent_account = "Accounts Payable - " + self.company_abbr
|
||||
acc.company = self.company
|
||||
acc.account_currency = "USD"
|
||||
acc.account_type = "Payable"
|
||||
acc.insert()
|
||||
else:
|
||||
name = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": account_name, "company": self.company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
acc = frappe.get_doc("Account", name)
|
||||
self.creditors_usd = acc.name
|
||||
for x in accounts:
|
||||
if not frappe.db.get_value("Account", filters={"account_name": x.name, "company": self.company}):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = x.name
|
||||
acc.parent_account = x.parent_account
|
||||
acc.company = self.company
|
||||
acc.account_currency = x.account_currency
|
||||
acc.account_type = x.account_type
|
||||
acc.insert()
|
||||
else:
|
||||
name = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": x.name, "company": self.company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
acc = frappe.get_doc("Account", name)
|
||||
setattr(self, x.attribute_name, acc.name)
|
||||
|
||||
def setup_advance_accounts_in_party_master(self):
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.book_advance_payments_in_separate_party_account = 1
|
||||
company.save()
|
||||
|
||||
customer = frappe.get_doc("Customer", self.customer)
|
||||
customer.append(
|
||||
"accounts",
|
||||
{
|
||||
"company": self.company,
|
||||
"account": self.debtors_usd,
|
||||
"advance_account": self.advance_received_usd,
|
||||
},
|
||||
)
|
||||
customer.save()
|
||||
|
||||
supplier = frappe.get_doc("Supplier", self.supplier)
|
||||
supplier.append(
|
||||
"accounts",
|
||||
{
|
||||
"company": self.company,
|
||||
"account": self.creditors_usd,
|
||||
"advance_account": self.advance_paid_usd,
|
||||
},
|
||||
)
|
||||
supplier.save()
|
||||
|
||||
def remove_advance_accounts_from_party_master(self):
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.book_advance_payments_in_separate_party_account = 0
|
||||
company.save()
|
||||
customer = frappe.get_doc("Customer", self.customer)
|
||||
customer.accounts = []
|
||||
customer.save()
|
||||
supplier = frappe.get_doc("Supplier", self.supplier)
|
||||
supplier.accounts = []
|
||||
supplier.save()
|
||||
|
||||
def create_sales_invoice(
|
||||
self,
|
||||
@@ -218,6 +275,48 @@ class TestAccountsController(FrappeTestCase):
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def create_purchase_invoice(
|
||||
self,
|
||||
qty=1,
|
||||
rate=1,
|
||||
conversion_rate=80,
|
||||
posting_date=None,
|
||||
do_not_save=False,
|
||||
do_not_submit=False,
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in purchase invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
pinv = make_purchase_invoice(
|
||||
posting_date=posting_date,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="USD",
|
||||
conversion_rate=conversion_rate,
|
||||
is_pos=0,
|
||||
is_return=0,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=True,
|
||||
)
|
||||
pinv.credit_to = self.creditors_usd
|
||||
if not do_not_save:
|
||||
pinv.save()
|
||||
if not do_not_submit:
|
||||
pinv.submit()
|
||||
return pinv
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
@@ -1698,3 +1797,123 @@ class TestAccountsController(FrappeTestCase):
|
||||
# Exchange Gain/Loss Journal should've been cancelled
|
||||
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||
self.assertEqual(exc_je_for_je1, [])
|
||||
|
||||
def test_70_advance_payment_against_sales_invoice_in_foreign_currency(self):
|
||||
"""
|
||||
Customer advance booked under Liability
|
||||
"""
|
||||
self.setup_advance_accounts_in_party_master()
|
||||
|
||||
adv = self.create_payment_entry(amount=1, source_exc_rate=83)
|
||||
adv.save() # explicit 'save' is needed to trigger set_liability_account()
|
||||
self.assertEqual(adv.paid_from, self.advance_received_usd)
|
||||
adv.submit()
|
||||
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True)
|
||||
si.debit_to = self.debtors_usd
|
||||
si.save().submit()
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.receivable_payable_account = self.debtors_usd
|
||||
pr.default_advance_account = self.advance_received_usd
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(pr.invoices[0].invoice_number, si.name)
|
||||
self.assertEqual(pr.payments[0].reference_name, adv.name)
|
||||
|
||||
# Allocate and Reconcile
|
||||
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)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exc Gain/Loss journal should've been creatad
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_adv), 1)
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_adv)
|
||||
|
||||
adv.reload()
|
||||
adv.cancel()
|
||||
si.reload()
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
# Exc Gain/Loss journal should've been cancelled
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(len(exc_je_for_si), 0)
|
||||
self.assertEqual(len(exc_je_for_adv), 0)
|
||||
|
||||
self.remove_advance_accounts_from_party_master()
|
||||
|
||||
def test_71_advance_payment_against_purchase_invoice_in_foreign_currency(self):
|
||||
"""
|
||||
Supplier advance booked under Asset
|
||||
"""
|
||||
self.setup_advance_accounts_in_party_master()
|
||||
|
||||
usd_amount = 1
|
||||
inr_amount = 85
|
||||
exc_rate = 85
|
||||
adv = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Pay",
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
paid_from=self.cash,
|
||||
paid_to=self.advance_paid_usd,
|
||||
paid_amount=inr_amount,
|
||||
)
|
||||
adv.source_exchange_rate = 1
|
||||
adv.target_exchange_rate = exc_rate
|
||||
adv.received_amount = usd_amount
|
||||
adv.paid_amount = exc_rate * usd_amount
|
||||
adv.posting_date = nowdate()
|
||||
adv.save()
|
||||
# Make sure that advance account is still set
|
||||
self.assertEqual(adv.paid_to, self.advance_paid_usd)
|
||||
adv.submit()
|
||||
|
||||
pi = self.create_purchase_invoice(qty=1, conversion_rate=83, rate=1)
|
||||
self.assertEqual(pi.credit_to, self.creditors_usd)
|
||||
self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0)
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party_type = "Supplier"
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.default_advance_account = self.advance_paid_usd
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(pr.invoices[0].invoice_number, pi.name)
|
||||
self.assertEqual(pr.payments[0].reference_name, adv.name)
|
||||
|
||||
# Allocate and Reconcile
|
||||
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)
|
||||
self.assert_ledger_outstanding(pi.doctype, pi.name, 0.0, 0.0)
|
||||
|
||||
# Exc Gain/Loss journal should've been creatad
|
||||
exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(len(exc_je_for_pi), 1)
|
||||
self.assertEqual(len(exc_je_for_adv), 1)
|
||||
self.assertEqual(exc_je_for_pi, exc_je_for_adv)
|
||||
|
||||
adv.reload()
|
||||
adv.cancel()
|
||||
pi.reload()
|
||||
self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0)
|
||||
# Exc Gain/Loss journal should've been cancelled
|
||||
exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(len(exc_je_for_pi), 0)
|
||||
self.assertEqual(len(exc_je_for_adv), 0)
|
||||
|
||||
self.remove_advance_accounts_from_party_master()
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
frappe.listview_settings["Lead"] = {
|
||||
get_indicator: function (doc) {
|
||||
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
||||
return indicator;
|
||||
},
|
||||
onload: function (listview) {
|
||||
if (frappe.boot.user.can_create.includes("Prospect")) {
|
||||
listview.page.add_action_item(__("Create Prospect"), function () {
|
||||
|
||||
@@ -442,6 +442,7 @@ scheduler_events = {
|
||||
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
|
||||
"erpnext.accounts.utils.run_ledger_health_checks",
|
||||
"erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status",
|
||||
],
|
||||
"weekly": [
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||
|
||||
@@ -1259,12 +1259,18 @@ def get_children(parent=None, is_root=False, **filters):
|
||||
def add_additional_cost(stock_entry, work_order):
|
||||
# Add non stock items cost in the additional cost
|
||||
stock_entry.additional_costs = []
|
||||
expenses_included_in_valuation = frappe.get_cached_value(
|
||||
"Company", work_order.company, "expenses_included_in_valuation"
|
||||
company_account = frappe.db.get_value(
|
||||
"Company",
|
||||
work_order.company,
|
||||
["expenses_included_in_valuation", "default_operating_cost_account"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
add_non_stock_items_cost(stock_entry, work_order, expenses_included_in_valuation)
|
||||
add_operations_cost(stock_entry, work_order, expenses_included_in_valuation)
|
||||
expense_account = (
|
||||
company_account.default_operating_cost_account or company_account.expenses_included_in_valuation
|
||||
)
|
||||
add_non_stock_items_cost(stock_entry, work_order, expense_account)
|
||||
add_operations_cost(stock_entry, work_order, expense_account)
|
||||
|
||||
|
||||
def add_non_stock_items_cost(stock_entry, work_order, expense_account):
|
||||
|
||||
@@ -212,7 +212,6 @@ erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionC
|
||||
item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item));
|
||||
refresh_field("stock_qty", item.name, item.parentfield);
|
||||
this.toggle_conversion_factor(item);
|
||||
this.frm.events.update_cost(this.frm);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Operation Time ",
|
||||
"label": "Operation Time",
|
||||
"oldfieldname": "time_in_mins",
|
||||
"oldfieldtype": "Currency",
|
||||
"reqd": 1
|
||||
@@ -203,4 +203,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,13 +400,20 @@ frappe.ui.form.on("Production Plan", {
|
||||
},
|
||||
|
||||
download_materials_required(frm) {
|
||||
const warehouses_data = [];
|
||||
|
||||
if (frm.doc.for_warehouse) {
|
||||
warehouses_data.push({ warehouse: frm.doc.for_warehouse });
|
||||
}
|
||||
|
||||
const fields = [
|
||||
{
|
||||
fieldname: "warehouses",
|
||||
fieldtype: "Table MultiSelect",
|
||||
label: __("Warehouses"),
|
||||
default: frm.doc.from_warehouse,
|
||||
default: warehouses_data,
|
||||
options: "Production Plan Material Request Warehouse",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -1748,6 +1748,81 @@ class TestWorkOrder(FrappeTestCase):
|
||||
job_card2.time_logs = []
|
||||
job_card2.save()
|
||||
|
||||
def test_operating_cost_account(self):
|
||||
operating_cost_account = "Test Operating Cost Account - _TC"
|
||||
company = "_Test Company"
|
||||
if not frappe.db.exists("Account", operating_cost_account):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "Test Operating Cost Account",
|
||||
"account_type": "Expense Account",
|
||||
"company": company,
|
||||
"parent_account": "Expenses - _TC",
|
||||
"root_type": "Expense",
|
||||
}
|
||||
).insert()
|
||||
|
||||
frappe.db.set_value("Company", company, "default_operating_cost_account", operating_cost_account)
|
||||
|
||||
for item in ["TEST RM OP COST Item 1", "TEST FG OP COST Item"]:
|
||||
if not frappe.db.exists("Item", item):
|
||||
make_item(item_code=item, properties={"is_stock_item": 1})
|
||||
|
||||
fg_item = "TEST FG OP COST Item"
|
||||
bom_doc = make_bom(
|
||||
item=fg_item,
|
||||
raw_materials=["TEST RM OP COST Item 1"],
|
||||
rate=150,
|
||||
with_operations=1,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
workstation = "Test Workstation For Capacity Planning 1"
|
||||
if not frappe.db.exists("Workstation", workstation):
|
||||
make_workstation(workstation=workstation, production_capacity=1)
|
||||
|
||||
operation = "Test Operation For Capacity Planning 1"
|
||||
if not frappe.db.exists("Operation", operation):
|
||||
make_operation(operation=operation, workstation=workstation)
|
||||
|
||||
bom_doc.append(
|
||||
"operations",
|
||||
{"operation": operation, "time_in_mins": 60, "hour_rate": 100, "workstation": workstation},
|
||||
)
|
||||
|
||||
bom_doc.save()
|
||||
bom_doc.submit()
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
production_item=fg_item,
|
||||
bom_no=bom_doc.name,
|
||||
qty=1,
|
||||
skip_transfer=1,
|
||||
)
|
||||
|
||||
job_cards = frappe.get_all("Job Card", filters={"work_order": wo.name})
|
||||
for job_card in job_cards:
|
||||
job_card_doc = frappe.get_doc("Job Card", job_card.name)
|
||||
job_card_doc.time_logs = []
|
||||
job_card_doc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": now(),
|
||||
"to_time": add_to_date(now(), minutes=60),
|
||||
"time_in_mins": 60,
|
||||
"completed_qty": 1,
|
||||
},
|
||||
)
|
||||
|
||||
job_card_doc.submit()
|
||||
|
||||
se_doc = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
|
||||
se_doc.save()
|
||||
|
||||
for row in se_doc.additional_costs:
|
||||
self.assertEqual(row.expense_account, operating_cost_account)
|
||||
|
||||
def test_op_cost_and_scrap_based_on_sub_assemblies(self):
|
||||
# Make Sub Assembly BOM 1
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
"column_break_3",
|
||||
"production_capacity",
|
||||
"warehouse",
|
||||
"production_capacity_section",
|
||||
"parts_per_hour",
|
||||
"workstation_status_tab",
|
||||
"status",
|
||||
"column_break_glcv",
|
||||
@@ -210,16 +208,6 @@
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "production_capacity_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Production Capacity"
|
||||
},
|
||||
{
|
||||
"fieldname": "parts_per_hour",
|
||||
"fieldtype": "Float",
|
||||
"label": "Parts Per Hour"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_working_hours",
|
||||
"fieldtype": "Float",
|
||||
@@ -252,7 +240,7 @@
|
||||
"idx": 1,
|
||||
"image_field": "on_status_image",
|
||||
"links": [],
|
||||
"modified": "2023-11-30 12:43:35.808845",
|
||||
"modified": "2024-06-20 14:17:13.806609",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Workstation",
|
||||
@@ -277,4 +265,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ class Workstation(Document):
|
||||
hour_rate_electricity: DF.Currency
|
||||
hour_rate_labour: DF.Currency
|
||||
hour_rate_rent: DF.Currency
|
||||
off_status_image: DF.AttachImage | None
|
||||
on_status_image: DF.AttachImage | None
|
||||
plant_floor: DF.Link | None
|
||||
production_capacity: DF.Int
|
||||
working_hours: DF.Table[WorkstationWorkingHour]
|
||||
workstation_name: DF.Data
|
||||
|
||||
@@ -365,3 +365,7 @@ erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||
erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations
|
||||
erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_doctype
|
||||
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
|
||||
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
|
||||
|
||||
@@ -9,15 +9,13 @@ def execute():
|
||||
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
records = (
|
||||
frappe.qb.from_(dt)
|
||||
.select(dt.name, dt.notes, dt.modified_by, dt.modified)
|
||||
.where(dt.notes.isnotnull() & dt.notes != "")
|
||||
frappe.qb.from_(dt).select(dt.name, dt.notes).where(dt.notes.isnotnull() & dt.notes != "")
|
||||
).run(as_dict=True)
|
||||
|
||||
for d in records:
|
||||
if strip_html(cstr(d.notes)).strip():
|
||||
doc = frappe.get_doc(doctype, d.name)
|
||||
doc.append("notes", {"note": d.notes, "added_by": d.modified_by, "added_on": d.modified})
|
||||
doc.append("notes", {"note": d.notes})
|
||||
doc.update_child_table("notes")
|
||||
|
||||
frappe.db.sql_ddl(f"alter table `tab{doctype}` drop column `notes`")
|
||||
|
||||
@@ -4,6 +4,7 @@ import frappe
|
||||
def execute():
|
||||
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
|
||||
frappe.reload_doc("assets", "doctype", "Asset Finance Book")
|
||||
frappe.reload_doc("assets", "doctype", "Asset")
|
||||
|
||||
assets = get_details_of_draft_or_submitted_depreciable_assets()
|
||||
|
||||
@@ -43,7 +44,7 @@ def get_details_of_draft_or_submitted_depreciable_assets():
|
||||
asset.name,
|
||||
asset.opening_accumulated_depreciation,
|
||||
asset.gross_purchase_amount,
|
||||
asset.number_of_depreciations_booked,
|
||||
asset.opening_number_of_booked_depreciations,
|
||||
asset.docstatus,
|
||||
)
|
||||
.where(asset.calculate_depreciation == 1)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.has_column("Asset", "number_of_depreciations_booked"):
|
||||
rename_field("Asset", "number_of_depreciations_booked", "opening_number_of_booked_depreciations")
|
||||
@@ -0,0 +1,15 @@
|
||||
import frappe
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.has_column("Asset Repair", "stock_entry"):
|
||||
AssetRepair = DocType("Asset Repair")
|
||||
StockEntry = DocType("Stock Entry")
|
||||
|
||||
(
|
||||
frappe.qb.update(StockEntry)
|
||||
.join(AssetRepair)
|
||||
.on(StockEntry.name == AssetRepair.stock_entry)
|
||||
.set(StockEntry.asset_repair, AssetRepair.name)
|
||||
).run()
|
||||
@@ -3,18 +3,19 @@ import frappe
|
||||
|
||||
def execute():
|
||||
# not using frappe.qb because https://github.com/frappe/frappe/issues/20292
|
||||
# nosemgrep
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabAsset Depreciation Schedule`
|
||||
JOIN `tabAsset`
|
||||
ON `tabAsset Depreciation Schedule`.`asset`=`tabAsset`.`name`
|
||||
SET
|
||||
`tabAsset Depreciation Schedule`.`gross_purchase_amount`=`tabAsset`.`gross_purchase_amount`,
|
||||
`tabAsset Depreciation Schedule`.`number_of_depreciations_booked`=`tabAsset`.`number_of_depreciations_booked`
|
||||
`tabAsset Depreciation Schedule`.`opening_number_of_booked_depreciations`=`tabAsset`.`opening_number_of_booked_depreciations`
|
||||
WHERE
|
||||
(
|
||||
`tabAsset Depreciation Schedule`.`gross_purchase_amount`<>`tabAsset`.`gross_purchase_amount`
|
||||
OR
|
||||
`tabAsset Depreciation Schedule`.`number_of_depreciations_booked`<>`tabAsset`.`number_of_depreciations_booked`
|
||||
`tabAsset Depreciation Schedule`.`opening_number_of_booked_depreciations`<>`tabAsset`.`opening_number_of_booked_depreciations`
|
||||
)
|
||||
AND `tabAsset Depreciation Schedule`.`docstatus`<2"""
|
||||
)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_depr_schedule,
|
||||
)
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.has_column("Asset Finance Book", "total_number_of_booked_depreciations"):
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"docstatus": 1}, fields=["name", "opening_number_of_booked_depreciations"]
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
asset_doc = frappe.get_doc("Asset", asset.name)
|
||||
|
||||
for fb_row in asset_doc.get("finance_books"):
|
||||
depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book)
|
||||
total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations or 0
|
||||
|
||||
if depr_schedule:
|
||||
for je in depr_schedule:
|
||||
if je.journal_entry:
|
||||
total_number_of_booked_depreciations += 1
|
||||
frappe.db.set_value(
|
||||
"Asset Finance Book",
|
||||
fb_row.name,
|
||||
"total_number_of_booked_depreciations",
|
||||
total_number_of_booked_depreciations,
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
import frappe
|
||||
|
||||
|
||||
# not able to use frappe.qb because of this bug https://github.com/frappe/frappe/issues/20292
|
||||
def execute():
|
||||
if frappe.db.has_column("Asset Repair", "warehouse"):
|
||||
# nosemgrep
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabAsset Repair Consumed Item` ar_item
|
||||
JOIN `tabAsset Repair` ar
|
||||
ON ar.name = ar_item.parent
|
||||
SET ar_item.warehouse = ar.warehouse
|
||||
WHERE ifnull(ar.warehouse, '') != ''"""
|
||||
)
|
||||
@@ -325,7 +325,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
const me = this;
|
||||
if (!this.frm.is_new() && this.frm.doc.docstatus === 0) {
|
||||
if (!this.frm.is_new() && this.frm.doc.docstatus === 0 && frappe.model.can_create("Quality Inspection")) {
|
||||
this.frm.add_custom_button(__("Quality Inspection(s)"), () => {
|
||||
me.make_quality_inspection();
|
||||
}, __("Create"));
|
||||
@@ -1733,6 +1733,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
me.frm.doc.items.forEach(d => {
|
||||
if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on])) {
|
||||
for(var k in data) {
|
||||
if (data.pricing_rule_for == "Discount Percentage" && data.apply_rule_on_other_items && k == "discount_amount") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'Price' || k === 'pricing_rules')) {
|
||||
frappe.model.set_value(d.doctype, d.name, k, data[k]);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{% for(var i=0, l=notes.length; i<l; i++) { %}
|
||||
<div class="comment-content p-3 row" name="{{ notes[i].name }}">
|
||||
<div class="mb-2 head col-xs-3">
|
||||
{% if (notes[i].added_by && notes[i].added_on) %}
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
{{ frappe.avatar(notes[i].added_by) }}
|
||||
@@ -25,6 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="content col-xs-8">
|
||||
{{ notes[i].note }}
|
||||
|
||||
@@ -933,7 +933,13 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
frappe.msgprint(__("Please select {0}", [opts.source_doctype]));
|
||||
return;
|
||||
}
|
||||
opts.source_name = values;
|
||||
|
||||
if (values.constructor === Array) {
|
||||
opts.source_name = [...new Set(values)];
|
||||
} else {
|
||||
opts.source_name = values;
|
||||
}
|
||||
|
||||
if (
|
||||
opts.allow_child_item_selection ||
|
||||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
|
||||
|
||||
@@ -635,6 +635,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
set_data(data) {
|
||||
data.forEach((d) => {
|
||||
d.qty = Math.abs(d.qty);
|
||||
d.name = d.child_row || d.name;
|
||||
this.dialog.fields_dict.entries.df.data.push(d);
|
||||
});
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ class Customer(TransactionBase):
|
||||
self.validate_default_bank_account()
|
||||
self.validate_internal_customer()
|
||||
self.add_role_for_user()
|
||||
self.validate_currency_for_receivable_payable_and_advance_account()
|
||||
|
||||
# set loyalty program tier
|
||||
if frappe.db.exists("Customer", self.name):
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"customer_type": "Individual",
|
||||
"doctype": "Customer",
|
||||
"territory": "_Test Territory",
|
||||
"default_currency": "USD",
|
||||
"accounts": [{
|
||||
"company": "_Test Company",
|
||||
"account": "_Test Receivable USD - _TC"
|
||||
|
||||
@@ -58,7 +58,8 @@ frappe.ui.form.on("Sales Order", {
|
||||
if (
|
||||
frm.doc.status !== "Closed" &&
|
||||
flt(frm.doc.per_delivered, 2) < 100 &&
|
||||
flt(frm.doc.per_billed, 2) < 100
|
||||
flt(frm.doc.per_billed, 2) < 100 &&
|
||||
frm.has_perm("write")
|
||||
) {
|
||||
frm.add_custom_button(__("Update Items"), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
@@ -85,7 +86,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
}
|
||||
|
||||
// Stock Reservation > Unreserve button will be only visible if the SO has un-delivered reserved stock.
|
||||
if (frm.doc.__onload && frm.doc.__onload.has_reserved_stock) {
|
||||
if (
|
||||
frm.doc.__onload &&
|
||||
frm.doc.__onload.has_reserved_stock &&
|
||||
frappe.model.can_cancel("Stock Reservation Entry")
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Unreserve"),
|
||||
() => frm.events.cancel_stock_reservation_entries(frm),
|
||||
@@ -94,7 +99,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
}
|
||||
|
||||
frm.doc.items.forEach((item) => {
|
||||
if (flt(item.stock_reserved_qty) > 0) {
|
||||
if (flt(item.stock_reserved_qty) > 0 && frappe.model.can_read("Stock Reservation Entry")) {
|
||||
frm.add_custom_button(
|
||||
__("Reserved Stock"),
|
||||
() => frm.events.show_reserved_stock(frm),
|
||||
@@ -142,6 +147,10 @@ frappe.ui.form.on("Sales Order", {
|
||||
},
|
||||
|
||||
get_items_from_internal_purchase_order(frm) {
|
||||
if (!frappe.model.can_read("Purchase Order")) {
|
||||
return;
|
||||
}
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Purchase Order"),
|
||||
() => {
|
||||
@@ -217,7 +226,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.set_value("advance_paid", 0);
|
||||
}
|
||||
|
||||
frm.ignore_doctypes_on_cancel_all = ["Purchase Order"];
|
||||
frm.ignore_doctypes_on_cancel_all = [
|
||||
"Purchase Order",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
];
|
||||
},
|
||||
|
||||
delivery_date: function (frm) {
|
||||
@@ -630,15 +643,17 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc.__onload || !doc.__onload.has_reserved_stock) {
|
||||
// Don't show the `Reserve` button if the Sales Order has Picked Items.
|
||||
if (flt(doc.per_picked, 2) < 100 && flt(doc.per_delivered, 2) < 100) {
|
||||
this.frm.add_custom_button(
|
||||
__("Pick List"),
|
||||
() => this.create_pick_list(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
if (
|
||||
(!doc.__onload || !doc.__onload.has_reserved_stock) &&
|
||||
flt(doc.per_picked, 2) < 100 &&
|
||||
flt(doc.per_delivered, 2) < 100 &&
|
||||
frappe.model.can_create("Pick List")
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Pick List"),
|
||||
() => this.create_pick_list(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1;
|
||||
@@ -653,20 +668,25 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
(order_is_a_sale || order_is_a_custom_sale) &&
|
||||
allow_delivery
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
() => this.make_delivery_note_based_on_delivery_date(true),
|
||||
__("Create")
|
||||
);
|
||||
this.frm.add_custom_button(
|
||||
__("Work Order"),
|
||||
() => this.make_work_order(),
|
||||
__("Create")
|
||||
);
|
||||
if (frappe.model.can_create("Delivery Note")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
() => this.make_delivery_note_based_on_delivery_date(true),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
if (frappe.model.can_create("Work Order")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Work Order"),
|
||||
() => this.make_work_order(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// sales invoice
|
||||
if (flt(doc.per_billed, 2) < 100) {
|
||||
if (flt(doc.per_billed, 2) < 100 && frappe.model.can_create("Sales Invoice")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Sales Invoice"),
|
||||
() => me.make_sales_invoice(),
|
||||
@@ -676,8 +696,10 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
|
||||
// material request
|
||||
if (
|
||||
!doc.order_type ||
|
||||
((order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered, 2) < 100)
|
||||
(!doc.order_type ||
|
||||
((order_is_a_sale || order_is_a_custom_sale) &&
|
||||
flt(doc.per_delivered, 2) < 100)) &&
|
||||
frappe.model.can_create("Material Request")
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
@@ -692,7 +714,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
}
|
||||
|
||||
// Make Purchase Order
|
||||
if (!this.frm.doc.is_internal_customer) {
|
||||
if (!this.frm.doc.is_internal_customer && frappe.model.can_create("Purchase Order")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Purchase Order"),
|
||||
() => this.make_purchase_order(),
|
||||
@@ -702,24 +724,32 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
|
||||
// maintenance
|
||||
if (flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Maintenance Visit"),
|
||||
() => this.make_maintenance_visit(),
|
||||
__("Create")
|
||||
);
|
||||
this.frm.add_custom_button(
|
||||
__("Maintenance Schedule"),
|
||||
() => this.make_maintenance_schedule(),
|
||||
__("Create")
|
||||
);
|
||||
if (frappe.model.can_create("Maintenance Visit")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Maintenance Visit"),
|
||||
() => this.make_maintenance_visit(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
if (frappe.model.can_create("Maintenance Schedule")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Maintenance Schedule"),
|
||||
() => this.make_maintenance_schedule(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// project
|
||||
if (flt(doc.per_delivered, 2) < 100) {
|
||||
if (flt(doc.per_delivered, 2) < 100 && frappe.model.can_create("Project")) {
|
||||
this.frm.add_custom_button(__("Project"), () => this.make_project(), __("Create"));
|
||||
}
|
||||
|
||||
if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
|
||||
if (
|
||||
doc.docstatus === 1 &&
|
||||
!doc.inter_company_order_reference &&
|
||||
frappe.model.can_create("Purchase Order")
|
||||
) {
|
||||
let me = this;
|
||||
let internal = me.frm.doc.is_internal_customer;
|
||||
if (internal) {
|
||||
@@ -748,13 +778,20 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
() => this.make_payment_request(),
|
||||
__("Create")
|
||||
);
|
||||
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
|
||||
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
() => this.make_payment_entry(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.frm.doc.docstatus === 0) {
|
||||
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Quotation")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Quotation"),
|
||||
function () {
|
||||
|
||||
@@ -421,7 +421,13 @@ class SalesOrder(SellingController):
|
||||
self.create_stock_reservation_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
super().on_cancel()
|
||||
|
||||
# Cannot cancel closed SO
|
||||
@@ -490,6 +496,10 @@ class SalesOrder(SellingController):
|
||||
def update_status(self, status):
|
||||
self.check_modified_date()
|
||||
self.set_status(update=True, status=status)
|
||||
# Upon Sales Order Re-open, check for credit limit.
|
||||
# Limit should be checked after the 'Hold/Closed' status is reset.
|
||||
if status == "Draft" and self.docstatus == 1:
|
||||
self.check_credit_limit()
|
||||
self.update_reserved_qty()
|
||||
self.notify_update()
|
||||
clear_doctype_notifications(self)
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
||||
make_maintenance_schedule,
|
||||
@@ -31,7 +32,7 @@ from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
class TestSalesOrder(FrappeTestCase):
|
||||
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
@@ -49,6 +50,9 @@ class TestSalesOrder(FrappeTestCase):
|
||||
)
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.create_customer("_Test Customer Credit")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -2086,6 +2090,28 @@ class TestSalesOrder(FrappeTestCase):
|
||||
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||
|
||||
def test_credit_limit_on_so_reopning(self):
|
||||
# set credit limit
|
||||
company = "_Test Company"
|
||||
customer = frappe.get_doc("Customer", self.customer)
|
||||
customer.credit_limits = []
|
||||
customer.append(
|
||||
"credit_limits", {"company": company, "credit_limit": 1000, "bypass_credit_limit_check": False}
|
||||
)
|
||||
customer.save()
|
||||
|
||||
so1 = make_sales_order(qty=9, rate=100, do_not_submit=True)
|
||||
so1.customer = self.customer
|
||||
so1.save().submit()
|
||||
|
||||
so1.update_status("Closed")
|
||||
|
||||
so2 = make_sales_order(qty=9, rate=100, do_not_submit=True)
|
||||
so2.customer = self.customer
|
||||
so2.save().submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
||||
|
||||
|
||||
def automatically_fetch_payment_terms(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
|
||||
@@ -857,7 +857,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
});
|
||||
this.$customer_section.find(".customer-details").html(
|
||||
`<div class="header">
|
||||
<div class="label">Contact Details</div>
|
||||
<div class="label">${__("Contact Details")}</div>
|
||||
<div class="close-details-btn">
|
||||
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
|
||||
@@ -877,7 +877,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="loyalty_program-field"></div>
|
||||
<div class="loyalty_points-field"></div>
|
||||
</div>
|
||||
<div class="transactions-label">Recent Transactions</div>`
|
||||
<div class="transactions-label">${__("Recent Transactions")}</div>`
|
||||
);
|
||||
// transactions need to be in diff div from sticky elem for scrolling
|
||||
this.$customer_section.append(`<div class="customer-transactions"></div>`);
|
||||
|
||||
@@ -28,6 +28,12 @@ frappe.ui.form.on("Company", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("default_operating_cost_account", function (doc) {
|
||||
return {
|
||||
filters: { company: doc.name, root_type: "Expense" },
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("default_selling_terms", function () {
|
||||
return { filters: { selling: 1 } };
|
||||
});
|
||||
|
||||
@@ -109,6 +109,8 @@
|
||||
"stock_received_but_not_billed",
|
||||
"default_provisional_account",
|
||||
"expenses_included_in_valuation",
|
||||
"manufacturing_section",
|
||||
"default_operating_cost_account",
|
||||
"dashboard_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -773,7 +775,7 @@
|
||||
{
|
||||
"fieldname": "stock_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Stock"
|
||||
"label": "Stock and Manufacturing"
|
||||
},
|
||||
{
|
||||
"fieldname": "dashboard_tab",
|
||||
@@ -788,6 +790,17 @@
|
||||
"fieldname": "reconcile_on_advance_payment_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Reconcile on Advance Payment Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "manufacturing_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Manufacturing"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_operating_cost_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Operating Cost Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-building",
|
||||
@@ -795,7 +808,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-27 17:32:49.057386",
|
||||
"modified": "2024-06-21 17:46:25.567565",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
@@ -862,4 +875,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class Company(NestedSet):
|
||||
default_income_account: DF.Link | None
|
||||
default_inventory_account: DF.Link | None
|
||||
default_letter_head: DF.Link | None
|
||||
default_operating_cost_account: DF.Link | None
|
||||
default_payable_account: DF.Link | None
|
||||
default_provisional_account: DF.Link | None
|
||||
default_receivable_account: DF.Link | None
|
||||
|
||||
@@ -38,6 +38,53 @@ class CustomerGroup(NestedSet):
|
||||
def validate(self):
|
||||
if not self.parent_customer_group:
|
||||
self.parent_customer_group = get_root_of("Customer Group")
|
||||
self.validate_currency_for_receivable_and_advance_account()
|
||||
|
||||
def validate_currency_for_receivable_and_advance_account(self):
|
||||
for x in self.accounts:
|
||||
company_default_currency = frappe.get_cached_value("Company", x.company, "default_currency")
|
||||
receivable_account_currency = None
|
||||
advance_account_currency = None
|
||||
|
||||
if x.account:
|
||||
receivable_account_currency = frappe.get_cached_value(
|
||||
"Account", x.account, "account_currency"
|
||||
)
|
||||
|
||||
if x.advance_account:
|
||||
advance_account_currency = frappe.get_cached_value(
|
||||
"Account", x.advance_account, "account_currency"
|
||||
)
|
||||
|
||||
if receivable_account_currency and receivable_account_currency != company_default_currency:
|
||||
frappe.throw(
|
||||
_("Receivable Account: {0} must be in Company default currency: {1}").format(
|
||||
frappe.bold(x.account),
|
||||
frappe.bold(company_default_currency),
|
||||
)
|
||||
)
|
||||
|
||||
if advance_account_currency and advance_account_currency != company_default_currency:
|
||||
frappe.throw(
|
||||
_("Advance Account: {0} must be in Company default currency: {1}").format(
|
||||
frappe.bold(x.advance_account), frappe.bold(company_default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
receivable_account_currency
|
||||
and advance_account_currency
|
||||
and receivable_account_currency != advance_account_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Both Receivable Account: {0} and Advance Account: {1} must be of same currency for company: {2}"
|
||||
).format(
|
||||
frappe.bold(x.account),
|
||||
frappe.bold(x.advance_account),
|
||||
frappe.bold(x.company),
|
||||
)
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
self.validate_name_with_customer()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user