mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-22 18:49:40 +00:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a95c9d642 | ||
|
|
f6707b2b92 | ||
|
|
62dc68bb57 | ||
|
|
f80fb97c71 | ||
|
|
6308fca587 | ||
|
|
ab71a7bba8 | ||
|
|
958a3320e8 | ||
|
|
60d2bf939b | ||
|
|
a8ea3efae2 | ||
|
|
c1de4e4420 | ||
|
|
f04542eac9 | ||
|
|
f6410393ce | ||
|
|
5be4c6ffbc | ||
|
|
124d7dea1b | ||
|
|
bbcdd1e2e2 | ||
|
|
b6bc29ac92 | ||
|
|
e9a453c430 | ||
|
|
7e1d5e3595 | ||
|
|
a9f5be3f98 | ||
|
|
d6b0e622ea | ||
|
|
fbeaabffc9 | ||
|
|
c7c611d929 | ||
|
|
8106c64c91 | ||
|
|
bd1191783b | ||
|
|
2f4ffe137e | ||
|
|
6735b09dd9 | ||
|
|
745bef8ebc | ||
|
|
5a9673ae1f | ||
|
|
8d0b45b835 | ||
|
|
6a9660de65 | ||
|
|
edbbb2469f | ||
|
|
e3ad0b1655 | ||
|
|
7738ca1ce0 | ||
|
|
8f42833fba | ||
|
|
b2a3e014e9 | ||
|
|
64018c29f3 | ||
|
|
813e8bb664 | ||
|
|
81e4be37ff | ||
|
|
62edb118eb | ||
|
|
f5efb2057c | ||
|
|
3425a3bef9 | ||
|
|
4bf3e310e1 | ||
|
|
abb466e2fb | ||
|
|
54c1642e3b | ||
|
|
b6839d8f51 | ||
|
|
af3ad155e5 | ||
|
|
939a3121b7 | ||
|
|
ac6186e16f | ||
|
|
b72a35a622 | ||
|
|
0d8a4bf936 | ||
|
|
be5edd329f | ||
|
|
0902a5c440 | ||
|
|
bb5641535b | ||
|
|
fc7aac9d41 | ||
|
|
0ff5099cbc | ||
|
|
b38ad66012 | ||
|
|
71395b9a8e | ||
|
|
d3aa37aece | ||
|
|
1b11566485 | ||
|
|
28f5d28201 | ||
|
|
421814e9b3 | ||
|
|
814333b0cc | ||
|
|
0862f670ee | ||
|
|
593d7f3dd6 | ||
|
|
5ed6a74fc4 | ||
|
|
76b6833b61 | ||
|
|
b7e9e4a7c5 | ||
|
|
54b2f78a99 | ||
|
|
fc4be1b337 | ||
|
|
b9b110674e | ||
|
|
7959e41a81 | ||
|
|
d6913fffe6 | ||
|
|
7174a2cd93 | ||
|
|
47e500c2eb | ||
|
|
4511d41329 | ||
|
|
7243f71d7d | ||
|
|
3daaa021eb | ||
|
|
09e13d279c | ||
|
|
d6504320b1 | ||
|
|
af3a0e56f6 | ||
|
|
2da543ebd4 | ||
|
|
ae031cea63 | ||
|
|
6135d2972e | ||
|
|
d717ca0325 | ||
|
|
1c5c06716b | ||
|
|
61d06dd702 | ||
|
|
b702a02f61 | ||
|
|
055f8536c3 | ||
|
|
40ab5b034c | ||
|
|
13906cba9a | ||
|
|
4741ce13c6 | ||
|
|
f81d4a79ea | ||
|
|
550daf2108 | ||
|
|
959eae1b5c | ||
|
|
f2d83b1b21 | ||
|
|
f1670e922f | ||
|
|
ec780ac263 | ||
|
|
847171bd14 | ||
|
|
97488aee88 | ||
|
|
2b3a0ba9c4 | ||
|
|
5f7dc8a5b9 | ||
|
|
edc20ae8b8 | ||
|
|
6ebc9c5c82 | ||
|
|
5a4d92b1bc | ||
|
|
ff48c44496 | ||
|
|
2f81f15f02 | ||
|
|
f1bb8933c1 | ||
|
|
0f0a2b100c | ||
|
|
eef0f453d2 | ||
|
|
e4af69bc93 | ||
|
|
42fe63da2c | ||
|
|
e23d7aa968 | ||
|
|
a46aa808be | ||
|
|
f3b6b4609e | ||
|
|
7dcf0f0866 | ||
|
|
29dcce53db | ||
|
|
a450c8dce9 | ||
|
|
287411667a | ||
|
|
ab30e2a9c7 | ||
|
|
65dd72a0b0 | ||
|
|
66bf1071bb | ||
|
|
410e617834 | ||
|
|
0b952e8bba | ||
|
|
e9d85a3ee4 | ||
|
|
c20d469f31 | ||
|
|
7d0a118eab | ||
|
|
2394f64872 | ||
|
|
a50ad1d292 |
3
.github/helper/.flake8_strict
vendored
3
.github/helper/.flake8_strict
vendored
@@ -66,7 +66,8 @@ ignore =
|
||||
F841,
|
||||
E713,
|
||||
E712,
|
||||
B023
|
||||
B023,
|
||||
B028
|
||||
|
||||
|
||||
max-line-length = 200
|
||||
|
||||
99
.github/helper/documentation.py
vendored
99
.github/helper/documentation.py
vendored
@@ -3,52 +3,71 @@ from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
docs_repos = [
|
||||
"frappe_docs",
|
||||
"erpnext_documentation",
|
||||
WEBSITE_REPOS = [
|
||||
"erpnext_com",
|
||||
"frappe_io",
|
||||
]
|
||||
|
||||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"frappeframework.com",
|
||||
]
|
||||
|
||||
def uri_validator(x):
|
||||
result = urlparse(x)
|
||||
return all([result.scheme, result.netloc, result.path])
|
||||
|
||||
def docs_link_exists(body):
|
||||
for line in body.splitlines():
|
||||
for word in line.split():
|
||||
if word.startswith('http') and uri_validator(word):
|
||||
parsed_url = urlparse(word)
|
||||
if parsed_url.netloc == "github.com":
|
||||
parts = parsed_url.path.split('/')
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||
return True
|
||||
elif parsed_url.netloc == "docs.erpnext.com":
|
||||
return True
|
||||
def is_valid_url(url: str) -> bool:
|
||||
parts = urlparse(url)
|
||||
return all((parts.scheme, parts.netloc, parts.path))
|
||||
|
||||
|
||||
def is_documentation_link(word: str) -> bool:
|
||||
if not word.startswith("http") or not is_valid_url(word):
|
||||
return False
|
||||
|
||||
parsed_url = urlparse(word)
|
||||
if parsed_url.netloc in DOCUMENTATION_DOMAINS:
|
||||
return True
|
||||
|
||||
if parsed_url.netloc == "github.com":
|
||||
parts = parsed_url.path.split("/")
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def contains_documentation_link(body: str) -> bool:
|
||||
return any(
|
||||
is_documentation_link(word)
|
||||
for line in body.splitlines()
|
||||
for word in line.split()
|
||||
)
|
||||
|
||||
|
||||
def check_pull_request(number: str) -> "tuple[int, str]":
|
||||
response = requests.get(f"https://api.github.com/repos/frappe/erpnext/pulls/{number}")
|
||||
if not response.ok:
|
||||
return 1, "Pull Request Not Found! ⚠️"
|
||||
|
||||
payload = response.json()
|
||||
title = (payload.get("title") or "").lower().strip()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if (
|
||||
not title.startswith("feat")
|
||||
or not head_sha
|
||||
or "no-docs" in body
|
||||
or "backport" in body
|
||||
):
|
||||
return 0, "Skipping documentation checks... 🏃"
|
||||
|
||||
if contains_documentation_link(body):
|
||||
return 0, "Documentation Link Found. You're Awesome! 🎉"
|
||||
|
||||
return 1, "Documentation Link Not Found! ⚠️"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pr = sys.argv[1]
|
||||
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
|
||||
|
||||
if response.ok:
|
||||
payload = response.json()
|
||||
title = (payload.get("title") or "").lower().strip()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if (title.startswith("feat")
|
||||
and head_sha
|
||||
and "no-docs" not in body
|
||||
and "backport" not in body
|
||||
):
|
||||
if docs_link_exists(body):
|
||||
print("Documentation Link Found. You're Awesome! 🎉")
|
||||
|
||||
else:
|
||||
print("Documentation Link Not Found! ⚠️")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print("Skipping documentation checks... 🏃")
|
||||
exit_code, message = check_pull_request(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(exit_code)
|
||||
|
||||
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: '3.10'
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v2
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js v14
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
@@ -28,4 +28,4 @@ jobs:
|
||||
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
||||
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
||||
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
||||
run: npx semantic-release
|
||||
run: npx semantic-release
|
||||
|
||||
@@ -32,8 +32,8 @@ repos:
|
||||
- id: black
|
||||
additional_dependencies: ['click==8.0.4']
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.9.1
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.43.0"
|
||||
__version__ = "13.46.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
{
|
||||
"country_code": "de",
|
||||
"name": "SKR03 mit Kontonummern",
|
||||
"tree": {
|
||||
"Aktiva": {
|
||||
"is_group": 1,
|
||||
"country_code": "de",
|
||||
"name": "SKR03 mit Kontonummern",
|
||||
"tree": {
|
||||
"Aktiva": {
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"A - Anlagevermögen": {
|
||||
"is_group": 1,
|
||||
"EDV-Software": {
|
||||
"account_number": "0027",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Gesch\u00e4ftsausstattung": {
|
||||
"account_number": "0410",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"B\u00fcroeinrichtung": {
|
||||
"account_number": "0420",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Darlehen": {
|
||||
"account_number": "0565"
|
||||
},
|
||||
"Maschinen": {
|
||||
"account_number": "0210",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Betriebsausstattung": {
|
||||
"account_number": "0400",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Ladeneinrichtung": {
|
||||
"account_number": "0430",
|
||||
"account_type": "Fixed Asset"
|
||||
"A - Anlagevermögen": {
|
||||
"is_group": 1,
|
||||
"EDV-Software": {
|
||||
"account_number": "0027",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Geschäftsausstattung": {
|
||||
"account_number": "0410",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Büroeinrichtung": {
|
||||
"account_number": "0420",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Darlehen": {
|
||||
"account_number": "0565"
|
||||
},
|
||||
"Maschinen": {
|
||||
"account_number": "0210",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Betriebsausstattung": {
|
||||
"account_number": "0400",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Ladeneinrichtung": {
|
||||
"account_number": "0430",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation": {
|
||||
"account_type": "Accumulated Depreciation"
|
||||
@@ -60,36 +60,46 @@
|
||||
"Durchlaufende Posten": {
|
||||
"account_number": "1590"
|
||||
},
|
||||
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
|
||||
"Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": {
|
||||
"account_number": "1371"
|
||||
},
|
||||
"Abziehbare Vorsteuer": {
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"Abziehbare Vorsteuer 7%": {
|
||||
"account_number": "1571"
|
||||
"Abziehbare Vorsteuer 7 %": {
|
||||
"account_number": "1571",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 7.0
|
||||
},
|
||||
"Abziehbare Vorsteuer 19%": {
|
||||
"account_number": "1576"
|
||||
"Abziehbare Vorsteuer 19 %": {
|
||||
"account_number": "1576",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 19.0
|
||||
},
|
||||
"Abziehbare Vorsteuer nach \u00a713b UStG 19%": {
|
||||
"account_number": "1577"
|
||||
},
|
||||
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
|
||||
"account_number": "3120"
|
||||
"Abziehbare Vorsteuer nach § 13b UStG 19 %": {
|
||||
"account_number": "1577",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 19.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"III. Wertpapiere": {
|
||||
"is_group": 1
|
||||
"is_group": 1,
|
||||
"Anteile an verbundenen Unternehmen (Umlaufvermögen)": {
|
||||
"account_number": "1340"
|
||||
},
|
||||
"Anteile an herrschender oder mit Mehrheit beteiligter Gesellschaft": {
|
||||
"account_number": "1344"
|
||||
},
|
||||
"Sonstige Wertpapiere": {
|
||||
"account_number": "1348"
|
||||
}
|
||||
},
|
||||
"IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": {
|
||||
"is_group": 1,
|
||||
"Kasse": {
|
||||
"account_type": "Cash",
|
||||
"is_group": 1,
|
||||
"account_type": "Cash",
|
||||
"Kasse": {
|
||||
"is_group": 1,
|
||||
"account_number": "1000",
|
||||
"account_type": "Cash"
|
||||
}
|
||||
@@ -111,21 +121,21 @@
|
||||
"C - Rechnungsabgrenzungsposten": {
|
||||
"is_group": 1,
|
||||
"Aktive Rechnungsabgrenzung": {
|
||||
"account_number": "0980"
|
||||
"account_number": "0980"
|
||||
}
|
||||
},
|
||||
"D - Aktive latente Steuern": {
|
||||
"is_group": 1,
|
||||
"Aktive latente Steuern": {
|
||||
"account_number": "0983"
|
||||
"account_number": "0983"
|
||||
}
|
||||
},
|
||||
"E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": {
|
||||
"is_group": 1
|
||||
}
|
||||
},
|
||||
"Passiva": {
|
||||
"is_group": 1,
|
||||
},
|
||||
"Passiva": {
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"A. Eigenkapital": {
|
||||
"is_group": 1,
|
||||
@@ -200,26 +210,32 @@
|
||||
},
|
||||
"Umsatzsteuer": {
|
||||
"is_group": 1,
|
||||
"account_type": "Tax",
|
||||
"Umsatzsteuer 7%": {
|
||||
"account_number": "1771"
|
||||
"Umsatzsteuer 7 %": {
|
||||
"account_number": "1771",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 7.0
|
||||
},
|
||||
"Umsatzsteuer 19%": {
|
||||
"account_number": "1776"
|
||||
"Umsatzsteuer 19 %": {
|
||||
"account_number": "1776",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 19.0
|
||||
},
|
||||
"Umsatzsteuer-Vorauszahlung": {
|
||||
"account_number": "1780"
|
||||
"account_number": "1780",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Umsatzsteuer-Vorauszahlung 1/11": {
|
||||
"account_number": "1781"
|
||||
},
|
||||
"Umsatzsteuer \u00a7 13b UStG 19%": {
|
||||
"account_number": "1787"
|
||||
"Umsatzsteuer nach § 13b UStG 19 %": {
|
||||
"account_number": "1787",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 19.0
|
||||
},
|
||||
"Umsatzsteuer Vorjahr": {
|
||||
"account_number": "1790"
|
||||
},
|
||||
"Umsatzsteuer fr\u00fchere Jahre": {
|
||||
"Umsatzsteuer frühere Jahre": {
|
||||
"account_number": "1791"
|
||||
}
|
||||
}
|
||||
@@ -234,44 +250,56 @@
|
||||
"E. Passive latente Steuern": {
|
||||
"is_group": 1
|
||||
}
|
||||
},
|
||||
"Erl\u00f6se u. Ertr\u00e4ge 2/8": {
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Erl\u00f6skonten 8": {
|
||||
},
|
||||
"Erlöse u. Erträge 2/8": {
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Erlöskonten 8": {
|
||||
"is_group": 1,
|
||||
"Erl\u00f6se": {
|
||||
"account_number": "8200",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Erl\u00f6se USt. 19%": {
|
||||
"account_number": "8400",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Erl\u00f6se USt. 7%": {
|
||||
"account_number": "8300",
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
},
|
||||
"Ertragskonten 2": {
|
||||
"is_group": 1,
|
||||
"sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {
|
||||
"account_number": "2650",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Au\u00dferordentliche Ertr\u00e4ge": {
|
||||
"account_number": "2500",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Sonstige Ertr\u00e4ge": {
|
||||
"account_number": "2700",
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Aufwendungen 2/4": {
|
||||
"is_group": 1,
|
||||
"Erlöse": {
|
||||
"account_number": "8200",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Erlöse USt. 19 %": {
|
||||
"account_number": "8400",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Erlöse USt. 7 %": {
|
||||
"account_number": "8300",
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
},
|
||||
"Ertragskonten 2": {
|
||||
"is_group": 1,
|
||||
"sonstige Zinsen und ähnliche Erträge": {
|
||||
"account_number": "2650",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Außerordentliche Erträge": {
|
||||
"account_number": "2500",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Sonstige Erträge": {
|
||||
"account_number": "2700",
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Aufwendungen 2/4": {
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Fremdleistungen": {
|
||||
"account_number": "3100",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fremdleistungen ohne Vorsteuer": {
|
||||
"account_number": "3109",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Bauleistungen eines im Inland ansässigen Unternehmers 19 % Vorsteuer und 19 % Umsatzsteuer": {
|
||||
"account_number": "3120",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Wareneingang": {
|
||||
"account_number": "3200"
|
||||
},
|
||||
@@ -298,234 +326,234 @@
|
||||
"Gegenkonto 4996-4998": {
|
||||
"account_number": "4999"
|
||||
},
|
||||
"Abschreibungen": {
|
||||
"is_group": 1,
|
||||
"Abschreibungen": {
|
||||
"is_group": 1,
|
||||
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
|
||||
"account_number": "4830",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
"account_number": "4830",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"Abschreibungen auf Gebäude": {
|
||||
"account_number": "4831",
|
||||
"account_type": "Depreciation"
|
||||
"account_number": "4831",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Abschreibungen auf Kfz": {
|
||||
"account_number": "4832",
|
||||
"account_type": "Depreciation"
|
||||
"account_number": "4832",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Sofortabschreibung GWG": {
|
||||
"account_number": "4855",
|
||||
"account_type": "Expense Account"
|
||||
"account_number": "4855",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Kfz-Kosten": {
|
||||
"is_group": 1,
|
||||
"Kfz-Steuer": {
|
||||
"account_number": "4510",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Kfz-Versicherungen": {
|
||||
"account_number": "4520",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"laufende Kfz-Betriebskosten": {
|
||||
"account_number": "4530",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Kfz-Reparaturen": {
|
||||
"account_number": "4540",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fremdfahrzeuge": {
|
||||
"account_number": "4570",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"sonstige Kfz-Kosten": {
|
||||
"account_number": "4580",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Personalkosten": {
|
||||
"is_group": 1,
|
||||
"Geh\u00e4lter": {
|
||||
"account_number": "4120",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"gesetzliche soziale Aufwendungen": {
|
||||
"account_number": "4130",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aufwendungen f\u00fcr Altersvorsorge": {
|
||||
"account_number": "4165",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Verm\u00f6genswirksame Leistungen": {
|
||||
"account_number": "4170",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aushilfsl\u00f6hne": {
|
||||
"account_number": "4190",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Raumkosten": {
|
||||
"is_group": 1,
|
||||
"Miete und Nebenkosten": {
|
||||
"account_number": "4210",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
|
||||
"account_number": "4240",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Reinigung": {
|
||||
"account_number": "4250",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Reparatur/Instandhaltung": {
|
||||
"is_group": 1,
|
||||
"Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": {
|
||||
"account_number": "4805",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Versicherungsbeitr\u00e4ge": {
|
||||
"is_group": 1,
|
||||
"Versicherungen": {
|
||||
"account_number": "4360",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Beitr\u00e4ge": {
|
||||
"account_number": "4380",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"sonstige Ausgaben": {
|
||||
"account_number": "4390",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": {
|
||||
"account_number": "4396",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Werbe-/Reisekosten": {
|
||||
"is_group": 1,
|
||||
"Werbekosten": {
|
||||
"account_number": "4610",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aufmerksamkeiten": {
|
||||
"account_number": "4653",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": {
|
||||
"account_number": "4665",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Reisekosten Unternehmer": {
|
||||
"account_number": "4670",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"verschiedene Kosten": {
|
||||
"is_group": 1,
|
||||
"Porto": {
|
||||
"account_number": "4910",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Telekom": {
|
||||
"account_number": "4920",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Mobilfunk D2": {
|
||||
"account_number": "4921",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Internet": {
|
||||
"account_number": "4922",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"B\u00fcrobedarf": {
|
||||
"account_number": "4930",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Zeitschriften, B\u00fccher": {
|
||||
"account_number": "4940",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fortbildungskosten": {
|
||||
"account_number": "4945",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Buchf\u00fchrungskosten": {
|
||||
"account_number": "4955",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Abschlu\u00df- u. Pr\u00fcfungskosten": {
|
||||
"account_number": "4957",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Nebenkosten des Geldverkehrs": {
|
||||
"account_number": "4970",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Werkzeuge und Kleinger\u00e4te": {
|
||||
"account_number": "4985",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Zinsaufwendungen": {
|
||||
"is_group": 1,
|
||||
"Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": {
|
||||
"account_number": "2110",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Zinsaufwendungen f\u00fcr KFZ Finanzierung": {
|
||||
"account_number": "2121",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Anfangsbestand 9": {
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Saldenvortragskonten": {
|
||||
"is_group": 1,
|
||||
"Saldenvortrag Sachkonten": {
|
||||
"account_number": "9000"
|
||||
},
|
||||
"Saldenvortr\u00e4ge Debitoren": {
|
||||
"account_number": "9008"
|
||||
},
|
||||
"Saldenvortr\u00e4ge Kreditoren": {
|
||||
"account_number": "9009"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Privatkonten 1": {
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Privatentnahmen/-einlagen": {
|
||||
"is_group": 1,
|
||||
"Privatentnahme allgemein": {
|
||||
"account_number": "1800"
|
||||
},
|
||||
"Privatsteuern": {
|
||||
"account_number": "1810"
|
||||
},
|
||||
"Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": {
|
||||
"account_number": "1820"
|
||||
},
|
||||
"Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": {
|
||||
"account_number": "1830"
|
||||
},
|
||||
"Au\u00dfergew\u00f6hnliche Belastungen": {
|
||||
"account_number": "1850"
|
||||
},
|
||||
"Privateinlagen": {
|
||||
"account_number": "1890"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Kfz-Kosten": {
|
||||
"is_group": 1,
|
||||
"Kfz-Steuer": {
|
||||
"account_number": "4510",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Kfz-Versicherungen": {
|
||||
"account_number": "4520",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"laufende Kfz-Betriebskosten": {
|
||||
"account_number": "4530",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Kfz-Reparaturen": {
|
||||
"account_number": "4540",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fremdfahrzeuge": {
|
||||
"account_number": "4570",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"sonstige Kfz-Kosten": {
|
||||
"account_number": "4580",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Personalkosten": {
|
||||
"is_group": 1,
|
||||
"Gehälter": {
|
||||
"account_number": "4120",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"gesetzliche soziale Aufwendungen": {
|
||||
"account_number": "4130",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aufwendungen für Altersvorsorge": {
|
||||
"account_number": "4165",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Vermögenswirksame Leistungen": {
|
||||
"account_number": "4170",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aushilfslöhne": {
|
||||
"account_number": "4190",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Raumkosten": {
|
||||
"is_group": 1,
|
||||
"Miete und Nebenkosten": {
|
||||
"account_number": "4210",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
|
||||
"account_number": "4240",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Reinigung": {
|
||||
"account_number": "4250",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Reparatur/Instandhaltung": {
|
||||
"is_group": 1,
|
||||
"Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": {
|
||||
"account_number": "4805",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Versicherungsbeiträge": {
|
||||
"is_group": 1,
|
||||
"Versicherungen": {
|
||||
"account_number": "4360",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Beiträge": {
|
||||
"account_number": "4380",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"sonstige Ausgaben": {
|
||||
"account_number": "4390",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": {
|
||||
"account_number": "4396",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Werbe-/Reisekosten": {
|
||||
"is_group": 1,
|
||||
"Werbekosten": {
|
||||
"account_number": "4610",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aufmerksamkeiten": {
|
||||
"account_number": "4653",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": {
|
||||
"account_number": "4665",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Reisekosten Unternehmer": {
|
||||
"account_number": "4670",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"verschiedene Kosten": {
|
||||
"is_group": 1,
|
||||
"Porto": {
|
||||
"account_number": "4910",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Telekom": {
|
||||
"account_number": "4920",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Mobilfunk D2": {
|
||||
"account_number": "4921",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Internet": {
|
||||
"account_number": "4922",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Bürobedarf": {
|
||||
"account_number": "4930",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Zeitschriften, Bücher": {
|
||||
"account_number": "4940",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fortbildungskosten": {
|
||||
"account_number": "4945",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Buchführungskosten": {
|
||||
"account_number": "4955",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Abschluß- u. Prüfungskosten": {
|
||||
"account_number": "4957",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Nebenkosten des Geldverkehrs": {
|
||||
"account_number": "4970",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Werkzeuge und Kleingeräte": {
|
||||
"account_number": "4985",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Zinsaufwendungen": {
|
||||
"is_group": 1,
|
||||
"Zinsaufwendungen für kurzfristige Verbindlichkeiten": {
|
||||
"account_number": "2110",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Zinsaufwendungen für KFZ Finanzierung": {
|
||||
"account_number": "2121",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Anfangsbestand 9": {
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Saldenvortragskonten": {
|
||||
"is_group": 1,
|
||||
"Saldenvortrag Sachkonten": {
|
||||
"account_number": "9000"
|
||||
},
|
||||
"Saldenvorträge Debitoren": {
|
||||
"account_number": "9008"
|
||||
},
|
||||
"Saldenvorträge Kreditoren": {
|
||||
"account_number": "9009"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Privatkonten 1": {
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Privatentnahmen/-einlagen": {
|
||||
"is_group": 1,
|
||||
"Privatentnahme allgemein": {
|
||||
"account_number": "1800"
|
||||
},
|
||||
"Privatsteuern": {
|
||||
"account_number": "1810"
|
||||
},
|
||||
"Sonderausgaben beschränkt abzugsfähig": {
|
||||
"account_number": "1820"
|
||||
},
|
||||
"Sonderausgaben unbeschränkt abzugsfähig": {
|
||||
"account_number": "1830"
|
||||
},
|
||||
"Außergewöhnliche Belastungen": {
|
||||
"account_number": "1850"
|
||||
},
|
||||
"Privateinlagen": {
|
||||
"account_number": "1890"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ class JournalEntry(AccountsController):
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_expense_claim()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
@@ -235,6 +236,34 @@ class JournalEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.db.get_value(
|
||||
"Asset", d.reference_name, ["calculate_depreciation", "value_after_depreciation"], as_dict=1
|
||||
)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
continue
|
||||
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
frappe.db.set_value(
|
||||
"Asset",
|
||||
d.reference_name,
|
||||
"value_after_depreciation",
|
||||
asset.value_after_depreciation - depr_value,
|
||||
)
|
||||
|
||||
def update_inter_company_jv(self):
|
||||
if (
|
||||
self.voucher_type == "Inter Company Journal Entry"
|
||||
@@ -293,19 +322,41 @@ class JournalEntry(AccountsController):
|
||||
d.db_update()
|
||||
|
||||
def unlink_asset_reference(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.reference_type == "Asset" and d.reference_name:
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
for s in asset.get("schedules"):
|
||||
if s.journal_entry == self.name:
|
||||
s.db_set("journal_entry", None)
|
||||
|
||||
idx = cint(s.finance_book_id) or 1
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation += s.depreciation_amount
|
||||
finance_books.db_update()
|
||||
if asset.calculate_depreciation:
|
||||
for s in asset.get("schedules"):
|
||||
if s.journal_entry == self.name:
|
||||
s.db_set("journal_entry", None)
|
||||
|
||||
asset.set_status()
|
||||
idx = cint(s.finance_book_id) or 1
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation += s.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
asset.set_status()
|
||||
|
||||
break
|
||||
else:
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
frappe.db.set_value(
|
||||
"Asset",
|
||||
d.reference_name,
|
||||
"value_after_depreciation",
|
||||
asset.value_after_depreciation + depr_value,
|
||||
)
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
|
||||
@@ -1255,6 +1255,7 @@ def get_outstanding_reference_documents(args):
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
args.get("company"),
|
||||
filters=args,
|
||||
condition=condition,
|
||||
)
|
||||
|
||||
@@ -211,7 +211,7 @@ class PaymentReconciliation(Document):
|
||||
condition += " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
non_reconciled_invoices = get_outstanding_invoices(
|
||||
self.party_type, self.party, self.receivable_payable_account, condition=condition
|
||||
self.party_type, self.party, self.receivable_payable_account, self.company, condition=condition
|
||||
)
|
||||
|
||||
if self.invoice_limit:
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
{{ _("Against") }}: {{ row.against }}
|
||||
<br>{{ _("Remarks") }}: {{ row.remarks }}
|
||||
{% if row.bill_no %}
|
||||
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||
|
||||
@@ -598,7 +598,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def make_supplier_gl_entry(self, gl_entries):
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
||||
# because rounded_total had value even before introduction of posting GLE based on rounded total
|
||||
grand_total = (
|
||||
self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||
)
|
||||
@@ -799,10 +799,7 @@ class PurchaseInvoice(BuyingController):
|
||||
else item.deferred_expense_account
|
||||
)
|
||||
|
||||
if not item.is_fixed_asset:
|
||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||
else:
|
||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
|
||||
@@ -1790,6 +1790,8 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"hide_days": 1,
|
||||
@@ -2045,7 +2047,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-09-16 17:44:22.227332",
|
||||
"modified": "2023-01-28 19:45:47.538163",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2101,4 +2103,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,7 @@ from frappe import _, msgprint, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||
from six import iteritems
|
||||
|
||||
import erpnext
|
||||
@@ -33,10 +23,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
make_depreciation_entry,
|
||||
reset_depreciation_schedule,
|
||||
reverse_depreciation_entry_made_after_disposal,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
@@ -1114,18 +1106,20 @@ class SalesInvoice(SellingController):
|
||||
asset = self.get_asset(item)
|
||||
|
||||
if self.is_return:
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset, item.base_net_amount, item.finance_book
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
|
||||
reset_depreciation_schedule(asset, self.posting_date)
|
||||
|
||||
else:
|
||||
if asset.calculate_depreciation:
|
||||
self.depreciate_asset(asset)
|
||||
depreciate_asset(asset, self.posting_date)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset, item.base_net_amount, item.finance_book
|
||||
@@ -1193,95 +1187,6 @@ class SalesInvoice(SellingController):
|
||||
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
|
||||
)
|
||||
|
||||
def depreciate_asset(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(date_of_sale=self.posting_date)
|
||||
asset.save()
|
||||
|
||||
make_depreciation_entry(asset.name, self.posting_date)
|
||||
asset.load_from_db()
|
||||
|
||||
def reset_depreciation_schedule(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
asset.prepare_depreciation_data(date_of_return=self.posting_date)
|
||||
|
||||
self.modify_depreciation_schedule_for_asset_repairs(asset)
|
||||
asset.save()
|
||||
asset.load_from_db()
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
|
||||
)
|
||||
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
asset.prepare_depreciation_data()
|
||||
|
||||
def reverse_depreciation_entry_made_after_sale(self, asset):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
|
||||
|
||||
row = -1
|
||||
finance_book = asset.get("schedules")[0].get("finance_book")
|
||||
for schedule in asset.get("schedules"):
|
||||
if schedule.finance_book != finance_book:
|
||||
row = 0
|
||||
finance_book = schedule.finance_book
|
||||
else:
|
||||
row += 1
|
||||
|
||||
if schedule.schedule_date == posting_date_of_original_invoice:
|
||||
if not self.sale_was_made_on_original_schedule_date(
|
||||
asset, schedule, row, posting_date_of_original_invoice
|
||||
) or self.sale_happens_in_the_future(posting_date_of_original_invoice):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
|
||||
asset.finance_books[0].value_after_depreciation += depreciation_amount
|
||||
asset.save()
|
||||
|
||||
def get_posting_date_of_sales_invoice(self):
|
||||
return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
|
||||
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
||||
def sale_was_made_on_original_schedule_date(
|
||||
self, asset, schedule, row, posting_date_of_original_invoice
|
||||
):
|
||||
for finance_book in asset.get("finance_books"):
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
orginal_schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if orginal_schedule_date == posting_date_of_original_invoice:
|
||||
return True
|
||||
return False
|
||||
|
||||
def sale_happens_in_the_future(self, posting_date_of_original_invoice):
|
||||
if posting_date_of_original_invoice > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_depreciation_amount_in_je(self, journal_entry):
|
||||
if journal_entry.accounts[0].debit_in_account_currency:
|
||||
return journal_entry.accounts[0].debit_in_account_currency
|
||||
else:
|
||||
return journal_entry.accounts[0].credit_in_account_currency
|
||||
|
||||
@property
|
||||
def enable_discount_accounting(self):
|
||||
if not hasattr(self, "_enable_discount_accounting"):
|
||||
|
||||
@@ -1117,6 +1117,46 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
|
||||
bundle_item.append(
|
||||
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||
)
|
||||
bundle_item.save(ignore_permissions=True)
|
||||
|
||||
make_item("_Packed Item New 1", {"is_stock_item": 1})
|
||||
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="_Test Product Bundle Item New",
|
||||
update_stock=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
transaction_date=add_days(nowdate(), -1),
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
|
||||
|
||||
bin_details = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
|
||||
["actual_qty", "projected_qty", "ordered_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
si.transaction_date = nowdate()
|
||||
si.save()
|
||||
|
||||
packed_item = si.packed_items[0]
|
||||
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
|
||||
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
|
||||
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
|
||||
|
||||
def test_pos_si_without_payment(self):
|
||||
make_pos_profile()
|
||||
|
||||
|
||||
@@ -135,6 +135,34 @@ def get_assets(filters):
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
||||
0
|
||||
|
||||
@@ -378,15 +378,14 @@ class Deferred_Revenue_and_Expense_Report(object):
|
||||
ret += [{}]
|
||||
|
||||
# add total row
|
||||
if ret is not []:
|
||||
if self.filters.type == "Revenue":
|
||||
total_row = frappe._dict({"name": "Total Deferred Income"})
|
||||
elif self.filters.type == "Expense":
|
||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||
if self.filters.type == "Revenue":
|
||||
total_row = frappe._dict({"name": "Total Deferred Income"})
|
||||
elif self.filters.type == "Expense":
|
||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
total_row[period.key] = self.period_total[idx].total
|
||||
ret.append(total_row)
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
total_row[period.key] = self.period_total[idx].total
|
||||
ret.append(total_row)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{%= __("Date") %}</th>
|
||||
<th style="width: 15%">{%= __("Ref") %}</th>
|
||||
<th style="width: 25%">{%= __("Party") %}</th>
|
||||
<th style="width: 15%">{%= __("Reference") %}</th>
|
||||
<th style="width: 25%">{%= __("Remarks") %}</th>
|
||||
<th style="width: 15%">{%= __("Debit") %}</th>
|
||||
<th style="width: 15%">{%= __("Credit") %}</th>
|
||||
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
|
||||
@@ -45,7 +45,6 @@
|
||||
<br>
|
||||
{% } %}
|
||||
|
||||
{{ __("Against") }}: {%= data[i].against %}
|
||||
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
||||
{% if(data[i].bill_no) { %}
|
||||
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
|
||||
@@ -585,10 +585,35 @@ class GrossProfitGenerator(object):
|
||||
return self.calculate_buying_amount_from_sle(
|
||||
row, my_sle, parenttype, parent, item_row, item_code
|
||||
)
|
||||
elif row.sales_order and row.so_detail:
|
||||
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
|
||||
if incoming_amount:
|
||||
return incoming_amount
|
||||
else:
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
|
||||
return 0.0
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
|
||||
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
delivery_note = frappe.qb.DocType("Delivery Note")
|
||||
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(delivery_note)
|
||||
.inner_join(delivery_note_item)
|
||||
.on(delivery_note.name == delivery_note_item.parent)
|
||||
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
|
||||
.where(delivery_note.docstatus == 1)
|
||||
.where(delivery_note_item.item_code == item_code)
|
||||
.where(delivery_note_item.against_sales_order == sales_order)
|
||||
.where(delivery_note_item.so_detail == so_detail)
|
||||
.groupby(delivery_note_item.item_code)
|
||||
)
|
||||
|
||||
incoming_amount = query.run()
|
||||
return flt(incoming_amount[0][0]) if incoming_amount else 0
|
||||
|
||||
def get_average_buying_rate(self, row, item_code):
|
||||
args = row
|
||||
@@ -665,7 +690,8 @@ class GrossProfitGenerator(object):
|
||||
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
|
||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.dn_detail,
|
||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail,
|
||||
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
|
||||
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
|
||||
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
||||
|
||||
@@ -301,3 +301,82 @@ class TestGrossProfit(FrappeTestCase):
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertGreater(len(data), 0)
|
||||
|
||||
def test_order_connected_dn_and_inv(self):
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
"""
|
||||
Test gp calculation when invoice and delivery note aren't directly connected.
|
||||
SO -- INV
|
||||
|
|
||||
DN
|
||||
"""
|
||||
se = make_stock_entry(
|
||||
company=self.company,
|
||||
item_code=self.item,
|
||||
target=self.warehouse,
|
||||
qty=3,
|
||||
basic_rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
item = se.items[0]
|
||||
se.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"s_warehouse": item.s_warehouse,
|
||||
"t_warehouse": item.t_warehouse,
|
||||
"qty": 10,
|
||||
"basic_rate": 200,
|
||||
"conversion_factor": item.conversion_factor or 1.0,
|
||||
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no,
|
||||
"cost_center": item.cost_center,
|
||||
"expense_account": item.expense_account,
|
||||
},
|
||||
)
|
||||
se = se.save().submit()
|
||||
|
||||
so = make_sales_order(
|
||||
customer=self.customer,
|
||||
company=self.company,
|
||||
warehouse=self.warehouse,
|
||||
item=self.item,
|
||||
qty=4,
|
||||
do_not_save=False,
|
||||
do_not_submit=False,
|
||||
)
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
make_delivery_note,
|
||||
make_sales_invoice,
|
||||
)
|
||||
|
||||
make_delivery_note(so.name).submit()
|
||||
sinv = make_sales_invoice(so.name).submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
expected_entry = {
|
||||
"parent_invoice": sinv.name,
|
||||
"currency": "INR",
|
||||
"sales_invoice": self.item,
|
||||
"customer": self.customer,
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 4.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 125.0,
|
||||
"selling_amount": 400.0,
|
||||
"buying_amount": 500.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": -25.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
|
||||
@@ -840,7 +840,7 @@ def remove_return_pos_invoices(party_type, party, invoice_list):
|
||||
return invoice_list
|
||||
|
||||
|
||||
def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
|
||||
def get_outstanding_invoices(party_type, party, account, company, condition=None, filters=None):
|
||||
outstanding_invoices = []
|
||||
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
|
||||
|
||||
@@ -892,61 +892,73 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
|
||||
|
||||
invoice_list = remove_return_pos_invoices(party_type, party, invoice_list)
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select against_voucher_type, against_voucher,
|
||||
ifnull(sum({payment_dr_or_cr}), 0) as payment_amount
|
||||
from `tabGL Entry`
|
||||
where party_type = %(party_type)s and party = %(party)s
|
||||
and account = %(account)s
|
||||
and {payment_dr_or_cr} > 0
|
||||
and against_voucher is not null and against_voucher != ''
|
||||
and is_cancelled=0
|
||||
group by against_voucher_type, against_voucher
|
||||
""".format(
|
||||
payment_dr_or_cr=payment_dr_or_cr
|
||||
),
|
||||
{"party_type": party_type, "party": party, "account": account},
|
||||
as_dict=True,
|
||||
)
|
||||
if invoice_list:
|
||||
invoices = [d.voucher_no for d in invoice_list]
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select against_voucher_type, against_voucher,
|
||||
ifnull(sum({payment_dr_or_cr}), 0) as payment_amount
|
||||
from `tabGL Entry`
|
||||
where
|
||||
company = %(company)s
|
||||
and party_type = %(party_type)s and party = %(party)s
|
||||
and account = %(account)s
|
||||
and {payment_dr_or_cr} > 0
|
||||
and ifnull(against_voucher, '') != ''
|
||||
and is_cancelled=0
|
||||
and against_voucher in %(invoices)s
|
||||
group by against_voucher_type, against_voucher
|
||||
""".format(
|
||||
payment_dr_or_cr=payment_dr_or_cr,
|
||||
),
|
||||
{
|
||||
"company": company,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"account": account,
|
||||
"invoices": invoices,
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
pe_map = frappe._dict()
|
||||
for d in payment_entries:
|
||||
pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount)
|
||||
pe_map = frappe._dict()
|
||||
for d in payment_entries:
|
||||
pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount)
|
||||
|
||||
for d in invoice_list:
|
||||
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
|
||||
outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
|
||||
if outstanding_amount > 0.5 / (10**precision):
|
||||
if (
|
||||
filters
|
||||
and filters.get("outstanding_amt_greater_than")
|
||||
and not (
|
||||
outstanding_amount >= filters.get("outstanding_amt_greater_than")
|
||||
and outstanding_amount <= filters.get("outstanding_amt_less_than")
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices:
|
||||
outstanding_invoices.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_no": d.voucher_no,
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"payment_amount": payment_amount,
|
||||
"outstanding_amount": outstanding_amount,
|
||||
"due_date": d.due_date,
|
||||
"currency": d.currency,
|
||||
}
|
||||
for d in invoice_list:
|
||||
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
|
||||
outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
|
||||
if outstanding_amount > 0.5 / (10**precision):
|
||||
if (
|
||||
filters
|
||||
and filters.get("outstanding_amt_greater_than")
|
||||
and not (
|
||||
outstanding_amount >= filters.get("outstanding_amt_greater_than")
|
||||
and outstanding_amount <= filters.get("outstanding_amt_less_than")
|
||||
)
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices:
|
||||
outstanding_invoices.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_no": d.voucher_no,
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"payment_amount": payment_amount,
|
||||
"outstanding_amount": outstanding_amount,
|
||||
"due_date": d.due_date,
|
||||
"currency": d.currency,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
outstanding_invoices = sorted(
|
||||
outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate())
|
||||
)
|
||||
|
||||
outstanding_invoices = sorted(
|
||||
outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate())
|
||||
)
|
||||
return outstanding_invoices
|
||||
|
||||
|
||||
|
||||
@@ -132,6 +132,10 @@ frappe.ui.form.on('Asset', {
|
||||
}, __("Manage"));
|
||||
}
|
||||
|
||||
if (frm.doc.depr_entry_posting_status === "Failed") {
|
||||
frm.trigger("set_depr_posting_failure_alert");
|
||||
}
|
||||
|
||||
frm.trigger("setup_chart");
|
||||
}
|
||||
|
||||
@@ -142,6 +146,19 @@ frappe.ui.form.on('Asset', {
|
||||
}
|
||||
},
|
||||
|
||||
set_depr_posting_failure_alert: function (frm) {
|
||||
const alert = `
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<span class="indicator whitespace-nowrap red">
|
||||
<span>Failed to post depreciation entries</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
frm.dashboard.set_headline_alert(alert);
|
||||
},
|
||||
|
||||
toggle_reference_doc: function(frm) {
|
||||
if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) {
|
||||
frm.set_df_property('purchase_invoice', 'read_only', 1);
|
||||
@@ -184,39 +201,58 @@ frappe.ui.form.on('Asset', {
|
||||
})
|
||||
},
|
||||
|
||||
setup_chart: function(frm) {
|
||||
var x_intervals = [frm.doc.purchase_date];
|
||||
var asset_values = [frm.doc.gross_purchase_amount];
|
||||
var last_depreciation_date = frm.doc.purchase_date;
|
||||
|
||||
if(frm.doc.opening_accumulated_depreciation) {
|
||||
last_depreciation_date = frappe.datetime.add_months(frm.doc.next_depreciation_date,
|
||||
-1*frm.doc.frequency_of_depreciation);
|
||||
|
||||
x_intervals.push(last_depreciation_date);
|
||||
asset_values.push(flt(frm.doc.gross_purchase_amount) -
|
||||
flt(frm.doc.opening_accumulated_depreciation));
|
||||
setup_chart: async function(frm) {
|
||||
if(frm.doc.finance_books.length > 1) {
|
||||
return
|
||||
}
|
||||
|
||||
$.each(frm.doc.schedules || [], function(i, v) {
|
||||
x_intervals.push(v.schedule_date);
|
||||
var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount);
|
||||
if(v.journal_entry) {
|
||||
last_depreciation_date = v.schedule_date;
|
||||
asset_values.push(asset_value);
|
||||
} else {
|
||||
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
|
||||
asset_values.push(null);
|
||||
} else {
|
||||
asset_values.push(asset_value)
|
||||
}
|
||||
var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: 'Date' })];
|
||||
var asset_values = [frm.doc.gross_purchase_amount];
|
||||
|
||||
if(frm.doc.calculate_depreciation) {
|
||||
if(frm.doc.opening_accumulated_depreciation) {
|
||||
var depreciation_date = frappe.datetime.add_months(
|
||||
frm.doc.finance_books[0].depreciation_start_date,
|
||||
-1 * frm.doc.finance_books[0].frequency_of_depreciation
|
||||
);
|
||||
x_intervals.push(frappe.format(depreciation_date, { fieldtype: 'Date' }));
|
||||
asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
|
||||
}
|
||||
});
|
||||
|
||||
$.each(frm.doc.schedules || [], function(i, v) {
|
||||
x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' }));
|
||||
var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount'));
|
||||
if(v.journal_entry) {
|
||||
asset_values.push(asset_value);
|
||||
} else {
|
||||
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
|
||||
asset_values.push(null);
|
||||
} else {
|
||||
asset_values.push(asset_value);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if(frm.doc.opening_accumulated_depreciation) {
|
||||
x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' }));
|
||||
asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
|
||||
}
|
||||
|
||||
let depr_entries = (await frappe.call({
|
||||
method: "get_manual_depreciation_entries",
|
||||
doc: frm.doc,
|
||||
})).message;
|
||||
|
||||
$.each(depr_entries || [], function(i, v) {
|
||||
x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' }));
|
||||
let last_asset_value = asset_values[asset_values.length - 1]
|
||||
asset_values.push(last_asset_value - v.value);
|
||||
});
|
||||
}
|
||||
|
||||
if(in_list(["Scrapped", "Sold"], frm.doc.status)) {
|
||||
x_intervals.push(frm.doc.disposal_date);
|
||||
x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: 'Date' }));
|
||||
asset_values.push(0);
|
||||
last_depreciation_date = frm.doc.disposal_date;
|
||||
}
|
||||
|
||||
frm.dashboard.render_graph({
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"column_break_51",
|
||||
"purchase_receipt_amount",
|
||||
"default_finance_book",
|
||||
"depr_entry_posting_status",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
@@ -473,6 +474,16 @@
|
||||
"fieldname": "section_break_36",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Finance Books"
|
||||
},
|
||||
{
|
||||
"fieldname": "depr_entry_posting_status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Depreciation Entry Posting Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nSuccessful\nFailed",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -487,15 +498,21 @@
|
||||
{
|
||||
"group": "Repair",
|
||||
"link_doctype": "Asset Repair",
|
||||
"link_fieldname": "asset_name"
|
||||
"link_fieldname": "asset"
|
||||
},
|
||||
{
|
||||
"group": "Value",
|
||||
"link_doctype": "Asset Value Adjustment",
|
||||
"link_fieldname": "asset"
|
||||
},
|
||||
{
|
||||
"group": "Journal Entry",
|
||||
"link_doctype": "Journal Entry",
|
||||
"link_fieldname": "reference_name",
|
||||
"table_fieldname": "accounts"
|
||||
}
|
||||
],
|
||||
"modified": "2022-07-20 16:22:44.437579",
|
||||
"modified": "2023-01-31 01:03:09.467817",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -27,6 +27,7 @@ from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_depreciation_accounts,
|
||||
get_disposal_account_and_cost_center,
|
||||
is_last_day_of_the_month,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
@@ -79,12 +80,12 @@ class Asset(AccountsController):
|
||||
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
|
||||
)
|
||||
|
||||
def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None):
|
||||
def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None):
|
||||
if self.calculate_depreciation:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
self.make_depreciation_schedule(date_of_sale)
|
||||
self.set_accumulated_depreciation(date_of_sale, date_of_return)
|
||||
self.make_depreciation_schedule(date_of_disposal)
|
||||
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
|
||||
else:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
@@ -223,7 +224,7 @@ class Asset(AccountsController):
|
||||
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
|
||||
)
|
||||
|
||||
def make_depreciation_schedule(self, date_of_sale):
|
||||
def make_depreciation_schedule(self, date_of_disposal):
|
||||
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
|
||||
"schedules"
|
||||
):
|
||||
@@ -279,17 +280,17 @@ class Asset(AccountsController):
|
||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_sale:
|
||||
if date_of_disposal:
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book, depreciation_amount, from_date, date_of_sale
|
||||
finance_book, depreciation_amount, from_date, date_of_disposal
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": date_of_sale,
|
||||
"schedule_date": date_of_disposal,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
@@ -364,6 +365,9 @@ class Asset(AccountsController):
|
||||
},
|
||||
)
|
||||
|
||||
if len(self.get("finance_books")) > 1 and any(start):
|
||||
self.sort_depreciation_schedule()
|
||||
|
||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||
# JE: Journal Entry, FB: Finance Book
|
||||
def clear_depreciation_schedule(self):
|
||||
@@ -399,6 +403,14 @@ class Asset(AccountsController):
|
||||
|
||||
return start
|
||||
|
||||
def sort_depreciation_schedule(self):
|
||||
self.schedules = sorted(
|
||||
self.schedules, key=lambda s: (int(s.finance_book_id), getdate(s.schedule_date))
|
||||
)
|
||||
|
||||
for idx, s in enumerate(self.schedules, 1):
|
||||
s.idx = idx
|
||||
|
||||
def get_from_date(self, finance_book):
|
||||
if not self.get("schedules"):
|
||||
return self.available_for_use_date
|
||||
@@ -531,7 +543,7 @@ class Asset(AccountsController):
|
||||
return True
|
||||
|
||||
def set_accumulated_depreciation(
|
||||
self, date_of_sale=None, date_of_return=None, ignore_booked_entry=False
|
||||
self, date_of_disposal=None, date_of_return=None, ignore_booked_entry=False
|
||||
):
|
||||
straight_line_idx = [
|
||||
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
|
||||
@@ -544,7 +556,9 @@ class Asset(AccountsController):
|
||||
|
||||
if int(d.finance_book_id) not in finance_books:
|
||||
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
|
||||
value_after_depreciation = flt(
|
||||
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation
|
||||
)
|
||||
finance_books.append(int(d.finance_book_id))
|
||||
|
||||
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
|
||||
@@ -554,7 +568,7 @@ class Asset(AccountsController):
|
||||
if (
|
||||
straight_line_idx
|
||||
and i == max(straight_line_idx) - 1
|
||||
and not date_of_sale
|
||||
and not date_of_disposal
|
||||
and not date_of_return
|
||||
):
|
||||
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
|
||||
@@ -569,9 +583,6 @@ class Asset(AccountsController):
|
||||
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
|
||||
)
|
||||
|
||||
def get_value_after_depreciation(self, idx):
|
||||
return flt(self.get("finance_books")[cint(idx) - 1].value_after_depreciation)
|
||||
|
||||
def validate_expected_value_after_useful_life(self):
|
||||
for row in self.get("finance_books"):
|
||||
accumulated_depreciation_after_full_schedule = [
|
||||
@@ -626,15 +637,20 @@ class Asset(AccountsController):
|
||||
movement.cancel()
|
||||
|
||||
def delete_depreciation_entries(self):
|
||||
for d in self.get("schedules"):
|
||||
if d.journal_entry:
|
||||
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
|
||||
d.db_set("journal_entry", None)
|
||||
if self.calculate_depreciation:
|
||||
for d in self.get("schedules"):
|
||||
if d.journal_entry:
|
||||
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
|
||||
else:
|
||||
depr_entries = self.get_manual_depreciation_entries()
|
||||
|
||||
self.db_set(
|
||||
"value_after_depreciation",
|
||||
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
|
||||
)
|
||||
for depr_entry in depr_entries or []:
|
||||
frappe.get_doc("Journal Entry", depr_entry.name).cancel()
|
||||
|
||||
self.db_set(
|
||||
"value_after_depreciation",
|
||||
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
|
||||
)
|
||||
|
||||
def set_status(self, status=None):
|
||||
"""Get and update status"""
|
||||
@@ -665,6 +681,19 @@ class Asset(AccountsController):
|
||||
status = "Cancelled"
|
||||
return status
|
||||
|
||||
def get_value_after_depreciation(self, finance_book=None):
|
||||
if not self.calculate_depreciation:
|
||||
return flt(self.value_after_depreciation, self.precision("gross_purchase_amount"))
|
||||
|
||||
if not finance_book:
|
||||
return flt(
|
||||
self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount")
|
||||
)
|
||||
|
||||
for row in self.get("finance_books"):
|
||||
if finance_book == row.finance_book:
|
||||
return flt(row.value_after_depreciation, self.precision("gross_purchase_amount"))
|
||||
|
||||
def get_default_finance_book_idx(self):
|
||||
if not self.get("default_finance_book") and self.company:
|
||||
self.default_finance_book = erpnext.get_default_finance_book(self.company)
|
||||
@@ -674,6 +703,24 @@ class Asset(AccountsController):
|
||||
if d.finance_book == self.default_finance_book:
|
||||
return cint(d.idx) - 1
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_manual_depreciation_entries(self):
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(self)
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
|
||||
records = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.voucher_no.as_("name"), gle.debit.as_("value"), gle.posting_date)
|
||||
.where(gle.against_voucher == self.name)
|
||||
.where(gle.account == depreciation_expense_account)
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.orderby(gle.posting_date)
|
||||
).run(as_dict=True)
|
||||
|
||||
return records
|
||||
|
||||
def validate_make_gl_entry(self):
|
||||
purchase_document = self.get_purchase_document()
|
||||
if not purchase_document:
|
||||
@@ -838,7 +885,6 @@ def update_maintenance_status():
|
||||
|
||||
|
||||
def make_post_gl_entry():
|
||||
|
||||
asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"])
|
||||
|
||||
for asset_category in asset_categories:
|
||||
@@ -991,7 +1037,7 @@ def make_journal_entry(asset_name):
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.db.get_value(
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
@@ -1058,6 +1104,13 @@ def is_cwip_accounting_enabled(asset_category):
|
||||
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
return asset.get_value_after_depreciation(finance_book)
|
||||
|
||||
|
||||
def get_total_days(date, frequency):
|
||||
period_start_date = add_months(date, cint(frequency) * -1)
|
||||
|
||||
@@ -1067,12 +1120,6 @@ def get_total_days(date, frequency):
|
||||
return date_diff(date, period_start_date)
|
||||
|
||||
|
||||
def is_last_day_of_the_month(date):
|
||||
last_day_of_the_month = get_last_day(date)
|
||||
|
||||
return getdate(last_day_of_the_month) == getdate(date)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_depreciation_amount(asset, depreciable_value, row):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
|
||||
@@ -4,7 +4,17 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, getdate, today
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
flt,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
today,
|
||||
)
|
||||
from frappe.utils.user import get_users_with_role
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
@@ -20,9 +30,22 @@ def post_depreciation_entries(date=None):
|
||||
|
||||
if not date:
|
||||
date = today()
|
||||
for asset in get_depreciable_assets(date):
|
||||
make_depreciation_entry(asset, date)
|
||||
frappe.db.commit()
|
||||
|
||||
failed_asset_names = []
|
||||
|
||||
for asset_name in get_depreciable_assets(date):
|
||||
try:
|
||||
make_depreciation_entry(asset_name, date)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
failed_asset_names.append(asset_name)
|
||||
|
||||
if failed_asset_names:
|
||||
set_depr_entry_posting_status_for_failed_assets(failed_asset_names)
|
||||
notify_depr_entry_posting_error(failed_asset_names)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_depreciable_assets(date):
|
||||
@@ -121,6 +144,8 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
finance_books.value_after_depreciation -= d.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful")
|
||||
|
||||
asset.set_status()
|
||||
|
||||
return asset
|
||||
@@ -184,6 +209,42 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation
|
||||
return credit_account, debit_account
|
||||
|
||||
|
||||
def set_depr_entry_posting_status_for_failed_assets(failed_asset_names):
|
||||
for asset_name in failed_asset_names:
|
||||
frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Failed")
|
||||
|
||||
|
||||
def notify_depr_entry_posting_error(failed_asset_names):
|
||||
recipients = get_users_with_role("Accounts Manager")
|
||||
|
||||
if not recipients:
|
||||
recipients = get_users_with_role("System Manager")
|
||||
|
||||
subject = _("Error while posting depreciation entries")
|
||||
|
||||
asset_links = get_comma_separated_asset_links(failed_asset_names)
|
||||
|
||||
message = (
|
||||
_("Hi,")
|
||||
+ "<br>"
|
||||
+ _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
|
||||
+ "."
|
||||
)
|
||||
|
||||
frappe.sendmail(recipients=recipients, subject=subject, message=message)
|
||||
|
||||
|
||||
def get_comma_separated_asset_links(asset_names):
|
||||
asset_links = []
|
||||
|
||||
for asset_name in asset_names:
|
||||
asset_links.append(get_link_to_form("Asset", asset_name))
|
||||
|
||||
asset_links = ", ".join(asset_links)
|
||||
|
||||
return asset_links
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def scrap_asset(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
@@ -195,6 +256,11 @@ def scrap_asset(asset_name):
|
||||
_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)
|
||||
)
|
||||
|
||||
date = today()
|
||||
|
||||
depreciate_asset(asset, date)
|
||||
asset.reload()
|
||||
|
||||
depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, "series_for_depreciation_entry"
|
||||
)
|
||||
@@ -202,7 +268,7 @@ def scrap_asset(asset_name):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = today()
|
||||
je.posting_date = date
|
||||
je.company = asset.company
|
||||
je.remark = "Scrap Entry for asset {0}".format(asset_name)
|
||||
|
||||
@@ -213,7 +279,7 @@ def scrap_asset(asset_name):
|
||||
je.flags.ignore_permissions = True
|
||||
je.submit()
|
||||
|
||||
frappe.db.set_value("Asset", asset_name, "disposal_date", today())
|
||||
frappe.db.set_value("Asset", asset_name, "disposal_date", date)
|
||||
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
|
||||
asset.set_status("Scrapped")
|
||||
|
||||
@@ -224,6 +290,9 @@ def scrap_asset(asset_name):
|
||||
def restore_asset(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
|
||||
reset_depreciation_schedule(asset, asset.disposal_date)
|
||||
|
||||
je = asset.journal_entry_for_scrap
|
||||
|
||||
asset.db_set("disposal_date", None)
|
||||
@@ -234,6 +303,99 @@ def restore_asset(asset_name):
|
||||
asset.set_status()
|
||||
|
||||
|
||||
def depreciate_asset(asset, date):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(date_of_disposal=date)
|
||||
asset.save()
|
||||
|
||||
make_depreciation_entry(asset.name, date)
|
||||
|
||||
|
||||
def reset_depreciation_schedule(asset, date):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
asset.prepare_depreciation_data(date_of_return=date)
|
||||
|
||||
modify_depreciation_schedule_for_asset_repairs(asset)
|
||||
asset.save()
|
||||
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
|
||||
)
|
||||
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
asset.prepare_depreciation_data()
|
||||
|
||||
|
||||
def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
row = -1
|
||||
finance_book = asset.get("schedules")[0].get("finance_book")
|
||||
for schedule in asset.get("schedules"):
|
||||
if schedule.finance_book != finance_book:
|
||||
row = 0
|
||||
finance_book = schedule.finance_book
|
||||
else:
|
||||
row += 1
|
||||
|
||||
if schedule.schedule_date == date:
|
||||
if not disposal_was_made_on_original_schedule_date(
|
||||
asset, schedule, row, date
|
||||
) or disposal_happens_in_the_future(date):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
|
||||
|
||||
idx = cint(schedule.finance_book_id)
|
||||
asset.finance_books[idx - 1].value_after_depreciation += depreciation_amount
|
||||
|
||||
asset.save()
|
||||
|
||||
|
||||
def get_depreciation_amount_in_je(journal_entry):
|
||||
if journal_entry.accounts[0].debit_in_account_currency:
|
||||
return journal_entry.accounts[0].debit_in_account_currency
|
||||
else:
|
||||
return journal_entry.accounts[0].credit_in_account_currency
|
||||
|
||||
|
||||
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
||||
def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal):
|
||||
for finance_book in asset.get("finance_books"):
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
orginal_schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if is_last_day_of_the_month(finance_book.depreciation_start_date):
|
||||
orginal_schedule_date = get_last_day(orginal_schedule_date)
|
||||
|
||||
if orginal_schedule_date == posting_date_of_disposal:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def disposal_happens_in_the_future(posting_date_of_disposal):
|
||||
if posting_date_of_disposal > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
|
||||
(
|
||||
fixed_asset_account,
|
||||
@@ -307,18 +469,8 @@ def get_asset_details(asset, finance_book=None):
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
idx = 1
|
||||
if finance_book:
|
||||
for d in asset.finance_books:
|
||||
if d.finance_book == finance_book:
|
||||
idx = d.idx
|
||||
break
|
||||
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
|
||||
|
||||
value_after_depreciation = (
|
||||
asset.finance_books[idx - 1].value_after_depreciation
|
||||
if asset.calculate_depreciation
|
||||
else asset.value_after_depreciation
|
||||
)
|
||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||
|
||||
return (
|
||||
@@ -358,3 +510,9 @@ def get_disposal_account_and_cost_center(company):
|
||||
frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company))
|
||||
|
||||
return disposal_account, depreciation_cost_center
|
||||
|
||||
|
||||
def is_last_day_of_the_month(date):
|
||||
last_day_of_the_month = get_last_day(date)
|
||||
|
||||
return getdate(last_day_of_the_month) == getdate(date)
|
||||
|
||||
@@ -4,11 +4,22 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cstr,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
is_last_day_of_the_month,
|
||||
post_depreciation_entries,
|
||||
restore_asset,
|
||||
scrap_asset,
|
||||
@@ -153,28 +164,59 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(doc.items[0].is_fixed_asset, 1)
|
||||
|
||||
def test_scrap_asset(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-01-01",
|
||||
purchase_date="2020-01-01",
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
post_depreciation_entries(date=add_months("2020-01-01", 4))
|
||||
post_depreciation_entries(date=add_months(purchase_date, 2))
|
||||
asset.load_from_db()
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
self.assertEquals(accumulated_depr_amount, 18000.0)
|
||||
|
||||
scrap_asset(asset.name)
|
||||
|
||||
asset.load_from_db()
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
self.assertEquals(
|
||||
accumulated_depr_amount,
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Scrapped")
|
||||
self.assertTrue(asset.journal_entry_for_scrap)
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 36000.0, 0.0),
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
@@ -183,7 +225,7 @@ class TestAsset(AssetSetup):
|
||||
order by account""",
|
||||
asset.journal_entry_for_scrap,
|
||||
)
|
||||
self.assertEqual(gle, expected_gle)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
restore_asset(asset.name)
|
||||
|
||||
@@ -191,34 +233,57 @@ class TestAsset(AssetSetup):
|
||||
self.assertFalse(asset.journal_entry_for_scrap)
|
||||
self.assertEqual(asset.status, "Partially Depreciated")
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
this_month_depr_amount = 9000.0 if is_last_day_of_the_month(date) else 0
|
||||
|
||||
self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount)
|
||||
|
||||
def test_gle_made_by_asset_sale(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-06-06",
|
||||
purchase_date="2020-01-01",
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=10,
|
||||
depreciation_start_date="2020-12-31",
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
post_depreciation_entries(date="2021-01-01")
|
||||
|
||||
post_depreciation_entries(date=add_months(purchase_date, 2))
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si.customer = "_Test Customer"
|
||||
si.set_posting_time = 1
|
||||
si.posting_date = "2021-10-31"
|
||||
si.due_date = "2021-10-31"
|
||||
si.get("items")[0].rate = 75000
|
||||
si.due_date = nowdate()
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 50490.2, 0.0),
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 0.0, 25490.2),
|
||||
("Debtors - _TC", 75000.0, 0.0),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
flt(57000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
@@ -228,14 +293,9 @@ class TestAsset(AssetSetup):
|
||||
si.name,
|
||||
)
|
||||
|
||||
for i, gle_entry in enumerate(gle):
|
||||
self.assertEqual(gle_entry[0], expected_gle[i][0])
|
||||
self.assertEqual(flt(gle_entry[1], 1), flt(expected_gle[i][1], 1))
|
||||
self.assertEqual(flt(gle_entry[2], 1), flt(expected_gle[i][2], 1))
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
si.load_from_db()
|
||||
si.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
@@ -1351,6 +1411,36 @@ class TestDepreciationBasics(AssetSetup):
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date))
|
||||
|
||||
def test_manual_depreciation_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
is_existing_asset=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-01-30",
|
||||
submit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Submitted")
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 100000)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
)
|
||||
for d in jv.accounts:
|
||||
d.reference_type = "Asset"
|
||||
d.reference_name = asset.name
|
||||
jv.voucher_type = "Depreciation Entry"
|
||||
jv.insert()
|
||||
jv.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 99900)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 100000)
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
@@ -1387,6 +1477,7 @@ def create_asset(**args):
|
||||
"location": args.location or "Test Location",
|
||||
"asset_owner": args.asset_owner or "Company",
|
||||
"is_existing_asset": args.is_existing_asset or 1,
|
||||
"depr_entry_posting_status": args.depr_entry_posting_status or "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.test_asset import (
|
||||
create_asset,
|
||||
create_asset_data,
|
||||
@@ -105,20 +106,20 @@ class TestAssetRepair(unittest.TestCase):
|
||||
|
||||
def test_increase_in_asset_value_due_to_stock_consumption(self):
|
||||
asset = create_asset(calculate_depreciation=1, submit=1)
|
||||
initial_asset_value = get_asset_value(asset)
|
||||
initial_asset_value = get_asset_value_after_depreciation(asset.name)
|
||||
asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1)
|
||||
asset.reload()
|
||||
|
||||
increase_in_asset_value = get_asset_value(asset) - initial_asset_value
|
||||
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)
|
||||
|
||||
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
|
||||
asset = create_asset(calculate_depreciation=1, submit=1)
|
||||
initial_asset_value = get_asset_value(asset)
|
||||
initial_asset_value = get_asset_value_after_depreciation(asset.name)
|
||||
asset_repair = create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1)
|
||||
asset.reload()
|
||||
|
||||
increase_in_asset_value = get_asset_value(asset) - initial_asset_value
|
||||
increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value
|
||||
self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
|
||||
|
||||
def test_purchase_invoice(self):
|
||||
@@ -143,10 +144,6 @@ class TestAssetRepair(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
def get_asset_value(asset):
|
||||
return asset.finance_books[0].value_after_depreciation
|
||||
|
||||
|
||||
def num_of_depreciations(asset):
|
||||
return asset.finance_books[0].total_number_of_depreciations
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
|
||||
set_current_asset_value: function(frm) {
|
||||
if (frm.doc.asset) {
|
||||
frm.call({
|
||||
method: "erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment.get_current_asset_value",
|
||||
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
|
||||
args: {
|
||||
asset: frm.doc.asset,
|
||||
finance_book: frm.doc.finance_book
|
||||
|
||||
@@ -10,7 +10,10 @@ from frappe.utils import cint, date_diff, flt, formatdate, getdate
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import get_depreciation_amount
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_value_after_depreciation,
|
||||
get_depreciation_amount,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||
from erpnext.regional.india.utils import (
|
||||
get_depreciation_amount as get_depreciation_amount_for_india,
|
||||
@@ -45,7 +48,7 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
def set_current_asset_value(self):
|
||||
if not self.current_asset_value and self.asset:
|
||||
self.current_asset_value = get_current_asset_value(self.asset, self.finance_book)
|
||||
self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book)
|
||||
|
||||
def make_depreciation_entry(self):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
@@ -148,12 +151,3 @@ class AssetValueAdjustment(Document):
|
||||
for asset_data in asset.schedules:
|
||||
if not asset_data.journal_entry:
|
||||
asset_data.db_update()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_current_asset_value(asset, finance_book=None):
|
||||
cond = {"parent": asset, "parenttype": "Asset"}
|
||||
if finance_book:
|
||||
cond.update({"finance_book": finance_book})
|
||||
|
||||
return frappe.db.get_value("Asset Finance Book", cond, "value_after_depreciation")
|
||||
|
||||
@@ -6,10 +6,8 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import add_days, get_last_day, nowdate
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_data
|
||||
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
|
||||
get_current_asset_value,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
|
||||
@@ -43,7 +41,7 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
)
|
||||
asset_doc.submit()
|
||||
|
||||
current_value = get_current_asset_value(asset_doc.name)
|
||||
current_value = get_asset_value_after_depreciation(asset_doc.name)
|
||||
self.assertEqual(current_value, 100000.0)
|
||||
|
||||
def test_asset_depreciation_value_adjustment(self):
|
||||
@@ -73,7 +71,7 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
)
|
||||
asset_doc.submit()
|
||||
|
||||
current_value = get_current_asset_value(asset_doc.name)
|
||||
current_value = get_asset_value_after_depreciation(asset_doc.name)
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cstr, flt, formatdate, getdate
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
@@ -11,6 +12,8 @@ from erpnext.accounts.report.financial_statements import (
|
||||
get_period_list,
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -85,7 +88,9 @@ def get_data(filters):
|
||||
"asset_name",
|
||||
"status",
|
||||
"department",
|
||||
"company",
|
||||
"cost_center",
|
||||
"calculate_depreciation",
|
||||
"purchase_receipt",
|
||||
"asset_category",
|
||||
"purchase_date",
|
||||
@@ -97,12 +102,21 @@ def get_data(filters):
|
||||
]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
assets_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
|
||||
for asset in assets_record:
|
||||
asset_value = (
|
||||
asset.gross_purchase_amount
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(depreciation_amount_map.get(asset.name))
|
||||
)
|
||||
if filters.finance_book:
|
||||
if asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
else:
|
||||
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
|
||||
row = {
|
||||
"asset_id": asset.asset_id,
|
||||
"asset_name": asset.asset_name,
|
||||
@@ -113,7 +127,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": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters),
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
"location": asset.location,
|
||||
"asset_category": asset.asset_category,
|
||||
@@ -170,6 +184,15 @@ def prepare_chart_data(data, filters):
|
||||
}
|
||||
|
||||
|
||||
def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
|
||||
if asset.calculate_depreciation:
|
||||
depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
else:
|
||||
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
|
||||
|
||||
return flt(depr_amount, 2)
|
||||
|
||||
|
||||
def get_finance_book_value_map(filters):
|
||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
|
||||
@@ -189,6 +212,31 @@ def get_finance_book_value_map(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_manual_depreciation_amount_of_asset(asset, filters):
|
||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(asset)
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(Sum(gle.debit))
|
||||
.where(gle.against_voucher == asset.asset_id)
|
||||
.where(gle.account == depreciation_expense_account)
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(gle.posting_date <= date)
|
||||
).run()
|
||||
|
||||
if result and result[0] and result[0][0]:
|
||||
depr_amount = result[0][0]
|
||||
else:
|
||||
depr_amount = 0
|
||||
|
||||
return depr_amount
|
||||
|
||||
|
||||
def get_purchase_receipt_supplier_map():
|
||||
return frappe._dict(
|
||||
frappe.db.sql(
|
||||
|
||||
@@ -1239,6 +1239,11 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_variant_item_po(self):
|
||||
po = create_purchase_order(item_code="_Test Variant Item", qty=1, rate=100, do_not_save=1)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
|
||||
|
||||
def make_pr_against_po(po, received_qty=0):
|
||||
pr = make_purchase_receipt(po)
|
||||
@@ -1342,8 +1347,8 @@ def create_purchase_order(**args):
|
||||
},
|
||||
)
|
||||
|
||||
po.set_missing_values()
|
||||
if not args.do_not_save:
|
||||
po.set_missing_values()
|
||||
po.insert()
|
||||
if not args.do_not_submit:
|
||||
if po.is_subcontracted == "Yes":
|
||||
|
||||
@@ -383,7 +383,7 @@ class AccountsController(TransactionBase):
|
||||
self.get("inter_company_reference")
|
||||
or self.get("inter_company_invoice_reference")
|
||||
or self.get("inter_company_order_reference")
|
||||
):
|
||||
) and not self.get("is_return"):
|
||||
msg = _("Internal Sale or Delivery Reference missing.")
|
||||
msg += _("Please create purchase from internal sale or delivery document itself")
|
||||
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
|
||||
|
||||
@@ -755,6 +755,8 @@ class BuyingController(StockController, Subcontracting):
|
||||
asset.purchase_date = self.posting_date
|
||||
asset.supplier = self.supplier
|
||||
elif self.docstatus == 2:
|
||||
if asset.docstatus == 2:
|
||||
continue
|
||||
if asset.docstatus == 0:
|
||||
asset.set(field, None)
|
||||
asset.supplier = None
|
||||
|
||||
@@ -25,7 +25,7 @@ class SellingController(StockController):
|
||||
def onload(self):
|
||||
super(SellingController, self).onload()
|
||||
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
for item in self.get("items"):
|
||||
for item in self.get("items") + (self.get("packed_items") or []):
|
||||
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -58,7 +58,7 @@ status_map = {
|
||||
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1",
|
||||
],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed'"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
["On Hold", "eval:self.status=='On Hold'"],
|
||||
],
|
||||
"Purchase Order": [
|
||||
@@ -79,7 +79,7 @@ status_map = {
|
||||
["Delivered", "eval:self.status=='Delivered'"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["On Hold", "eval:self.status=='On Hold'"],
|
||||
["Closed", "eval:self.status=='Closed'"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
"Delivery Note": [
|
||||
["Draft", None],
|
||||
@@ -87,7 +87,7 @@ status_map = {
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed'"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
"Purchase Receipt": [
|
||||
["Draft", None],
|
||||
@@ -95,7 +95,7 @@ status_map = {
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed'"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
"Material Request": [
|
||||
["Draft", None],
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-06-29 18:27:02.832979",
|
||||
"modified": "2022-12-28 16:35:34.377575",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment",
|
||||
@@ -121,16 +121,6 @@
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Guest",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
from collections import Counter
|
||||
|
||||
import frappe
|
||||
import frappe.share
|
||||
from frappe import _
|
||||
from frappe.desk.form.assign_to import add as add_assignment
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_url, getdate
|
||||
from frappe.utils.verified_command import get_signed_params
|
||||
@@ -118,21 +120,18 @@ class Appointment(Document):
|
||||
self.party = lead.name
|
||||
|
||||
def auto_assign(self):
|
||||
from frappe.desk.form.assign_to import add as add_assignemnt
|
||||
|
||||
existing_assignee = self.get_assignee_from_latest_opportunity()
|
||||
if existing_assignee:
|
||||
# If the latest opportunity is assigned to someone
|
||||
# Assign the appointment to the same
|
||||
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]})
|
||||
self.assign_agent(existing_assignee)
|
||||
return
|
||||
if self._assign:
|
||||
return
|
||||
available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time))
|
||||
for agent in available_agents:
|
||||
if _check_agent_availability(agent, self.scheduled_time):
|
||||
agent = agent[0]
|
||||
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
|
||||
self.assign_agent(agent[0])
|
||||
break
|
||||
|
||||
def get_assignee_from_latest_opportunity(self):
|
||||
@@ -187,9 +186,15 @@ class Appointment(Document):
|
||||
params = {"email": self.customer_email, "appointment": self.name}
|
||||
return get_url(verify_route + "?" + get_signed_params(params))
|
||||
|
||||
def assign_agent(self, agent):
|
||||
if not frappe.has_permission(doc=self, user=agent):
|
||||
frappe.share.add(self.doctype, self.name, agent, flags={"ignore_share_permission": True})
|
||||
|
||||
add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
|
||||
|
||||
|
||||
def _get_agents_sorted_by_asc_workload(date):
|
||||
appointments = frappe.db.get_list("Appointment", fields="*")
|
||||
appointments = frappe.get_all("Appointment", fields="*")
|
||||
agent_list = _get_agent_list_as_strings()
|
||||
if not appointments:
|
||||
return agent_list
|
||||
@@ -214,7 +219,7 @@ def _get_agent_list_as_strings():
|
||||
|
||||
|
||||
def _check_agent_availability(agent_email, scheduled_time):
|
||||
appointemnts_at_scheduled_time = frappe.get_list(
|
||||
appointemnts_at_scheduled_time = frappe.get_all(
|
||||
"Appointment", filters={"scheduled_time": scheduled_time}
|
||||
)
|
||||
for appointment in appointemnts_at_scheduled_time:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2019-08-27 10:56:48.309824",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -101,7 +102,8 @@
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"modified": "2019-11-26 12:14:17.669366",
|
||||
"links": [],
|
||||
"modified": "2022-12-28 16:41:28.773090",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment Booking Settings",
|
||||
@@ -117,13 +119,6 @@
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Guest",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
|
||||
@@ -173,7 +173,10 @@ class TestWebsiteItem(unittest.TestCase):
|
||||
# Website Item Portal Tests Begin
|
||||
|
||||
def test_website_item_breadcrumbs(self):
|
||||
"Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group."
|
||||
"""
|
||||
Check if breadcrumbs include homepage, product listing navigation page,
|
||||
parent item group(s) and item group
|
||||
"""
|
||||
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
|
||||
|
||||
item_code = "Test Breadcrumb Item"
|
||||
@@ -196,7 +199,7 @@ class TestWebsiteItem(unittest.TestCase):
|
||||
breadcrumbs = get_parent_item_groups(item.item_group)
|
||||
|
||||
self.assertEqual(breadcrumbs[0]["name"], "Home")
|
||||
self.assertEqual(breadcrumbs[1]["name"], "Shop by Category")
|
||||
self.assertEqual(breadcrumbs[1]["name"], "All Products")
|
||||
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
|
||||
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
|
||||
|
||||
|
||||
@@ -345,7 +345,8 @@
|
||||
"image_field": "website_image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-28 17:10:30.613251",
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-09-13 04:05:11.614087",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Website Item",
|
||||
|
||||
@@ -111,34 +111,30 @@ def get_data(filters: Filters) -> List:
|
||||
employee.leave_approver
|
||||
)
|
||||
|
||||
if (
|
||||
(leave_approvers and len(leave_approvers) and user in leave_approvers)
|
||||
or (user in ["Administrator", employee.user_id])
|
||||
or ("HR Manager" in frappe.get_roles(user))
|
||||
):
|
||||
if len(active_employees) > 1:
|
||||
row = frappe._dict()
|
||||
row.employee = employee.name
|
||||
row.employee_name = employee.employee_name
|
||||
if len(active_employees) > 1:
|
||||
row = frappe._dict()
|
||||
|
||||
leaves_taken = (
|
||||
get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1
|
||||
)
|
||||
row.employee = employee.name
|
||||
row.employee_name = employee.employee_name
|
||||
|
||||
new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
|
||||
filters.from_date, filters.to_date, employee.name, leave_type
|
||||
)
|
||||
opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
|
||||
leaves_taken = (
|
||||
get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1
|
||||
)
|
||||
|
||||
row.leaves_allocated = new_allocation
|
||||
row.leaves_expired = expired_leaves
|
||||
row.opening_balance = opening
|
||||
row.leaves_taken = leaves_taken
|
||||
new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
|
||||
filters.from_date, filters.to_date, employee.name, leave_type
|
||||
)
|
||||
opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
|
||||
|
||||
# not be shown on the basis of days left it create in user mind for carry_forward leave
|
||||
row.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken)
|
||||
row.indent = 1
|
||||
data.append(row)
|
||||
row.leaves_allocated = new_allocation
|
||||
row.leaves_expired = expired_leaves
|
||||
row.opening_balance = opening
|
||||
row.leaves_taken = leaves_taken
|
||||
|
||||
# not be shown on the basis of days left it create in user mind for carry_forward leave
|
||||
row.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken)
|
||||
row.indent = 1
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -65,21 +65,16 @@ def get_data(filters, leave_types):
|
||||
if employee.leave_approver:
|
||||
leave_approvers.append(employee.leave_approver)
|
||||
|
||||
if (
|
||||
(len(leave_approvers) and user in leave_approvers)
|
||||
or (user in ["Administrator", employee.user_id])
|
||||
or ("HR Manager" in frappe.get_roles(user))
|
||||
):
|
||||
row = [employee.name, employee.employee_name, employee.department]
|
||||
available_leave = get_leave_details(employee.name, filters.date)
|
||||
for leave_type in leave_types:
|
||||
remaining = 0
|
||||
if leave_type in available_leave["leave_allocation"]:
|
||||
# opening balance
|
||||
remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"]
|
||||
row = [employee.name, employee.employee_name, employee.department]
|
||||
available_leave = get_leave_details(employee.name, filters.date)
|
||||
for leave_type in leave_types:
|
||||
remaining = 0
|
||||
if leave_type in available_leave["leave_allocation"]:
|
||||
# opening balance
|
||||
remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"]
|
||||
|
||||
row += [remaining]
|
||||
row += [remaining]
|
||||
|
||||
data.append(row)
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
@@ -13,38 +13,24 @@ frappe.query_reports["Work Order Summary"] = {
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: "fiscal_year",
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.defaults.get_user_default("fiscal_year"),
|
||||
reqd: 1,
|
||||
on_change: function(query_report) {
|
||||
var fiscal_year = query_report.get_values().fiscal_year;
|
||||
if (!fiscal_year) {
|
||||
return;
|
||||
}
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
from_date: fy.year_start_date,
|
||||
to_date: fy.year_end_date
|
||||
});
|
||||
});
|
||||
}
|
||||
label: __("Based On"),
|
||||
fieldname:"based_on",
|
||||
fieldtype: "Select",
|
||||
options: "Creation Date\nPlanned Date\nActual Date",
|
||||
default: "Creation Date"
|
||||
},
|
||||
{
|
||||
label: __("From Posting Date"),
|
||||
fieldname:"from_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.defaults.get_user_default("year_start_date"),
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -3),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("To Posting Date"),
|
||||
fieldname:"to_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.defaults.get_user_default("year_end_date"),
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ def get_data(filters):
|
||||
"sales_order",
|
||||
"production_item",
|
||||
"qty",
|
||||
"creation",
|
||||
"produced_qty",
|
||||
"planned_start_date",
|
||||
"planned_end_date",
|
||||
@@ -47,8 +48,14 @@ def get_data(filters):
|
||||
if filters.get(field):
|
||||
query_filters[field] = filters.get(field)
|
||||
|
||||
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
|
||||
if filters.get("based_on") == "Planned Date":
|
||||
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
|
||||
elif filters.get("based_on") == "Actual Date":
|
||||
query_filters["actual_start_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["actual_end_date"] = ("<=", filters.get("to_date"))
|
||||
else:
|
||||
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
|
||||
|
||||
data = frappe.get_all(
|
||||
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
|
||||
@@ -212,6 +219,12 @@ def get_columns(filters):
|
||||
"options": "Sales Order",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Created On"),
|
||||
"fieldname": "creation",
|
||||
"fieldtype": "Date",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Planned Start Date"),
|
||||
"fieldname": "planned_start_date",
|
||||
|
||||
@@ -374,4 +374,5 @@ erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0})
|
||||
erpnext.patches.v13_0.update_schedule_type_in_loans
|
||||
erpnext.patches.v13_0.update_schedule_type_in_loans
|
||||
erpnext.patches.v13_0.update_asset_value_for_manual_depr_entries
|
||||
|
||||
@@ -7,6 +7,7 @@ from erpnext.stock.stock_ledger import update_entries_after
|
||||
|
||||
def execute():
|
||||
doctypes_to_reload = [
|
||||
("setup", "company"),
|
||||
("stock", "repost_item_valuation"),
|
||||
("stock", "stock_entry_detail"),
|
||||
("stock", "purchase_receipt_item"),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import frappe
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
|
||||
|
||||
def execute():
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
aca = frappe.qb.DocType("Asset Category Account")
|
||||
company = frappe.qb.DocType("Company")
|
||||
|
||||
asset_total_depr_value_map = (
|
||||
frappe.qb.from_(gle)
|
||||
.join(asset)
|
||||
.on(gle.against_voucher == asset.name)
|
||||
.join(aca)
|
||||
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
|
||||
.join(company)
|
||||
.on(company.name == asset.company)
|
||||
.select(Sum(gle.debit).as_("value"), asset.name.as_("asset_name"))
|
||||
.where(
|
||||
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
)
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(asset.docstatus == 1)
|
||||
.where(asset.calculate_depreciation == 0)
|
||||
.groupby(asset.name)
|
||||
)
|
||||
|
||||
frappe.qb.update(asset).join(asset_total_depr_value_map).on(
|
||||
asset_total_depr_value_map.asset_name == asset.name
|
||||
).set(
|
||||
asset.value_after_depreciation, asset.value_after_depreciation - asset_total_depr_value_map.value
|
||||
).where(
|
||||
asset.docstatus == 1
|
||||
).where(
|
||||
asset.calculate_depreciation == 0
|
||||
).run()
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import datetime
|
||||
import math
|
||||
|
||||
@@ -316,6 +315,8 @@ class SalarySlip(TransactionBase):
|
||||
)
|
||||
|
||||
working_days = date_diff(self.end_date, self.start_date) + 1
|
||||
working_days_list = [add_days(self.start_date, i) for i in range(working_days)]
|
||||
|
||||
if for_preview:
|
||||
self.total_working_days = working_days
|
||||
self.payment_days = working_days
|
||||
@@ -325,6 +326,8 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
if not cint(include_holidays_in_total_working_days):
|
||||
working_days -= len(holidays)
|
||||
working_days_list = [cstr(day) for day in working_days_list if cstr(day) not in holidays]
|
||||
|
||||
if working_days < 0:
|
||||
frappe.throw(_("There are more holidays than working days this month."))
|
||||
|
||||
@@ -335,7 +338,7 @@ class SalarySlip(TransactionBase):
|
||||
actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays)
|
||||
self.absent_days = absent
|
||||
else:
|
||||
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days)
|
||||
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days_list)
|
||||
|
||||
if not lwp:
|
||||
lwp = actual_lwp
|
||||
@@ -458,16 +461,15 @@ class SalarySlip(TransactionBase):
|
||||
def get_holidays_for_employee(self, start_date, end_date):
|
||||
return get_holiday_dates_for_employee(self.employee, start_date, end_date)
|
||||
|
||||
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
|
||||
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days_list):
|
||||
lwp = 0
|
||||
holidays = "','".join(holidays)
|
||||
|
||||
daily_wages_fraction_for_half_day = (
|
||||
flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
|
||||
)
|
||||
|
||||
for d in range(working_days):
|
||||
date = add_days(cstr(getdate(self.start_date)), d)
|
||||
leave = get_lwp_or_ppl_for_date(date, self.employee, holidays)
|
||||
for d in working_days_list:
|
||||
leave = get_lwp_or_ppl_for_date(d, self.employee, holidays)
|
||||
|
||||
if leave:
|
||||
equivalent_lwp_count = 0
|
||||
|
||||
@@ -115,24 +115,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
calculate_item_values: function() {
|
||||
let me = this;
|
||||
if (!this.discount_amount_applied) {
|
||||
$.each(this.frm.doc["items"] || [], function(i, item) {
|
||||
for (const item of this.frm.doc.items || []) {
|
||||
frappe.model.round_floats_in(item);
|
||||
item.net_rate = item.rate;
|
||||
|
||||
if ((!item.qty) && me.frm.doc.is_return) {
|
||||
item.amount = flt(item.rate * -1, precision("amount", item));
|
||||
} else if ((!item.qty) && me.frm.doc.is_debit_note) {
|
||||
item.amount = flt(item.rate, precision("amount", item));
|
||||
} else {
|
||||
item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||
}
|
||||
|
||||
item.net_amount = item.amount;
|
||||
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
|
||||
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||
item.item_tax_amount = 0.0;
|
||||
item.total_weight = flt(item.weight_per_unit * item.stock_qty);
|
||||
|
||||
me.set_in_company_currency(item, ["price_list_rate", "rate", "amount", "net_rate", "net_amount"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1781,6 +1781,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
var me = this;
|
||||
var valid = true;
|
||||
|
||||
if (frappe.flags.ignore_company_party_validation) {
|
||||
return valid;
|
||||
}
|
||||
|
||||
$.each(["company", "customer"], function(i, fieldname) {
|
||||
if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && me.frm.doc.doctype != "Purchase Order") {
|
||||
if (!me.frm.doc[fieldname]) {
|
||||
|
||||
@@ -466,7 +466,20 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`);
|
||||
const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision;
|
||||
|
||||
this.data = [];
|
||||
this.data = frm.doc[opts.child_docname].map((d) => {
|
||||
return {
|
||||
"docname": d.name,
|
||||
"name": d.name,
|
||||
"item_code": d.item_code,
|
||||
"delivery_date": d.delivery_date,
|
||||
"schedule_date": d.schedule_date,
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"qty": d.qty,
|
||||
"rate": d.rate,
|
||||
"uom": d.uom
|
||||
}
|
||||
});
|
||||
|
||||
const fields = [{
|
||||
fieldtype:'Data',
|
||||
fieldname:"docname",
|
||||
@@ -559,7 +572,7 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
})
|
||||
}
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
new frappe.ui.Dialog({
|
||||
title: __("Update Items"),
|
||||
fields: [
|
||||
{
|
||||
@@ -595,24 +608,7 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
refresh_field("items");
|
||||
},
|
||||
primary_action_label: __('Update')
|
||||
});
|
||||
|
||||
frm.doc[opts.child_docname].forEach(d => {
|
||||
dialog.fields_dict.trans_items.df.data.push({
|
||||
"docname": d.name,
|
||||
"name": d.name,
|
||||
"item_code": d.item_code,
|
||||
"delivery_date": d.delivery_date,
|
||||
"schedule_date": d.schedule_date,
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"qty": d.qty,
|
||||
"rate": d.rate,
|
||||
"uom": d.uom
|
||||
});
|
||||
this.data = dialog.fields_dict.trans_items.df.data;
|
||||
dialog.fields_dict.trans_items.grid.refresh();
|
||||
})
|
||||
dialog.show();
|
||||
}).show();
|
||||
}
|
||||
|
||||
erpnext.utils.map_current_doc = function(opts) {
|
||||
|
||||
@@ -71,7 +71,11 @@ def validate_eligibility(doc):
|
||||
|
||||
# if export invoice, then taxes can be empty
|
||||
# invoice can only be ineligible if no taxes applied and is not an export invoice
|
||||
no_taxes_applied = not doc.get("taxes") and not doc.get("gst_category") == "Overseas"
|
||||
no_taxes_applied = (
|
||||
not doc.get("taxes")
|
||||
and not doc.get("gst_category") == "Overseas"
|
||||
and not doc.get("gst_category") == "SEZ"
|
||||
)
|
||||
has_non_gst_item = any(d for d in doc.get("items", []) if d.get("is_non_gst"))
|
||||
|
||||
if (
|
||||
|
||||
@@ -84,11 +84,15 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Sales Order"),
|
||||
this.frm.cscript["Make Sales Order"],
|
||||
__("Create")
|
||||
);
|
||||
if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation
|
||||
|| (!doc.valid_till)
|
||||
|| frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
|
||||
this.frm.add_custom_button(
|
||||
__("Sales Order"),
|
||||
this.frm.cscript["Make Sales Order"],
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
if(doc.status!=="Ordered") {
|
||||
this.frm.add_custom_button(__('Set as Lost'), () => {
|
||||
|
||||
@@ -191,14 +191,18 @@ def get_list_context(context=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_order(source_name, target_doc=None):
|
||||
quotation = frappe.db.get_value(
|
||||
"Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
|
||||
)
|
||||
if quotation.valid_till and (
|
||||
quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
|
||||
def make_sales_order(source_name: str, target_doc=None):
|
||||
if not frappe.db.get_singles_value(
|
||||
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
|
||||
):
|
||||
frappe.throw(_("Validity period of this quotation has ended."))
|
||||
quotation = frappe.db.get_value(
|
||||
"Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
|
||||
)
|
||||
if quotation.valid_till and (
|
||||
quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
|
||||
):
|
||||
frappe.throw(_("Validity period of this quotation has ended."))
|
||||
|
||||
return _make_sales_order(source_name, target_doc)
|
||||
|
||||
|
||||
|
||||
@@ -118,18 +118,31 @@ class TestQuotation(FrappeTestCase):
|
||||
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
|
||||
)
|
||||
|
||||
def test_valid_till(self):
|
||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||
|
||||
def test_valid_till_before_transaction_date(self):
|
||||
quotation = frappe.copy_doc(test_records[0])
|
||||
quotation.valid_till = add_days(quotation.transaction_date, -1)
|
||||
self.assertRaises(frappe.ValidationError, quotation.validate)
|
||||
|
||||
def test_so_from_expired_quotation(self):
|
||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0
|
||||
)
|
||||
|
||||
quotation = frappe.copy_doc(test_records[0])
|
||||
quotation.valid_till = add_days(nowdate(), -1)
|
||||
quotation.insert()
|
||||
quotation.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1
|
||||
)
|
||||
|
||||
make_sales_order(quotation.name)
|
||||
|
||||
def test_shopping_cart_without_website_item(self):
|
||||
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):
|
||||
frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete()
|
||||
|
||||
@@ -14,7 +14,6 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Quotation": ["items", "prevdoc_docname"],
|
||||
"Material Request": ["items", "material_request"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
|
||||
@@ -546,6 +546,42 @@ class TestSalesOrder(FrappeTestCase):
|
||||
workflow.is_active = 0
|
||||
workflow.save()
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
|
||||
bundle_item.append(
|
||||
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||
)
|
||||
bundle_item.save(ignore_permissions=True)
|
||||
|
||||
make_item("_Packed Item New 1", {"is_stock_item": 1})
|
||||
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
|
||||
|
||||
so = make_sales_order(
|
||||
item_code="_Test Product Bundle Item New",
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
transaction_date=add_days(nowdate(), -1),
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
|
||||
|
||||
bin_details = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
|
||||
["actual_qty", "projected_qty", "ordered_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
so.transaction_date = nowdate()
|
||||
so.save()
|
||||
|
||||
packed_item = so.packed_items[0]
|
||||
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
|
||||
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
|
||||
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
|
||||
|
||||
def test_update_child_product_bundle(self):
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Product Bundle Item"):
|
||||
|
||||
@@ -809,7 +809,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-27 03:15:34.366563",
|
||||
"modified": "2022-12-25 02:51:10.247569",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
@@ -820,4 +820,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"sales_update_frequency",
|
||||
"allow_multiple_items",
|
||||
"allow_against_multiple_purchase_orders",
|
||||
"hide_tax_id"
|
||||
"hide_tax_id",
|
||||
"allow_sales_order_creation_for_expired_quotation"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -199,6 +200,12 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Contract Naming By",
|
||||
"options": "Party Name\nNaming Series"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_sales_order_creation_for_expired_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Sales Order Creation For Expired Quotation"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@@ -206,7 +213,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-28 12:18:06.768403",
|
||||
"modified": "2023-02-04 12:37:53.380857",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
|
||||
@@ -41,8 +41,20 @@ def get_columns(filters):
|
||||
{"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150},
|
||||
{"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150},
|
||||
{"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100},
|
||||
{"label": _("Rate"), "fieldname": "rate", "options": "Currency", "width": 120},
|
||||
{"label": _("Amount"), "fieldname": "amount", "options": "Currency", "width": 120},
|
||||
{
|
||||
"label": _("Rate"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Amount"),
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Sales Order"),
|
||||
"fieldtype": "Link",
|
||||
@@ -93,8 +105,9 @@ def get_columns(filters):
|
||||
},
|
||||
{
|
||||
"label": _("Billed Amount"),
|
||||
"fieldtype": "currency",
|
||||
"fieldtype": "Currency",
|
||||
"fieldname": "billed_amount",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
@@ -104,6 +117,13 @@ def get_columns(filters):
|
||||
"options": "Company",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "currency",
|
||||
"options": "Currency",
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -141,31 +161,12 @@ def get_data(filters):
|
||||
"billed_amount": flt(record.get("billed_amt")),
|
||||
"company": record.get("company"),
|
||||
}
|
||||
row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency")
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
if filters.get("item_group"):
|
||||
conditions += "AND so_item.item_group = %s" % frappe.db.escape(filters.item_group)
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions += "AND so.transaction_date >= '%s'" % filters.from_date
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions += "AND so.transaction_date <= '%s'" % filters.to_date
|
||||
|
||||
if filters.get("item_code"):
|
||||
conditions += "AND so_item.item_code = %s" % frappe.db.escape(filters.item_code)
|
||||
|
||||
if filters.get("customer"):
|
||||
conditions += "AND so.customer = %s" % frappe.db.escape(filters.customer)
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_customer_details():
|
||||
details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"])
|
||||
customer_details = {}
|
||||
@@ -187,29 +188,50 @@ def get_item_details():
|
||||
|
||||
|
||||
def get_sales_order_details(company_list, filters):
|
||||
conditions = get_conditions(filters)
|
||||
db_so = frappe.qb.DocType("Sales Order")
|
||||
db_so_item = frappe.qb.DocType("Sales Order Item")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
so_item.item_code, so_item.description, so_item.qty,
|
||||
so_item.uom, so_item.base_rate, so_item.base_amount,
|
||||
so.name, so.transaction_date, so.customer,so.territory,
|
||||
so.project, so_item.delivered_qty,
|
||||
so_item.billed_amt, so.company
|
||||
FROM
|
||||
`tabSales Order` so, `tabSales Order Item` so_item
|
||||
WHERE
|
||||
so.name = so_item.parent
|
||||
AND so.company in ({0})
|
||||
AND so.docstatus = 1 {1}
|
||||
""".format(
|
||||
",".join(["%s"] * len(company_list)), conditions
|
||||
),
|
||||
tuple(company_list),
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(db_so)
|
||||
.inner_join(db_so_item)
|
||||
.on(db_so_item.parent == db_so.name)
|
||||
.select(
|
||||
db_so.name,
|
||||
db_so.customer,
|
||||
db_so.transaction_date,
|
||||
db_so.territory,
|
||||
db_so.project,
|
||||
db_so.company,
|
||||
db_so_item.item_code,
|
||||
db_so_item.description,
|
||||
db_so_item.qty,
|
||||
db_so_item.uom,
|
||||
db_so_item.base_rate,
|
||||
db_so_item.base_amount,
|
||||
db_so_item.delivered_qty,
|
||||
(db_so_item.billed_amt * db_so.conversion_rate).as_("billed_amt"),
|
||||
)
|
||||
.where(db_so.docstatus == 1)
|
||||
.where(db_so.company.isin(tuple(company_list)))
|
||||
)
|
||||
|
||||
if filters.get("item_group"):
|
||||
query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_group))
|
||||
|
||||
if filters.get("from_date"):
|
||||
query = query.where(db_so.transaction_date >= filters.from_date)
|
||||
|
||||
if filters.get("to_date"):
|
||||
query = query.where(db_so.transaction_date <= filters.to_date)
|
||||
|
||||
if filters.get("item_code"):
|
||||
query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_code))
|
||||
|
||||
if filters.get("customer"):
|
||||
query = query.where(db_so.customer == filters.customer)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def get_chart_data(data):
|
||||
item_wise_sales_map = {}
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"extends_another_page": 0,
|
||||
"hide_custom": 1,
|
||||
"hide_custom": 0,
|
||||
"icon": "sell",
|
||||
"idx": 0,
|
||||
"is_default": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Selling",
|
||||
"links": [
|
||||
@@ -515,7 +516,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2020-12-01 13:38:35.971277",
|
||||
"modified": "2023-01-28 17:10:02.716760",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling",
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
"fieldname": "route",
|
||||
"fieldtype": "Data",
|
||||
"label": "Route",
|
||||
"no_copy": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
@@ -232,11 +233,10 @@
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"max_attachments": 3,
|
||||
"modified": "2022-03-09 12:27:11.055782",
|
||||
"modified": "2023-01-05 12:21:30.458628",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Item Group",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By fieldname",
|
||||
"nsm_parent_field": "parent_item_group",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -149,12 +149,12 @@ def get_item_for_list_in_html(context):
|
||||
|
||||
|
||||
def get_parent_item_groups(item_group_name, from_item=False):
|
||||
base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"}
|
||||
base_nav_page = {"name": _("All Products"), "route": "/all-products"}
|
||||
|
||||
if from_item and frappe.request.environ.get("HTTP_REFERER"):
|
||||
# base page after 'Home' will vary on Item page
|
||||
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
|
||||
if last_page and last_page in ("shop-by-category", "all-products"):
|
||||
if last_page and last_page == "shop-by-category":
|
||||
base_nav_page_title = " ".join(last_page.split("-")).title()
|
||||
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ def boot_session(bootinfo):
|
||||
frappe.db.get_single_value("Selling Settings", "default_valid_till")
|
||||
)
|
||||
|
||||
bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint(
|
||||
frappe.db.get_single_value(
|
||||
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
|
||||
)
|
||||
)
|
||||
|
||||
# if no company, show a dialog box to create a new company
|
||||
bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0]
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ erpnext.stock.ItemDashboard = Class.extend({
|
||||
let warehouse = unescape(element.attr('data-warehouse'));
|
||||
let actual_qty = unescape(element.attr('data-actual_qty'));
|
||||
let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry')));
|
||||
let entry_type = action === "Move" ? "Material Transfer" : null;
|
||||
let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt";
|
||||
|
||||
if (disable_quick_entry) {
|
||||
open_stock_entry(item, warehouse, entry_type);
|
||||
@@ -63,11 +63,19 @@ erpnext.stock.ItemDashboard = Class.extend({
|
||||
function open_stock_entry(item, warehouse, entry_type) {
|
||||
frappe.model.with_doctype('Stock Entry', function () {
|
||||
var doc = frappe.model.get_new_doc('Stock Entry');
|
||||
if (entry_type) doc.stock_entry_type = entry_type;
|
||||
if (entry_type) {
|
||||
doc.stock_entry_type = entry_type;
|
||||
}
|
||||
|
||||
var row = frappe.model.add_child(doc, 'items');
|
||||
row.item_code = item;
|
||||
row.s_warehouse = warehouse;
|
||||
|
||||
if (entry_type === "Material Transfer") {
|
||||
row.s_warehouse = warehouse;
|
||||
}
|
||||
else {
|
||||
row.t_warehouse = warehouse;
|
||||
}
|
||||
|
||||
frappe.set_route('Form', doc.doctype, doc.name);
|
||||
});
|
||||
|
||||
@@ -150,12 +150,17 @@ def update_qty(bin_name, args):
|
||||
last_sle_qty = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.qty_after_transaction)
|
||||
.where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse")))
|
||||
.where(
|
||||
(sle.item_code == args.get("item_code"))
|
||||
& (sle.warehouse == args.get("warehouse"))
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
|
||||
.orderby(sle.creation, order=Order.desc)
|
||||
.run()
|
||||
)
|
||||
|
||||
actual_qty = 0.0
|
||||
if last_sle_qty:
|
||||
actual_qty = last_sle_qty[0][0]
|
||||
|
||||
|
||||
@@ -490,6 +490,46 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertEqual(gle_warehouse_amount, 1400)
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
|
||||
bundle_item.append(
|
||||
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||
)
|
||||
bundle_item.save(ignore_permissions=True)
|
||||
|
||||
make_item("_Packed Item New 1", {"is_stock_item": 1})
|
||||
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
|
||||
|
||||
si = create_delivery_note(
|
||||
item_code="_Test Product Bundle Item New",
|
||||
update_stock=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
transaction_date=add_days(nowdate(), -1),
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
|
||||
|
||||
bin_details = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
|
||||
["actual_qty", "projected_qty", "ordered_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
si.transaction_date = nowdate()
|
||||
si.save()
|
||||
|
||||
packed_item = si.packed_items[0]
|
||||
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
|
||||
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
|
||||
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
|
||||
|
||||
def test_return_for_serialized_items(self):
|
||||
se = make_serialized_item()
|
||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||
@@ -650,6 +690,11 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
update_delivery_note_status(dn.name, "Closed")
|
||||
self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed")
|
||||
|
||||
# Check cancelling closed delivery note
|
||||
dn.load_from_db()
|
||||
dn.cancel()
|
||||
self.assertEqual(dn.status, "Cancelled")
|
||||
|
||||
def test_dn_billing_status_case1(self):
|
||||
# SO -> DN -> SI
|
||||
so = make_sales_order()
|
||||
|
||||
@@ -849,6 +849,12 @@ function open_form(frm, doctype, child_doctype, parentfield) {
|
||||
new_child_doc.uom = frm.doc.stock_uom;
|
||||
new_child_doc.description = frm.doc.description;
|
||||
|
||||
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
|
||||
frappe.run_serially([
|
||||
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),
|
||||
() => {
|
||||
frappe.flags.ignore_company_party_validation = true;
|
||||
frappe.model.trigger("item_code", frm.doc.name, new_child_doc);
|
||||
}
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
@@ -955,7 +955,8 @@
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-28 04:52:10.272256",
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-09-13 04:08:17.431731",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -104,7 +104,6 @@ class TestItem(FrappeTestCase):
|
||||
"conversion_factor": 1.0,
|
||||
"reserved_qty": 1,
|
||||
"actual_qty": 5,
|
||||
"ordered_qty": 10,
|
||||
"projected_qty": 14,
|
||||
}
|
||||
|
||||
|
||||
@@ -74,11 +74,10 @@ class ItemAttribute(Document):
|
||||
def validate_duplication(self):
|
||||
values, abbrs = [], []
|
||||
for d in self.item_attribute_values:
|
||||
d.abbr = d.abbr.upper()
|
||||
if d.attribute_value in values:
|
||||
frappe.throw(_("{0} must appear only once").format(d.attribute_value))
|
||||
if d.attribute_value.lower() in map(str.lower, values):
|
||||
frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title()))
|
||||
values.append(d.attribute_value)
|
||||
|
||||
if d.abbr in abbrs:
|
||||
frappe.throw(_("{0} must appear only once").format(d.abbr))
|
||||
if d.abbr.lower() in map(str.lower, abbrs):
|
||||
frappe.throw(_("Abbreviation: {0} must appear only once").format(d.abbr.title()))
|
||||
abbrs.append(d.abbr)
|
||||
|
||||
@@ -366,10 +366,11 @@ frappe.ui.form.on('Material Request', {
|
||||
|
||||
frappe.ui.form.on("Material Request Item", {
|
||||
qty: function (frm, doctype, name) {
|
||||
var d = locals[doctype][name];
|
||||
if (flt(d.qty) < flt(d.min_order_qty)) {
|
||||
const item = locals[doctype][name];
|
||||
if (flt(item.qty) < flt(item.min_order_qty)) {
|
||||
frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum Order Qty"));
|
||||
}
|
||||
frm.events.get_item_data(frm, item, false);
|
||||
},
|
||||
|
||||
from_warehouse: function(frm, doctype, name) {
|
||||
|
||||
@@ -83,8 +83,8 @@ def reset_packing_list(doc):
|
||||
# 1. items were deleted
|
||||
# 2. if bundle item replaced by another item (same no. of items but different items)
|
||||
# we maintain list to track recurring item rows as well
|
||||
items_before_save = [item.item_code for item in doc_before_save.get("items")]
|
||||
items_after_save = [item.item_code for item in doc.get("items")]
|
||||
items_before_save = [(item.name, item.item_code) for item in doc_before_save.get("items")]
|
||||
items_after_save = [(item.name, item.item_code) for item in doc.get("items")]
|
||||
reset_table = items_before_save != items_after_save
|
||||
else:
|
||||
# reset: if via Update Items OR
|
||||
|
||||
@@ -198,7 +198,8 @@ class PickList(Document):
|
||||
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
|
||||
|
||||
def before_print(self, settings=None):
|
||||
self.group_similar_items()
|
||||
if self.group_same_items:
|
||||
self.group_similar_items()
|
||||
|
||||
def group_similar_items(self):
|
||||
group_item_qty = defaultdict(float)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "pick_list",
|
||||
"internal_links": {
|
||||
"Sales Order": ["locations", "sales_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"items": ["Stock Entry", "Delivery Note"]},
|
||||
{"items": ["Stock Entry", "Sales Order", "Delivery Note"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -432,10 +432,10 @@ class TestPickList(FrappeTestCase):
|
||||
pl.before_print()
|
||||
self.assertEqual(len(pl.locations), 4)
|
||||
|
||||
# grouping should halve the number of items
|
||||
# grouping should not happen if group_same_items is False
|
||||
pl = frappe.get_doc(
|
||||
doctype="Pick List",
|
||||
group_same_items=True,
|
||||
group_same_items=False,
|
||||
locations=[
|
||||
_dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
|
||||
_dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
|
||||
@@ -444,6 +444,11 @@ class TestPickList(FrappeTestCase):
|
||||
],
|
||||
)
|
||||
pl.before_print()
|
||||
self.assertEqual(len(pl.locations), 4)
|
||||
|
||||
# grouping should halve the number of items
|
||||
pl.group_same_items = True
|
||||
pl.before_print()
|
||||
self.assertEqual(len(pl.locations), 2)
|
||||
|
||||
expected_items = [
|
||||
|
||||
@@ -221,7 +221,7 @@ class QualityInspection(Document):
|
||||
def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
from_doctype = cstr(filters.get("doctype"))
|
||||
from_doctype = cstr(filters.get("from"))
|
||||
if not from_doctype or not frappe.db.exists("DocType", from_doctype):
|
||||
return []
|
||||
|
||||
|
||||
@@ -2440,7 +2440,7 @@ def get_uom_details(item_code, uom, qty):
|
||||
|
||||
if not conversion_factor:
|
||||
frappe.msgprint(
|
||||
_("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code)
|
||||
_("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code)
|
||||
)
|
||||
ret = {"uom": ""}
|
||||
else:
|
||||
|
||||
@@ -1571,6 +1571,48 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
self.assertRaises(BatchExpiredError, se.save)
|
||||
|
||||
def test_negative_stock_reco(self):
|
||||
from erpnext.controllers.stock_controller import BatchExpiredError
|
||||
from erpnext.stock.doctype.batch.test_batch import make_new_batch
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0)
|
||||
|
||||
item_code = "Test Negative Item - 001"
|
||||
item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="00:00:00",
|
||||
purpose="Material Receipt",
|
||||
qty=10,
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
posting_date=today(),
|
||||
posting_time="00:00:00",
|
||||
purpose="Material Receipt",
|
||||
qty=8,
|
||||
from_warehouse="_Test Warehouse - _TC",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
sr_doc = create_stock_reconciliation(
|
||||
purpose="Stock Reconciliation",
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="00:00:00",
|
||||
item_code=item_code,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
valuation_rate=10,
|
||||
qty=7,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, sr_doc.submit)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -226,8 +226,10 @@ def validate_item_details(args, item):
|
||||
|
||||
validate_end_of_life(item.name, item.end_of_life, item.disabled)
|
||||
|
||||
if args.transaction_type == "selling" and cint(item.has_variants):
|
||||
throw(_("Item {0} is a template, please select one of its variants").format(item.name))
|
||||
if cint(item.has_variants):
|
||||
msg = f"Item {item.name} is a template, please select one of its variants"
|
||||
|
||||
throw(_(msg), title=_("Template Item Selected"))
|
||||
|
||||
elif args.transaction_type == "buying" and args.doctype != "Material Request":
|
||||
if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != 1:
|
||||
@@ -1161,7 +1163,7 @@ def get_projected_qty(item_code, warehouse):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False):
|
||||
bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0}
|
||||
bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
|
||||
|
||||
if warehouse:
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
@@ -1177,7 +1179,6 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses
|
||||
Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"),
|
||||
Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"),
|
||||
Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"),
|
||||
Coalesce(Sum(bin.ordered_qty), 0).as_("ordered_qty"),
|
||||
)
|
||||
.where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses)))
|
||||
).run(as_dict=True)[0]
|
||||
|
||||
@@ -41,7 +41,7 @@ def get_data(report_filters):
|
||||
key = (d.voucher_type, d.voucher_no)
|
||||
gl_data = voucher_wise_gl_data.get(key) or {}
|
||||
d.account_value = gl_data.get("account_value", 0)
|
||||
d.difference_value = abs(d.stock_value - d.account_value)
|
||||
d.difference_value = d.stock_value - d.account_value
|
||||
if abs(d.difference_value) > 0.1:
|
||||
data.append(d)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def execute(filters=None):
|
||||
continue
|
||||
|
||||
total_stock_value = sum(item_value[(item, item_group)])
|
||||
row = [item, item_group, total_stock_value]
|
||||
row = [item, item_map[item]["item_name"], item_group, total_stock_value]
|
||||
|
||||
fifo_queue = item_ageing[item]["fifo_queue"]
|
||||
average_age = 0.00
|
||||
@@ -89,10 +89,11 @@ def get_columns(filters):
|
||||
"""return columns"""
|
||||
|
||||
columns = [
|
||||
_("Item") + ":Link/Item:180",
|
||||
_("Item Group") + "::100",
|
||||
_("Value") + ":Currency:100",
|
||||
_("Age") + ":Float:60",
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + ":Link/Item:150",
|
||||
_("Item Group") + "::120",
|
||||
_("Value") + ":Currency:120",
|
||||
_("Age") + ":Float:120",
|
||||
]
|
||||
return columns
|
||||
|
||||
@@ -132,7 +133,7 @@ def get_warehouse_list(filters):
|
||||
|
||||
def add_warehouse_column(columns, warehouse_list):
|
||||
if len(warehouse_list) > 1:
|
||||
columns += [_("Total Qty") + ":Int:50"]
|
||||
columns += [_("Total Qty") + ":Int:120"]
|
||||
|
||||
for wh in warehouse_list:
|
||||
columns += [_(wh.name) + ":Int:54"]
|
||||
columns += [_(wh.name) + ":Int:100"]
|
||||
|
||||
@@ -1021,7 +1021,7 @@ class update_entries_after(object):
|
||||
frappe.db.set_value("Bin", bin_name, updated_values)
|
||||
|
||||
|
||||
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
|
||||
def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
|
||||
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
||||
|
||||
args["time_format"] = "%H:%i:%s"
|
||||
@@ -1043,11 +1043,17 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
|
||||
and warehouse = %(warehouse)s
|
||||
and is_cancelled = 0
|
||||
{voucher_condition}
|
||||
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
|
||||
and (
|
||||
posting_date < %(posting_date)s or
|
||||
(
|
||||
posting_date = %(posting_date)s and
|
||||
time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
|
||||
)
|
||||
)
|
||||
order by timestamp(posting_date, posting_time) desc, creation desc
|
||||
limit 1
|
||||
for update""".format(
|
||||
voucher_condition=voucher_condition
|
||||
operator=operator, voucher_condition=voucher_condition
|
||||
),
|
||||
args,
|
||||
as_dict=1,
|
||||
@@ -1144,7 +1150,7 @@ def get_stock_ledger_entries(
|
||||
def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
||||
return frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
|
||||
{"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
|
||||
[
|
||||
"item_code",
|
||||
"warehouse",
|
||||
@@ -1184,20 +1190,6 @@ def get_valuation_rate(
|
||||
(item_code, warehouse, voucher_no, voucher_type),
|
||||
)
|
||||
|
||||
if not last_valuation_rate:
|
||||
# Get valuation rate from last sle for the item against any warehouse
|
||||
last_valuation_rate = frappe.db.sql(
|
||||
"""select valuation_rate
|
||||
from `tabStock Ledger Entry` force index (item_code)
|
||||
where
|
||||
item_code = %s
|
||||
AND valuation_rate > 0
|
||||
AND is_cancelled = 0
|
||||
AND NOT(voucher_no = %s AND voucher_type = %s)
|
||||
order by posting_date desc, posting_time desc, name desc limit 1""",
|
||||
(item_code, voucher_no, voucher_type),
|
||||
)
|
||||
|
||||
if last_valuation_rate:
|
||||
return flt(last_valuation_rate[0][0])
|
||||
|
||||
@@ -1299,7 +1291,7 @@ def get_stock_reco_qty_shift(args):
|
||||
stock_reco_qty_shift = flt(args.actual_qty)
|
||||
else:
|
||||
# reco is being submitted
|
||||
last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
|
||||
last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
|
||||
"qty_after_transaction"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ frappe.ready(async () => {
|
||||
initialise_select_date();
|
||||
})
|
||||
|
||||
window.holiday_list = [];
|
||||
|
||||
async function initialise_select_date() {
|
||||
navigate_to_page(1);
|
||||
@@ -20,7 +19,6 @@ async function get_global_variables() {
|
||||
window.timezones = (await frappe.call({
|
||||
method:'erpnext.www.book_appointment.index.get_timezones'
|
||||
})).message;
|
||||
window.holiday_list = window.appointment_settings.holiday_list;
|
||||
}
|
||||
|
||||
function setup_timezone_selector() {
|
||||
|
||||
@@ -26,8 +26,12 @@ def get_context(context):
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_appointment_settings():
|
||||
settings = frappe.get_doc("Appointment Booking Settings")
|
||||
settings.holiday_list = frappe.get_doc("Holiday List", settings.holiday_list)
|
||||
settings = frappe.get_cached_value(
|
||||
"Appointment Booking Settings",
|
||||
None,
|
||||
["advance_booking_days", "appointment_duration", "success_redirect_url"],
|
||||
as_dict=True,
|
||||
)
|
||||
return settings
|
||||
|
||||
|
||||
@@ -106,7 +110,7 @@ def create_appointment(date, time, tz, contact):
|
||||
appointment.customer_details = contact.get("notes", None)
|
||||
appointment.customer_email = contact.get("email", None)
|
||||
appointment.status = "Open"
|
||||
appointment.insert()
|
||||
appointment.insert(ignore_permissions=True)
|
||||
return appointment
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import frappe
|
||||
from frappe.utils.verified_command import verify_request
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_context(context):
|
||||
if not verify_request():
|
||||
context.success = False
|
||||
|
||||
Reference in New Issue
Block a user