Merge branch 'develop' into alternative-items-quotation
diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py
index 378983e..8334604 100644
--- a/.github/helper/documentation.py
+++ b/.github/helper/documentation.py
@@ -3,52 +3,71 @@
from urllib.parse import urlparse
-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)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 73aae33..d70977c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -32,8 +32,8 @@
- 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$"
diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index 8ae90ce..d537adf 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -56,36 +56,41 @@
accounts = nodes;
}
- const get_balances = frappe.call({
- method: 'erpnext.accounts.utils.get_account_balances',
- args: {
- accounts: accounts,
- company: cur_tree.args.company
- },
- });
+ frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
+ if(value) {
- get_balances.then(r => {
- if (!r.message || r.message.length == 0) return;
+ const get_balances = frappe.call({
+ method: 'erpnext.accounts.utils.get_account_balances',
+ args: {
+ accounts: accounts,
+ company: cur_tree.args.company
+ },
+ });
- for (let account of r.message) {
+ get_balances.then(r => {
+ if (!r.message || r.message.length == 0) return;
- const node = cur_tree.nodes && cur_tree.nodes[account.value];
- if (!node || node.is_root) continue;
+ for (let account of r.message) {
- // show Dr if positive since balance is calculated as debit - credit else show Cr
- const balance = account.balance_in_account_currency || account.balance;
- const dr_or_cr = balance > 0 ? "Dr": "Cr";
- const format = (value, currency) => format_currency(Math.abs(value), currency);
+ const node = cur_tree.nodes && cur_tree.nodes[account.value];
+ if (!node || node.is_root) continue;
- if (account.balance!==undefined) {
- node.parent && node.parent.find('.balance-area').remove();
- $('<span class="balance-area pull-right">'
- + (account.balance_in_account_currency ?
- (format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
- + format(account.balance, account.company_currency)
- + " " + dr_or_cr
- + '</span>').insertBefore(node.$ul);
- }
+ // show Dr if positive since balance is calculated as debit - credit else show Cr
+ const balance = account.balance_in_account_currency || account.balance;
+ const dr_or_cr = balance > 0 ? "Dr": "Cr";
+ const format = (value, currency) => format_currency(Math.abs(value), currency);
+
+ if (account.balance!==undefined) {
+ node.parent && node.parent.find('.balance-area').remove();
+ $('<span class="balance-area pull-right">'
+ + (account.balance_in_account_currency ?
+ (format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ + format(account.balance, account.company_currency)
+ + " " + dr_or_cr
+ + '</span>').insertBefore(node.$ul);
+ }
+ }
+ });
}
});
},
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
index ee501f6..741d428 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
@@ -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"
+ }
+ }
+ }
+ }
}
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 1e2e2ac..3f985b6 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -56,7 +56,9 @@
"acc_frozen_upto",
"column_break_25",
"frozen_accounts_modifier",
- "report_settings_sb"
+ "report_settings_sb",
+ "tab_break_dpet",
+ "show_balance_in_coa"
],
"fields": [
{
@@ -347,6 +349,17 @@
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account "
+ },
+ {
+ "fieldname": "tab_break_dpet",
+ "fieldtype": "Tab Break",
+ "label": "Chart Of Accounts"
+ },
+ {
+ "default": "1",
+ "fieldname": "show_balance_in_coa",
+ "fieldtype": "Check",
+ "label": "Show Balances in Chart Of Accounts"
}
],
"icon": "icon-cog",
@@ -354,7 +367,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-11-27 21:49:52.538655",
+ "modified": "2023-01-02 12:07:42.434214",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
index d25016f..54ffe21 100644
--- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
+++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
@@ -28,9 +28,14 @@
class CostCenterAllocation(Document):
+ def __init__(self, *args, **kwargs):
+ super(CostCenterAllocation, self).__init__(*args, **kwargs)
+ self._skip_from_date_validation = False
+
def validate(self):
self.validate_total_allocation_percentage()
- self.validate_from_date_based_on_existing_gle()
+ if not self._skip_from_date_validation:
+ self.validate_from_date_based_on_existing_gle()
self.validate_backdated_allocation()
self.validate_main_cost_center()
self.validate_child_cost_centers()
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 30a3201..21f27ae 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,7 +8,7 @@
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
- frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry'];
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index ea8b7d8..5b0322a 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -81,6 +81,7 @@
self.check_credit_limit()
self.make_gl_entries()
self.update_advance_paid()
+ self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
@@ -225,6 +226,34 @@
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"
@@ -283,20 +312,48 @@
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 row in asset.get("finance_books"):
- depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
- for s in depr_schedule or []:
- if s.journal_entry == self.name:
- s.db_set("journal_entry", None)
+ if asset.calculate_depreciation:
+ je_found = False
- row.value_after_depreciation += s.depreciation_amount
- row.db_update()
+ for row in asset.get("finance_books"):
+ if je_found:
+ break
- asset.set_status()
+ depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
+
+ for s in depr_schedule or []:
+ if s.journal_entry == self.name:
+ s.db_set("journal_entry", None)
+
+ row.value_after_depreciation += s.depreciation_amount
+ row.db_update()
+
+ asset.set_status()
+
+ je_found = True
+ 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 (
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 12c0b7a..154fdc0 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -69,6 +69,10 @@
def get_jv_entries(self):
condition = self.get_conditions()
+
+ if self.get("cost_center"):
+ condition += f" and t2.cost_center = '{self.cost_center}' "
+
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 2ba90b4..00e3934 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -747,6 +747,73 @@
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
+ def test_cost_center_filter_on_vouchers(self):
+ """
+ Test Cost Center filter is applied on Invoices, Payment Entries and Journals
+ """
+ transaction_date = nowdate()
+ rate = 100
+
+ # 'Main - PR' Cost Center
+ si1 = self.create_sales_invoice(
+ qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
+ )
+ si1.cost_center = self.main_cc.name
+ si1.submit()
+
+ pe1 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
+ pe1.cost_center = self.main_cc.name
+ pe1 = pe1.save().submit()
+
+ je1 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
+ je1.accounts[0].cost_center = self.main_cc.name
+ je1.accounts[1].cost_center = self.main_cc.name
+ je1.accounts[1].party_type = "Customer"
+ je1.accounts[1].party = self.customer
+ je1 = je1.save().submit()
+
+ # 'Sub - PR' Cost Center
+ si2 = self.create_sales_invoice(
+ qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
+ )
+ si2.cost_center = self.sub_cc.name
+ si2.submit()
+
+ pe2 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
+ pe2.cost_center = self.sub_cc.name
+ pe2 = pe2.save().submit()
+
+ je2 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
+ je2.accounts[0].cost_center = self.sub_cc.name
+ je2.accounts[1].cost_center = self.sub_cc.name
+ je2.accounts[1].party_type = "Customer"
+ je2.accounts[1].party = self.customer
+ je2 = je2.save().submit()
+
+ pr = self.create_payment_reconciliation()
+ pr.cost_center = self.main_cc.name
+
+ pr.get_unreconciled_entries()
+
+ # check PR tool output
+ self.assertEqual(len(pr.get("invoices")), 1)
+ self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si1.name)
+ self.assertEqual(len(pr.get("payments")), 2)
+ payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
+ self.assertCountEqual(payment_vouchers, [pe1.name, je1.name])
+
+ # Change cost center
+ pr.cost_center = self.sub_cc.name
+
+ pr.get_unreconciled_entries()
+
+ # check PR tool output
+ self.assertEqual(len(pr.get("invoices")), 1)
+ self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si2.name)
+ self.assertEqual(len(pr.get("payments")), 2)
+ payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
+ self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
+
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 4fc12db..fc837c7 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -51,7 +51,7 @@
if existing_payment_request_amount:
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
- if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart":
+ if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 6281400..54caf6f 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -1426,6 +1426,7 @@
},
{
"default": "0",
+ "depends_on": "apply_tds",
"fieldname": "tax_withholding_net_total",
"fieldtype": "Currency",
"hidden": 1,
@@ -1435,12 +1436,13 @@
"read_only": 1
},
{
+ "depends_on": "apply_tds",
"fieldname": "base_tax_withholding_net_total",
"fieldtype": "Currency",
"hidden": 1,
"label": "Base Tax Withholding Net Total",
"no_copy": 1,
- "options": "currency",
+ "options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1554,7 +1556,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2022-12-12 18:37:38.142688",
+ "modified": "2023-01-28 19:18:56.586321",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 4729d9c..2f4e45e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1776,6 +1776,8 @@
"width": "50%"
},
{
+ "fetch_from": "sales_partner.commission_rate",
+ "fetch_if_empty": 1,
"fieldname": "commission_rate",
"fieldtype": "Float",
"hide_days": 1,
@@ -2141,7 +2143,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2022-12-12 18:34:33.409895",
+ "modified": "2023-01-28 19:45:47.538163",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index baeed03..01cfb58 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -211,7 +211,13 @@
else:
party_details.update(get_company_address(company))
- if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]:
+ if doctype and doctype in [
+ "Delivery Note",
+ "Sales Invoice",
+ "Sales Order",
+ "Quotation",
+ "POS Invoice",
+ ]:
if party_details.company_address:
party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address)
@@ -544,7 +550,7 @@
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days))
else:
- due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months))
+ due_date = max(due_date, get_last_day(add_months(due_date, term.credit_months)))
return due_date
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index fc23127..27b84c4 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -526,7 +526,7 @@
"options": "GL Entry",
"hidden": 1,
},
- {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90},
+ {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Account"),
"fieldname": "account",
@@ -538,13 +538,13 @@
"label": _("Debit ({0})").format(currency),
"fieldname": "debit",
"fieldtype": "Float",
- "width": 100,
+ "width": 130,
},
{
"label": _("Credit ({0})").format(currency),
"fieldname": "credit",
"fieldtype": "Float",
- "width": 100,
+ "width": 130,
},
{
"label": _("Balance ({0})").format(currency),
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 615804e..e89d429 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -50,6 +50,20 @@
"fieldtype": "Link",
"options": "Sales Person"
},
+ {
+ "fieldname": "warehouse",
+ "label": __("Warehouse"),
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "get_query": function () {
+ var company = frappe.query_report.get_filter_value('company');
+ return {
+ filters: [
+ ["Warehouse", "company", "=", company]
+ ]
+ };
+ },
+ },
],
"tree": true,
"name_field": "parent",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 130b715..e23265b 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -655,10 +655,35 @@
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
@@ -750,6 +775,13 @@
if self.filters.get("item_code"):
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s"
+ if self.filters.get("warehouse"):
+ warehouse_details = frappe.db.get_value(
+ "Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1
+ )
+ if warehouse_details:
+ conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
+
self.si_list = frappe.db.sql(
"""
select
@@ -760,7 +792,8 @@
`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,
diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py
index fa11a41..21681be 100644
--- a/erpnext/accounts/report/gross_profit/test_gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py
@@ -302,3 +302,82 @@
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])
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 8f5b85d..4ed99f7 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -221,34 +221,45 @@
asset_values.push(flt(frm.doc.gross_purchase_amount) -
flt(frm.doc.opening_accumulated_depreciation));
}
+ if(frm.doc.calculate_depreciation) {
+ if (frm.doc.finance_books.length == 1) {
+ let depr_schedule = (await frappe.call(
+ "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
+ {
+ asset_name: frm.doc.name,
+ status: frm.doc.docstatus ? "Active" : "Draft",
+ finance_book: frm.doc.finance_books[0].finance_book || null
+ }
+ )).message;
- let depr_schedule = [];
-
- if (frm.doc.finance_books.length == 1) {
- depr_schedule = (await frappe.call(
- "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
- {
- asset_name: frm.doc.name,
- status: frm.doc.docstatus ? "Active" : "Draft",
- finance_book: frm.doc.finance_books[0].finance_book || null
- }
- )).message;
- }
-
- $.each(depr_schedule || [], 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)
- }
+ $.each(depr_schedule || [], 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)
+ }
+ }
+ });
}
- });
+ } else {
+ let depr_entries = (await frappe.call({
+ method: "get_manual_depreciation_entries",
+ doc: frm.doc,
+ })).message;
+
+ $.each(depr_entries || [], function(i, v) {
+ x_intervals.push(v.posting_date);
+ last_depreciation_date = v.posting_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);
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 8a64a95..ea575fd 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -509,9 +509,15 @@
"group": "Depreciation",
"link_doctype": "Asset Depreciation Schedule",
"link_fieldname": "asset"
+ },
+ {
+ "group": "Journal Entry",
+ "link_doctype": "Journal Entry",
+ "link_fieldname": "reference_name",
+ "table_fieldname": "accounts"
}
],
- "modified": "2023-01-17 00:25:30.387242",
+ "modified": "2023-02-02 00:03:11.706427",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index df05d5e..4f1caca 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -36,7 +36,6 @@
get_depr_schedule,
make_draft_asset_depr_schedules,
make_draft_asset_depr_schedules_if_not_present,
- set_draft_asset_depr_schedule_details,
update_draft_asset_depr_schedules,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -240,17 +239,6 @@
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
- def _get_value_after_depreciation(self, finance_book):
- # value_after_depreciation - current Asset value
- if self.docstatus == 1 and finance_book.value_after_depreciation:
- value_after_depreciation = flt(finance_book.value_after_depreciation)
- else:
- value_after_depreciation = flt(self.gross_purchase_amount) - flt(
- self.opening_accumulated_depreciation
- )
-
- return value_after_depreciation
-
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
@@ -392,18 +380,23 @@
movement.cancel()
def delete_depreciation_entries(self):
- for row in self.get("finance_books"):
- depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book)
+ if self.calculate_depreciation:
+ for row in self.get("finance_books"):
+ depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book)
- for d in depr_schedule or []:
- if d.journal_entry:
- frappe.get_doc("Journal Entry", d.journal_entry).cancel()
- d.db_set("journal_entry", None)
+ for d in depr_schedule or []:
+ 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"""
@@ -434,6 +427,28 @@
status = "Cancelled"
return status
+ def get_value_after_depreciation(self, finance_book=None):
+ if not self.calculate_depreciation:
+ return self.value_after_depreciation
+
+ if not finance_book:
+ return self.get("finance_books")[0].value_after_depreciation
+
+ for row in self.get("finance_books"):
+ if finance_book == row.finance_book:
+ return row.value_after_depreciation
+
+ def _get_value_after_depreciation_for_making_schedule(self, fb_row):
+ # value_after_depreciation - current Asset value
+ if self.docstatus == 1 and fb_row.value_after_depreciation:
+ value_after_depreciation = flt(fb_row.value_after_depreciation)
+ else:
+ value_after_depreciation = flt(self.gross_purchase_amount) - flt(
+ self.opening_accumulated_depreciation
+ )
+
+ return value_after_depreciation
+
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)
@@ -443,6 +458,43 @@
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
+
+ @erpnext.allow_regional
+ def get_depreciation_amount(self, depreciable_value, fb_row):
+ if fb_row.depreciation_method in ("Straight Line", "Manual"):
+ # if the Depreciation Schedule is being prepared for the first time
+ if not self.flags.increase_in_asset_life:
+ depreciation_amount = (
+ flt(self.gross_purchase_amount) - flt(fb_row.expected_value_after_useful_life)
+ ) / flt(fb_row.total_number_of_depreciations)
+
+ # if the Depreciation Schedule is being modified after Asset Repair
+ else:
+ depreciation_amount = (
+ flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life)
+ ) / (date_diff(self.to_date, self.available_for_use_date) / 365)
+ else:
+ depreciation_amount = flt(depreciable_value * (flt(fb_row.rate_of_depreciation) / 100))
+
+ return depreciation_amount
+
def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document()
if not purchase_document:
@@ -603,7 +655,6 @@
def make_post_gl_entry():
-
asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"])
for asset_category in asset_categories:
@@ -756,7 +807,7 @@
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
@@ -821,6 +872,13 @@
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)
@@ -886,7 +944,7 @@
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, asset, row)
+ new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, row)
accumulated_depreciation = 0
@@ -938,7 +996,7 @@
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, new_asset, row)
+ new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
accumulated_depreciation = 0
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 5337fd6..e7a2532 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -4,7 +4,17 @@
import frappe
from frappe import _
-from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today
+from frappe.utils import (
+ add_months,
+ cint,
+ flt,
+ get_last_day,
+ get_link_to_form,
+ getdate,
+ is_last_day_of_the_month,
+ nowdate,
+ today,
+)
from frappe.utils.user import get_users_with_role
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -400,6 +410,9 @@
row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation)
)
+ if is_last_day_of_the_month(row.depreciation_start_date):
+ orginal_schedule_date = get_last_day(orginal_schedule_date)
+
if orginal_schedule_date == posting_date_of_disposal:
return True
@@ -520,18 +533,8 @@
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 (
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 51a2b52..9a15263 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -16,6 +16,7 @@
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,
@@ -28,7 +29,6 @@
scrap_asset,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- clear_depr_schedule,
get_asset_depr_schedule_doc,
get_depr_schedule,
)
@@ -924,11 +924,6 @@
def test_get_depreciation_amount(self):
"""Tests if get_depreciation_amount() returns the right value."""
-
- from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- get_depreciation_amount,
- )
-
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31")
asset.calculate_depreciation = 1
@@ -943,7 +938,7 @@
},
)
- depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
+ depreciation_amount = asset.get_depreciation_amount(100000, asset.finance_books[0])
self.assertEqual(depreciation_amount, 30000)
def test_make_depr_schedule(self):
@@ -1259,7 +1254,7 @@
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
- clear_depr_schedule(asset_depr_schedule_doc)
+ asset_depr_schedule_doc.clear_depr_schedule()
self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1)
@@ -1308,19 +1303,19 @@
asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 1"
)
- clear_depr_schedule(asset_depr_schedule_doc_1)
+ asset_depr_schedule_doc_1.clear_depr_schedule()
self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3)
asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 2"
)
- clear_depr_schedule(asset_depr_schedule_doc_2)
+ asset_depr_schedule_doc_2.clear_depr_schedule()
self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3)
asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 3"
)
- clear_depr_schedule(asset_depr_schedule_doc_3)
+ asset_depr_schedule_doc_3.clear_depr_schedule()
self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0)
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
@@ -1503,6 +1498,36 @@
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
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"):
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 821accf..5b910db 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -10,6 +10,7 @@
from frappe.utils import cint, flt, get_link_to_form
import erpnext
+from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_gl_entries_on_asset_disposal,
@@ -21,9 +22,6 @@
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
-from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
- get_current_asset_value,
-)
from erpnext.controllers.stock_controller import StockController
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -261,7 +259,9 @@
for d in self.get("asset_items"):
if d.asset:
finance_book = d.get("finance_book") or self.get("finance_book")
- d.current_asset_value = flt(get_current_asset_value(d.asset, finance_book=finance_book))
+ d.current_asset_value = flt(
+ get_asset_value_after_depreciation(d.asset, finance_book=finance_book)
+ )
d.asset_value = get_value_after_depreciation_on_disposal_date(
d.asset, self.posting_date, finance_book=finance_book
)
@@ -713,7 +713,7 @@
if args.asset:
out.current_asset_value = flt(
- get_current_asset_value(args.asset, finance_book=args.finance_book)
+ get_asset_value_after_depreciation(args.asset, finance_book=args.finance_book)
)
out.asset_value = get_value_after_depreciation_on_disposal_date(
args.asset, args.posting_date, finance_book=args.finance_book
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 1446a6e..7615fbc 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -4,17 +4,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import (
- add_days,
- add_months,
- cint,
- date_diff,
- flt,
- get_last_day,
- is_last_day_of_the_month,
-)
-
-import erpnext
+from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month
class AssetDepreciationSchedule(Document):
@@ -83,7 +73,256 @@
)
asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name)
- prepare_draft_asset_depr_schedule_data(self, asset_doc, asset_finance_book_doc)
+ self.prepare_draft_asset_depr_schedule_data(asset_doc, asset_finance_book_doc)
+
+ def prepare_draft_asset_depr_schedule_data(
+ self,
+ asset_doc,
+ row,
+ date_of_disposal=None,
+ date_of_return=None,
+ update_asset_finance_book_row=True,
+ ):
+ self.set_draft_asset_depr_schedule_details(asset_doc, row)
+ self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
+ self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+
+ def set_draft_asset_depr_schedule_details(self, asset_doc, row):
+ self.asset = asset_doc.name
+ self.finance_book = row.finance_book
+ self.finance_book_id = row.idx
+ self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation
+ self.depreciation_method = row.depreciation_method
+ self.total_number_of_depreciations = row.total_number_of_depreciations
+ self.frequency_of_depreciation = row.frequency_of_depreciation
+ self.rate_of_depreciation = row.rate_of_depreciation
+ self.expected_value_after_useful_life = row.expected_value_after_useful_life
+ self.status = "Draft"
+
+ def make_depr_schedule(
+ self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
+ ):
+ if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"):
+ self.depreciation_schedule = []
+
+ if not asset_doc.available_for_use_date:
+ return
+
+ start = self.clear_depr_schedule()
+
+ self._make_depr_schedule(asset_doc, row, start, date_of_disposal, update_asset_finance_book_row)
+
+ def clear_depr_schedule(self):
+ start = 0
+ num_of_depreciations_completed = 0
+ depr_schedule = []
+
+ for schedule in self.get("depreciation_schedule"):
+ if schedule.journal_entry:
+ num_of_depreciations_completed += 1
+ depr_schedule.append(schedule)
+ else:
+ start = num_of_depreciations_completed
+ break
+
+ self.depreciation_schedule = depr_schedule
+
+ return start
+
+ def _make_depr_schedule(
+ self, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
+ ):
+ asset_doc.validate_asset_finance_books(row)
+
+ value_after_depreciation = asset_doc._get_value_after_depreciation_for_making_schedule(row)
+ row.value_after_depreciation = value_after_depreciation
+
+ if update_asset_finance_book_row:
+ row.db_update()
+
+ number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
+ asset_doc.number_of_depreciations_booked
+ )
+
+ has_pro_rata = asset_doc.check_is_pro_rata(row)
+ if has_pro_rata:
+ number_of_pending_depreciations += 1
+
+ skip_row = False
+ should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date)
+
+ for n in range(start, number_of_pending_depreciations):
+ # If depreciation is already completed (for double declining balance)
+ if skip_row:
+ continue
+
+ depreciation_amount = asset_doc.get_depreciation_amount(value_after_depreciation, row)
+
+ if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
+ schedule_date = add_months(
+ row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
+ )
+
+ if should_get_last_day:
+ schedule_date = get_last_day(schedule_date)
+
+ # schedule date will be a year later from start date
+ # so monthly schedule date is calculated by removing 11 months from it
+ monthly_schedule_date = add_months(schedule_date, -row.frequency_of_depreciation + 1)
+
+ # if asset is being sold or scrapped
+ if date_of_disposal:
+ from_date = asset_doc.available_for_use_date
+ if self.depreciation_schedule:
+ from_date = self.depreciation_schedule[-1].schedule_date
+
+ depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
+ row, depreciation_amount, from_date, date_of_disposal
+ )
+
+ if depreciation_amount > 0:
+ self.add_depr_schedule_row(
+ date_of_disposal,
+ depreciation_amount,
+ row.depreciation_method,
+ )
+
+ break
+
+ # For first row
+ if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0:
+ from_date = add_days(
+ asset_doc.available_for_use_date, -1
+ ) # needed to calc depr amount for available_for_use_date too
+ depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
+ row, depreciation_amount, from_date, row.depreciation_start_date
+ )
+
+ # For first depr schedule date will be the start date
+ # so monthly schedule date is calculated by removing
+ # month difference between use date and start date
+ monthly_schedule_date = add_months(row.depreciation_start_date, -months + 1)
+
+ # For last row
+ elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
+ if not asset_doc.flags.increase_in_asset_life:
+ # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
+ asset_doc.to_date = add_months(
+ asset_doc.available_for_use_date,
+ (n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
+ )
+
+ depreciation_amount_without_pro_rata = depreciation_amount
+
+ depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
+ row, depreciation_amount, schedule_date, asset_doc.to_date
+ )
+
+ depreciation_amount = self.get_adjusted_depreciation_amount(
+ depreciation_amount_without_pro_rata, depreciation_amount
+ )
+
+ monthly_schedule_date = add_months(schedule_date, 1)
+ schedule_date = add_days(schedule_date, days)
+ last_schedule_date = schedule_date
+
+ if not depreciation_amount:
+ continue
+ value_after_depreciation -= flt(
+ depreciation_amount, asset_doc.precision("gross_purchase_amount")
+ )
+
+ # Adjust depreciation amount in the last period based on the expected value after useful life
+ if row.expected_value_after_useful_life and (
+ (
+ n == cint(number_of_pending_depreciations) - 1
+ and value_after_depreciation != row.expected_value_after_useful_life
+ )
+ or value_after_depreciation < row.expected_value_after_useful_life
+ ):
+ depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life
+ skip_row = True
+
+ if depreciation_amount > 0:
+ self.add_depr_schedule_row(
+ schedule_date,
+ depreciation_amount,
+ row.depreciation_method,
+ )
+
+ # to ensure that final accumulated depreciation amount is accurate
+ def get_adjusted_depreciation_amount(
+ self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row
+ ):
+ if not self.opening_accumulated_depreciation:
+ depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row()
+
+ if (
+ depreciation_amount_for_first_row + depreciation_amount_for_last_row
+ != depreciation_amount_without_pro_rata
+ ):
+ depreciation_amount_for_last_row = (
+ depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
+ )
+
+ return depreciation_amount_for_last_row
+
+ def get_depreciation_amount_for_first_row(self):
+ return self.get("depreciation_schedule")[0].depreciation_amount
+
+ def add_depr_schedule_row(
+ self,
+ schedule_date,
+ depreciation_amount,
+ depreciation_method,
+ ):
+ self.append(
+ "depreciation_schedule",
+ {
+ "schedule_date": schedule_date,
+ "depreciation_amount": depreciation_amount,
+ "depreciation_method": depreciation_method,
+ },
+ )
+
+ def set_accumulated_depreciation(
+ self,
+ row,
+ date_of_disposal=None,
+ date_of_return=None,
+ ignore_booked_entry=False,
+ ):
+ straight_line_idx = [
+ d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line"
+ ]
+
+ accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+ value_after_depreciation = flt(row.value_after_depreciation)
+
+ for i, d in enumerate(self.get("depreciation_schedule")):
+ if ignore_booked_entry and d.journal_entry:
+ continue
+
+ depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
+ value_after_depreciation -= flt(depreciation_amount)
+
+ # for the last row, if depreciation method = Straight Line
+ if (
+ straight_line_idx
+ and i == max(straight_line_idx) - 1
+ and not date_of_disposal
+ and not date_of_return
+ ):
+ depreciation_amount += flt(
+ value_after_depreciation - flt(row.expected_value_after_useful_life),
+ d.precision("depreciation_amount"),
+ )
+
+ d.depreciation_amount = depreciation_amount
+ accumulated_depreciation += d.depreciation_amount
+ d.accumulated_depreciation_amount = flt(
+ accumulated_depreciation, d.precision("accumulated_depreciation_amount")
+ )
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
@@ -108,7 +347,7 @@
def make_draft_asset_depr_schedule(asset_doc, row):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
- prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row)
+ asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row)
asset_depr_schedule_doc.insert()
@@ -120,41 +359,11 @@
if not asset_depr_schedule_doc:
continue
- prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row)
+ asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row)
asset_depr_schedule_doc.save()
-def prepare_draft_asset_depr_schedule_data(
- asset_depr_schedule_doc,
- asset_doc,
- row,
- date_of_disposal=None,
- date_of_return=None,
- update_asset_finance_book_row=True,
-):
- set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row)
- make_depr_schedule(
- asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row
- )
- set_accumulated_depreciation(asset_depr_schedule_doc, row, date_of_disposal, date_of_return)
-
-
-def set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row):
- asset_depr_schedule_doc.asset = asset_doc.name
- asset_depr_schedule_doc.finance_book = row.finance_book
- asset_depr_schedule_doc.finance_book_id = row.idx
- asset_depr_schedule_doc.opening_accumulated_depreciation = (
- asset_doc.opening_accumulated_depreciation
- )
- asset_depr_schedule_doc.depreciation_method = row.depreciation_method
- asset_depr_schedule_doc.total_number_of_depreciations = row.total_number_of_depreciations
- asset_depr_schedule_doc.frequency_of_depreciation = row.frequency_of_depreciation
- asset_depr_schedule_doc.rate_of_depreciation = row.rate_of_depreciation
- asset_depr_schedule_doc.expected_value_after_useful_life = row.expected_value_after_useful_life
- asset_depr_schedule_doc.status = "Draft"
-
-
def convert_draft_asset_depr_schedules_into_active(asset_doc):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book)
@@ -192,8 +401,8 @@
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- make_depr_schedule(new_asset_depr_schedule_doc, asset_doc, row, date_of_disposal)
- set_accumulated_depreciation(new_asset_depr_schedule_doc, row, date_of_disposal, date_of_return)
+ new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal)
+ new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
new_asset_depr_schedule_doc.notes = notes
@@ -208,8 +417,7 @@
):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
- prepare_draft_asset_depr_schedule_data(
- asset_depr_schedule_doc,
+ asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
asset_doc,
row,
date_of_disposal,
@@ -220,21 +428,6 @@
return asset_depr_schedule_doc
-def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
- finance_book_filter = ["finance_book", "is", "not set"]
- if finance_book:
- finance_book_filter = ["finance_book", "=", finance_book]
-
- return frappe.db.get_value(
- doctype="Asset Depreciation Schedule",
- filters=[
- ["asset", "=", asset_name],
- finance_book_filter,
- ["status", "=", status],
- ],
- )
-
-
@frappe.whitelist()
def get_depr_schedule(asset_name, status, finance_book=None):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book)
@@ -256,261 +449,16 @@
return asset_depr_schedule_doc
-def make_depr_schedule(
- asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
-):
- if row.depreciation_method != "Manual" and not asset_depr_schedule_doc.get(
- "depreciation_schedule"
- ):
- asset_depr_schedule_doc.depreciation_schedule = []
+def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
+ finance_book_filter = ["finance_book", "is", "not set"]
+ if finance_book:
+ finance_book_filter = ["finance_book", "=", finance_book]
- if not asset_doc.available_for_use_date:
- return
-
- start = clear_depr_schedule(asset_depr_schedule_doc)
-
- _make_depr_schedule(
- asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
+ return frappe.db.get_value(
+ doctype="Asset Depreciation Schedule",
+ filters=[
+ ["asset", "=", asset_name],
+ finance_book_filter,
+ ["status", "=", status],
+ ],
)
-
-
-def clear_depr_schedule(asset_depr_schedule_doc):
- start = 0
- num_of_depreciations_completed = 0
- depr_schedule = []
-
- for schedule in asset_depr_schedule_doc.get("depreciation_schedule"):
- if schedule.journal_entry:
- num_of_depreciations_completed += 1
- depr_schedule.append(schedule)
- else:
- start = num_of_depreciations_completed
- break
-
- asset_depr_schedule_doc.depreciation_schedule = depr_schedule
-
- return start
-
-
-def _make_depr_schedule(
- asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
-):
- asset_doc.validate_asset_finance_books(row)
-
- value_after_depreciation = asset_doc._get_value_after_depreciation(row)
- row.value_after_depreciation = value_after_depreciation
-
- if update_asset_finance_book_row:
- row.db_update()
-
- number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
- asset_doc.number_of_depreciations_booked
- )
-
- has_pro_rata = asset_doc.check_is_pro_rata(row)
- if has_pro_rata:
- number_of_pending_depreciations += 1
-
- skip_row = False
- should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date)
-
- for n in range(start, number_of_pending_depreciations):
- # If depreciation is already completed (for double declining balance)
- if skip_row:
- continue
-
- depreciation_amount = get_depreciation_amount(asset_doc, value_after_depreciation, row)
-
- if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
- schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
-
- if should_get_last_day:
- schedule_date = get_last_day(schedule_date)
-
- # schedule date will be a year later from start date
- # so monthly schedule date is calculated by removing 11 months from it
- monthly_schedule_date = add_months(schedule_date, -row.frequency_of_depreciation + 1)
-
- # if asset is being sold or scrapped
- if date_of_disposal:
- from_date = asset_doc.available_for_use_date
- if asset_depr_schedule_doc.depreciation_schedule:
- from_date = asset_depr_schedule_doc.depreciation_schedule[-1].schedule_date
-
- depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
- row, depreciation_amount, from_date, date_of_disposal
- )
-
- if depreciation_amount > 0:
- add_depr_schedule_row(
- asset_depr_schedule_doc,
- date_of_disposal,
- depreciation_amount,
- row.depreciation_method,
- )
-
- break
-
- # For first row
- if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0:
- from_date = add_days(
- asset_doc.available_for_use_date, -1
- ) # needed to calc depr amount for available_for_use_date too
- depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
- row, depreciation_amount, from_date, row.depreciation_start_date
- )
-
- # For first depr schedule date will be the start date
- # so monthly schedule date is calculated by removing
- # month difference between use date and start date
- monthly_schedule_date = add_months(row.depreciation_start_date, -months + 1)
-
- # For last row
- elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
- if not asset_doc.flags.increase_in_asset_life:
- # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
- asset_doc.to_date = add_months(
- asset_doc.available_for_use_date,
- (n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
- )
-
- depreciation_amount_without_pro_rata = depreciation_amount
-
- depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
- row, depreciation_amount, schedule_date, asset_doc.to_date
- )
-
- depreciation_amount = get_adjusted_depreciation_amount(
- asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount
- )
-
- monthly_schedule_date = add_months(schedule_date, 1)
- schedule_date = add_days(schedule_date, days)
- last_schedule_date = schedule_date
-
- if not depreciation_amount:
- continue
- value_after_depreciation -= flt(
- depreciation_amount, asset_doc.precision("gross_purchase_amount")
- )
-
- # Adjust depreciation amount in the last period based on the expected value after useful life
- if row.expected_value_after_useful_life and (
- (
- n == cint(number_of_pending_depreciations) - 1
- and value_after_depreciation != row.expected_value_after_useful_life
- )
- or value_after_depreciation < row.expected_value_after_useful_life
- ):
- depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life
- skip_row = True
-
- if depreciation_amount > 0:
- add_depr_schedule_row(
- asset_depr_schedule_doc,
- schedule_date,
- depreciation_amount,
- row.depreciation_method,
- )
-
-
-# to ensure that final accumulated depreciation amount is accurate
-def get_adjusted_depreciation_amount(
- asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row
-):
- if not asset_depr_schedule_doc.opening_accumulated_depreciation:
- depreciation_amount_for_first_row = get_depreciation_amount_for_first_row(
- asset_depr_schedule_doc
- )
-
- if (
- depreciation_amount_for_first_row + depreciation_amount_for_last_row
- != depreciation_amount_without_pro_rata
- ):
- depreciation_amount_for_last_row = (
- depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
- )
-
- return depreciation_amount_for_last_row
-
-
-def get_depreciation_amount_for_first_row(asset_depr_schedule_doc):
- return asset_depr_schedule_doc.get("depreciation_schedule")[0].depreciation_amount
-
-
-@erpnext.allow_regional
-def get_depreciation_amount(asset_doc, depreciable_value, row):
- if row.depreciation_method in ("Straight Line", "Manual"):
- # if the Depreciation Schedule is being prepared for the first time
- if not asset_doc.flags.increase_in_asset_life:
- depreciation_amount = (
- flt(asset_doc.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations)
-
- # if the Depreciation Schedule is being modified after Asset Repair
- else:
- depreciation_amount = (
- flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- ) / (date_diff(asset_doc.to_date, asset_doc.available_for_use_date) / 365)
- else:
- depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
-
- return depreciation_amount
-
-
-def add_depr_schedule_row(
- asset_depr_schedule_doc,
- schedule_date,
- depreciation_amount,
- depreciation_method,
-):
- asset_depr_schedule_doc.append(
- "depreciation_schedule",
- {
- "schedule_date": schedule_date,
- "depreciation_amount": depreciation_amount,
- "depreciation_method": depreciation_method,
- },
- )
-
-
-def set_accumulated_depreciation(
- asset_depr_schedule_doc,
- row,
- date_of_disposal=None,
- date_of_return=None,
- ignore_booked_entry=False,
-):
- straight_line_idx = [
- d.idx
- for d in asset_depr_schedule_doc.get("depreciation_schedule")
- if d.depreciation_method == "Straight Line"
- ]
-
- accumulated_depreciation = flt(asset_depr_schedule_doc.opening_accumulated_depreciation)
- value_after_depreciation = flt(row.value_after_depreciation)
-
- for i, d in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
- if ignore_booked_entry and d.journal_entry:
- continue
-
- depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
- value_after_depreciation -= flt(depreciation_amount)
-
- # for the last row, if depreciation method = Straight Line
- if (
- straight_line_idx
- and i == max(straight_line_idx) - 1
- and not date_of_disposal
- and not date_of_return
- ):
- depreciation_amount += flt(
- value_after_depreciation - flt(row.expected_value_after_useful_life),
- d.precision("depreciation_amount"),
- )
-
- d.depreciation_amount = depreciation_amount
- accumulated_depreciation += d.depreciation_amount
- d.accumulated_depreciation_amount = flt(
- accumulated_depreciation, d.precision("accumulated_depreciation_amount")
- )
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index ff72aa9..a9d0b25 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -6,7 +6,10 @@
import frappe
from frappe.utils import flt, nowdate
-from erpnext.assets.doctype.asset.asset import get_asset_account
+from erpnext.assets.doctype.asset.asset import (
+ get_asset_account,
+ get_asset_value_after_depreciation,
+)
from erpnext.assets.doctype.asset.test_asset import (
create_asset,
create_asset_data,
@@ -109,20 +112,20 @@
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):
@@ -256,10 +259,6 @@
)
-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
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
index 36f510b..ae0e1bd 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
@@ -47,7 +47,7 @@
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
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 6cfbe53..31d6ffa 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -10,11 +10,10 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
+from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
- get_depreciation_amount,
- set_accumulated_depreciation,
)
@@ -46,7 +45,7 @@
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)
@@ -163,7 +162,7 @@
depreciation_amount = days * rate_per_day
from_date = data.schedule_date
else:
- depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
+ depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d)
if depreciation_amount:
value_after_depreciation -= flt(depreciation_amount)
@@ -171,18 +170,9 @@
d.db_update()
- set_accumulated_depreciation(new_asset_depr_schedule_doc, d, ignore_booked_entry=True)
+ new_asset_depr_schedule_doc.set_accumulated_depreciation(d, ignore_booked_entry=True)
for asset_data in depr_schedule:
if not asset_data.journal_entry:
asset_data.db_update()
new_asset_depr_schedule_doc.submit()
-
-
-@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")
diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
index 03dcea9..0b3dcba 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
@@ -6,13 +6,11 @@
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_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
-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
@@ -46,7 +44,7 @@
)
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):
@@ -79,7 +77,7 @@
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
- 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
)
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index faffd11..cead72e 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -4,13 +4,16 @@
import frappe
from frappe import _
-from frappe.utils import cstr, flt, formatdate, getdate
+from frappe.query_builder.functions import Sum
+from frappe.utils import cstr, formatdate, getdate
from erpnext.accounts.report.financial_statements import (
get_fiscal_year_data,
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,6 +88,7 @@
"asset_name",
"status",
"department",
+ "company",
"cost_center",
"calculate_depreciation",
"purchase_receipt",
@@ -98,8 +102,25 @@
]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
+ finance_book_filter = ("is", "not set")
+ if filters.finance_book:
+ finance_book_filter = ("=", filters.finance_book)
+
+ assets_linked_to_fb = frappe.db.get_all(
+ doctype="Asset Finance Book",
+ filters={"finance_book": finance_book_filter},
+ pluck="parent",
+ )
+
for asset in assets_record:
- asset_value = get_asset_value(asset, filters.finance_book)
+ 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,
@@ -110,7 +131,7 @@
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,
@@ -122,21 +143,6 @@
return data
-def get_asset_value(asset, finance_book=None):
- if not asset.calculate_depreciation:
- return flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation)
-
- finance_book_filter = ["finance_book", "is", "not set"]
- if finance_book:
- finance_book_filter = ["finance_book", "=", finance_book]
-
- return frappe.db.get_value(
- doctype="Asset Finance Book",
- filters=[["parent", "=", asset.asset_id], finance_book_filter],
- fieldname="value_after_depreciation",
- )
-
-
def prepare_chart_data(data, filters):
labels_values_map = {}
date_field = frappe.scrub(filters.date_based_on)
@@ -182,6 +188,15 @@
}
+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 depr_amount
+
+
def get_finance_book_value_map(filters):
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
@@ -203,6 +218,31 @@
)
+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(
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py
index 646dba5..c673be8 100644
--- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py
@@ -15,17 +15,6 @@
create_customer()
create_item()
- def test_for_single_record(self):
- so_name = create_so()
- transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
- data = frappe.db.get_list(
- "Sales Invoice",
- filters={"posting_date": date.today(), "customer": "Bulk Customer"},
- fields=["*"],
- )
- if not data:
- self.fail("No Sales Invoice Created !")
-
def test_entry_in_log(self):
so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index e1dd679..29afc84 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -1221,6 +1221,7 @@
},
{
"default": "0",
+ "depends_on": "apply_tds",
"fieldname": "tax_withholding_net_total",
"fieldtype": "Currency",
"hidden": 1,
@@ -1230,12 +1231,13 @@
"read_only": 1
},
{
+ "depends_on": "apply_tds",
"fieldname": "base_tax_withholding_net_total",
"fieldtype": "Currency",
"hidden": 1,
"label": "Base Tax Withholding Net Total",
"no_copy": 1,
- "options": "currency",
+ "options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1269,7 +1271,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2022-12-25 18:08:59.074182",
+ "modified": "2023-01-28 18:59:16.322824",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index f0360b2..920486a 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -10,6 +10,7 @@
from frappe.utils.data import today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.party import get_due_date_from_template
from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as make_pi_from_po,
@@ -685,6 +686,12 @@
else:
raise Exception
+ def test_default_payment_terms(self):
+ due_date = get_due_date_from_template(
+ "_Test Payment Term Template 1", "2023-02-03", None
+ ).strftime("%Y-%m-%d")
+ self.assertEqual(due_date, "2023-03-31")
+
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
po = create_purchase_order(do_not_save=1)
po.payment_terms_template = "_Test Payment Term Template"
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index 019d45b..bd65b0c 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -29,6 +29,7 @@
"message_for_supplier",
"terms_section_break",
"incoterm",
+ "named_place",
"tc_name",
"terms",
"printing_settings",
@@ -278,13 +279,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-17 17:26:33.770993",
+ "modified": "2023-01-31 23:22:06.684694",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
index 47a66ad..9b53421 100644
--- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
@@ -15,60 +15,4 @@
class TestProcurementTracker(FrappeTestCase):
- def test_result_for_procurement_tracker(self):
- filters = {"company": "_Test Procurement Company", "cost_center": "Main - _TPC"}
- expected_data = self.generate_expected_data()
- report = execute(filters)
-
- length = len(report[1])
- self.assertEqual(expected_data, report[1][length - 1])
-
- def generate_expected_data(self):
- if not frappe.db.exists("Company", "_Test Procurement Company"):
- frappe.get_doc(
- dict(
- doctype="Company",
- company_name="_Test Procurement Company",
- abbr="_TPC",
- default_currency="INR",
- country="Pakistan",
- )
- ).insert()
- warehouse = create_warehouse("_Test Procurement Warehouse", company="_Test Procurement Company")
- mr = make_material_request(
- company="_Test Procurement Company", warehouse=warehouse, cost_center="Main - _TPC"
- )
- po = make_purchase_order(mr.name)
- po.supplier = "_Test Supplier"
- po.get("items")[0].cost_center = "Main - _TPC"
- po.submit()
- pr = make_purchase_receipt(po.name)
- pr.get("items")[0].cost_center = "Main - _TPC"
- pr.submit()
- date_obj = datetime.date(datetime.now())
-
- po.load_from_db()
-
- expected_data = {
- "material_request_date": date_obj,
- "cost_center": "Main - _TPC",
- "project": None,
- "requesting_site": "_Test Procurement Warehouse - _TPC",
- "requestor": "Administrator",
- "material_request_no": mr.name,
- "item_code": "_Test Item",
- "quantity": 10.0,
- "unit_of_measurement": "_Test UOM",
- "status": "To Bill",
- "purchase_order_date": date_obj,
- "purchase_order": po.name,
- "supplier": "_Test Supplier",
- "estimated_cost": 0.0,
- "actual_cost": 0.0,
- "purchase_order_amt": po.net_total,
- "purchase_order_amt_in_company_currency": po.base_net_total,
- "expected_delivery_date": date_obj,
- "actual_delivery_date": date_obj,
- }
-
- return expected_data
+ pass
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 54f0d94..4f7d9ad 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -712,6 +712,8 @@
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
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 8bd0998..9fcb769 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -305,7 +305,7 @@
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
# Used retrun against and supplier and is_retrun because there is an index added for it
- data = frappe.db.get_list(
+ data = frappe.get_all(
doctype,
fields=fields,
filters=[
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 8f8a086..077e7fa 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -312,7 +312,8 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"fieldname": "language",
@@ -514,11 +515,10 @@
"idx": 5,
"image_field": "image",
"links": [],
- "modified": "2022-10-13 12:42:04.277879",
+ "modified": "2023-01-24 18:20:05.044791",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
- "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index b0ff5d4..2a588d8 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -282,6 +282,7 @@
"contact_no": "phone_1",
"fax": "fax_1",
},
+ "field_no_map": ["disabled"],
}
},
target_doc,
@@ -390,7 +391,7 @@
{
"territory": lead.territory,
"customer_name": lead.company_name or lead.lead_name,
- "contact_display": " ".join(filter(None, [lead.salutation, lead.lead_name])),
+ "contact_display": " ".join(filter(None, [lead.lead_name])),
"contact_email": lead.email_id,
"contact_mobile": lead.mobile_no,
"contact_phone": lead.phone,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 6744d16..211f074 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -325,3 +325,5 @@
erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
erpnext.patches.v14_0.update_entry_type_for_journal_entry
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
+erpnext.patches.v14_0.set_pick_list_status
+erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
diff --git a/erpnext/patches/v14_0/migrate_cost_center_allocations.py b/erpnext/patches/v14_0/migrate_cost_center_allocations.py
index 3bd2693..48f4e6d 100644
--- a/erpnext/patches/v14_0/migrate_cost_center_allocations.py
+++ b/erpnext/patches/v14_0/migrate_cost_center_allocations.py
@@ -18,9 +18,11 @@
cca = frappe.new_doc("Cost Center Allocation")
cca.main_cost_center = main_cc
cca.valid_from = today()
+ cca._skip_from_date_validation = True
for child_cc, percentage in allocations.items():
cca.append("allocation_percentages", ({"cost_center": child_cc, "percentage": percentage}))
+
cca.save()
cca.submit()
diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
index e15aa4a..853a99a 100644
--- a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
+++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
@@ -2,7 +2,8 @@
from frappe import qb
from frappe.query_builder import Case, CustomFunction
from frappe.query_builder.custom import ConstantColumn
-from frappe.query_builder.functions import IfNull
+from frappe.query_builder.functions import Count, IfNull
+from frappe.utils import flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_dimensions,
@@ -17,9 +18,9 @@
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
-def generate_name_for_payment_ledger_entries(gl_entries):
- for index, entry in enumerate(gl_entries, 1):
- entry.name = index
+def generate_name_for_payment_ledger_entries(gl_entries, start):
+ for index, entry in enumerate(gl_entries, 0):
+ entry.name = start + index
def get_columns():
@@ -81,6 +82,14 @@
def execute():
+ """
+ Description:
+ Migrate records from `tabGL Entry` to `tabPayment Ledger Entry`.
+ Patch is non-resumable. if patch failed or is terminatted abnormally, clear 'tabPayment Ledger Entry' table manually before re-running. Re-running is safe only during V13->V14 update.
+
+ Note: Post successful migration to V14, re-running is NOT-SAFE and SHOULD NOT be attempted.
+ """
+
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
# create accounting dimension fields in Payment Ledger
create_accounting_dimension_fields()
@@ -89,52 +98,90 @@
account = qb.DocType("Account")
ifelse = CustomFunction("IF", ["condition", "then", "else"])
- gl_entries = (
- qb.from_(gl)
- .inner_join(account)
- .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
- .select(
- gl.star,
- ConstantColumn(1).as_("docstatus"),
- account.account_type.as_("account_type"),
- IfNull(
- ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
- ).as_("against_voucher_type"),
- IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
- "against_voucher_no"
- ),
- # convert debit/credit to amount
- Case()
- .when(account.account_type == "Receivable", gl.debit - gl.credit)
- .else_(gl.credit - gl.debit)
- .as_("amount"),
- # convert debit/credit in account currency to amount in account currency
- Case()
- .when(
- account.account_type == "Receivable",
- gl.debit_in_account_currency - gl.credit_in_account_currency,
- )
- .else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
- .as_("amount_in_account_currency"),
- )
- .where(gl.is_cancelled == 0)
- .orderby(gl.creation)
- .run(as_dict=True)
+ # Get Records Count
+ accounts = (
+ qb.from_(account)
+ .select(account.name)
+ .where((account.account_type == "Receivable") | (account.account_type == "Payable"))
+ .orderby(account.name)
)
+ un_processed = (
+ qb.from_(gl)
+ .select(Count(gl.name))
+ .where((gl.is_cancelled == 0) & (gl.account.isin(accounts)))
+ .run()
+ )[0][0]
- # primary key(name) for payment ledger records
- generate_name_for_payment_ledger_entries(gl_entries)
+ if un_processed:
+ print(f"Migrating {un_processed} GL Entries to Payment Ledger")
- # split data into chunks
- chunk_size = 1000
- try:
- for i in range(0, len(gl_entries), chunk_size):
- insert_query = build_insert_query()
- insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size])
- frappe.db.commit()
- except Exception as err:
- frappe.db.rollback()
- ple = qb.DocType("Payment Ledger Entry")
- qb.from_(ple).delete().where(ple.docstatus >= 0).run()
- frappe.db.commit()
- raise err
+ processed = 0
+ last_update_percent = 0
+ batch_size = 5000
+ last_name = None
+
+ while True:
+ if last_name:
+ where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0)
+ else:
+ where_clause = gl.is_cancelled == 0
+
+ gl_entries = (
+ qb.from_(gl)
+ .inner_join(account)
+ .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
+ .select(
+ gl.star,
+ ConstantColumn(1).as_("docstatus"),
+ account.account_type.as_("account_type"),
+ IfNull(
+ ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
+ ).as_("against_voucher_type"),
+ IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
+ "against_voucher_no"
+ ),
+ # convert debit/credit to amount
+ Case()
+ .when(account.account_type == "Receivable", gl.debit - gl.credit)
+ .else_(gl.credit - gl.debit)
+ .as_("amount"),
+ # convert debit/credit in account currency to amount in account currency
+ Case()
+ .when(
+ account.account_type == "Receivable",
+ gl.debit_in_account_currency - gl.credit_in_account_currency,
+ )
+ .else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
+ .as_("amount_in_account_currency"),
+ )
+ .where(where_clause)
+ .orderby(gl.name)
+ .limit(batch_size)
+ .run(as_dict=True)
+ )
+
+ if gl_entries:
+ last_name = gl_entries[-1].name
+
+ # primary key(name) for payment ledger records
+ generate_name_for_payment_ledger_entries(gl_entries, processed)
+
+ try:
+ insert_query = build_insert_query()
+ insert_chunk_into_payment_ledger(insert_query, gl_entries)
+ frappe.db.commit()
+
+ processed += len(gl_entries)
+
+ # Progress message
+ percent = flt((processed / un_processed) * 100, 2)
+ if percent - last_update_percent > 1:
+ print(f"{percent}% ({processed}) records processed")
+ last_update_percent = percent
+
+ except Exception as err:
+ print("Migration Failed. Clear `tabPayment Ledger Entry` table before re-running")
+ raise err
+ else:
+ break
+ print(f"{processed} records have been sucessfully migrated")
diff --git a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py
index fd2a2a3..9d216c4 100644
--- a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py
+++ b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py
@@ -1,81 +1,98 @@
import frappe
from frappe import qb
-from frappe.utils import create_batch
-
-
-def remove_duplicate_entries(pl_entries):
- unique_vouchers = set()
- for x in pl_entries:
- unique_vouchers.add(
- (x.company, x.account, x.party_type, x.party, x.voucher_type, x.voucher_no, x.gle_remarks)
- )
-
- entries = []
- for x in unique_vouchers:
- entries.append(
- frappe._dict(
- company=x[0],
- account=x[1],
- party_type=x[2],
- party=x[3],
- voucher_type=x[4],
- voucher_no=x[5],
- gle_remarks=x[6],
- )
- )
- return entries
+from frappe.query_builder import CustomFunction
+from frappe.query_builder.functions import Count, IfNull
+from frappe.utils import flt
def execute():
+ """
+ Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry'
+ """
+
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry")
- # get ple and their remarks from GL Entry
- pl_entries = (
- qb.from_(ple)
- .left_join(gle)
- .on(
- (ple.account == gle.account)
- & (ple.party_type == gle.party_type)
- & (ple.party == gle.party)
- & (ple.voucher_type == gle.voucher_type)
- & (ple.voucher_no == gle.voucher_no)
- & (ple.company == gle.company)
- )
- .select(
- ple.company,
- ple.account,
- ple.party_type,
- ple.party,
- ple.voucher_type,
- ple.voucher_no,
- gle.remarks.as_("gle_remarks"),
- )
- .where((ple.delinked == 0) & (gle.is_cancelled == 0))
- .run(as_dict=True)
- )
+ # Get empty PLE records
+ un_processed = (
+ qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run()
+ )[0][0]
- pl_entries = remove_duplicate_entries(pl_entries)
+ if un_processed:
+ print(f"Remarks for {un_processed} Payment Ledger records will be updated from GL Entry")
- if pl_entries:
- # split into multiple batches, update and commit for each batch
+ ifelse = CustomFunction("IF", ["condition", "then", "else"])
+
+ processed = 0
+ last_percent_update = 0
batch_size = 1000
- for batch in create_batch(pl_entries, batch_size):
- for entry in batch:
- query = (
- qb.update(ple)
- .set(ple.remarks, entry.gle_remarks)
- .where(
- (ple.company == entry.company)
- & (ple.account == entry.account)
- & (ple.party_type == entry.party_type)
- & (ple.party == entry.party)
- & (ple.voucher_type == entry.voucher_type)
- & (ple.voucher_no == entry.voucher_no)
- )
- )
- query.run()
+ last_name = None
- frappe.db.commit()
+ while True:
+ if last_name:
+ where_clause = (ple.name.gt(last_name)) & (ple.remarks.isnull()) & (ple.delinked == 0)
+ else:
+ where_clause = (ple.remarks.isnull()) & (ple.delinked == 0)
+
+ # results are deterministic
+ names = (
+ qb.from_(ple).select(ple.name).where(where_clause).orderby(ple.name).limit(batch_size).run()
+ )
+
+ if names:
+ last_name = names[-1][0]
+
+ pl_entries = (
+ qb.from_(ple)
+ .left_join(gle)
+ .on(
+ (ple.account == gle.account)
+ & (ple.party_type == gle.party_type)
+ & (ple.party == gle.party)
+ & (ple.voucher_type == gle.voucher_type)
+ & (ple.voucher_no == gle.voucher_no)
+ & (
+ ple.against_voucher_type
+ == IfNull(
+ ifelse(gle.against_voucher_type == "", None, gle.against_voucher_type), gle.voucher_type
+ )
+ )
+ & (
+ ple.against_voucher_no
+ == IfNull(ifelse(gle.against_voucher == "", None, gle.against_voucher), gle.voucher_no)
+ )
+ & (ple.company == gle.company)
+ & (
+ ((ple.account_type == "Receivable") & (ple.amount == (gle.debit - gle.credit)))
+ | (ple.account_type == "Payable") & (ple.amount == (gle.credit - gle.debit))
+ )
+ & (gle.remarks.notnull())
+ & (gle.is_cancelled == 0)
+ )
+ .select(ple.name)
+ .distinct()
+ .select(
+ gle.remarks.as_("gle_remarks"),
+ )
+ .where(ple.name.isin(names))
+ .run(as_dict=True)
+ )
+
+ if pl_entries:
+ for entry in pl_entries:
+ query = qb.update(ple).set(ple.remarks, entry.gle_remarks).where((ple.name == entry.name))
+ query.run()
+
+ frappe.db.commit()
+
+ processed += len(pl_entries)
+ percentage = flt((processed / un_processed) * 100, 2)
+ if percentage - last_percent_update > 1:
+ print(f"{percentage}% ({processed}) PLE records updated")
+ last_percent_update = percentage
+
+ else:
+ break
+ print("Remarks succesfully migrated")
diff --git a/erpnext/patches/v14_0/set_pick_list_status.py b/erpnext/patches/v14_0/set_pick_list_status.py
new file mode 100644
index 0000000..eea5745
--- /dev/null
+++ b/erpnext/patches/v14_0/set_pick_list_status.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+
+import frappe
+from pypika.terms import ExistsCriterion
+
+
+def execute():
+ pl = frappe.qb.DocType("Pick List")
+ se = frappe.qb.DocType("Stock Entry")
+ dn = frappe.qb.DocType("Delivery Note")
+
+ (
+ frappe.qb.update(pl).set(
+ pl.status,
+ (
+ frappe.qb.terms.Case()
+ .when(pl.docstatus == 0, "Draft")
+ .when(pl.docstatus == 2, "Cancelled")
+ .else_("Completed")
+ ),
+ )
+ ).run()
+
+ (
+ frappe.qb.update(pl)
+ .set(pl.status, "Open")
+ .where(
+ (
+ ExistsCriterion(
+ frappe.qb.from_(se).select(se.name).where((se.docstatus == 1) & (se.pick_list == pl.name))
+ )
+ | ExistsCriterion(
+ frappe.qb.from_(dn).select(dn.name).where((dn.docstatus == 1) & (dn.pick_list == pl.name))
+ )
+ ).negate()
+ & (pl.docstatus == 1)
+ )
+ ).run()
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
index 5dc3cdd..371ecbc 100644
--- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -1,9 +1,5 @@
import frappe
-from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- set_draft_asset_depr_schedule_details,
-)
-
def execute():
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
@@ -16,7 +12,7 @@
for fb_row in finance_book_rows:
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
- set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset, fb_row)
+ asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row)
asset_depr_schedule_doc.insert()
diff --git a/erpnext/patches/v15_0/update_asset_value_for_manual_depr_entries.py b/erpnext/patches/v15_0/update_asset_value_for_manual_depr_entries.py
new file mode 100644
index 0000000..5d7b5cf
--- /dev/null
+++ b/erpnext/patches/v15_0/update_asset_value_for_manual_depr_entries.py
@@ -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()
diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
index 27c8fe4..3df56e6 100644
--- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py
+++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
@@ -10,62 +10,6 @@
class TestHomepageSection(unittest.TestCase):
- def test_homepage_section_card(self):
- try:
- frappe.get_doc(
- {
- "doctype": "Homepage Section",
- "name": "Card Section",
- "section_based_on": "Cards",
- "section_cards": [
- {
- "title": "Card 1",
- "subtitle": "Subtitle 1",
- "content": "This is test card 1",
- "route": "/card-1",
- },
- {
- "title": "Card 2",
- "subtitle": "Subtitle 2",
- "content": "This is test card 2",
- "image": "test.jpg",
- },
- ],
- "no_of_columns": 3,
- }
- ).insert(ignore_if_duplicate=True)
- except frappe.DuplicateEntryError:
- pass
-
- set_request(method="GET", path="home")
- response = get_response()
-
- self.assertEqual(response.status_code, 200)
-
- html = frappe.safe_decode(response.get_data())
-
- soup = BeautifulSoup(html, "html.parser")
- sections = soup.find("main").find_all("section")
- self.assertEqual(len(sections), 3)
-
- homepage_section = sections[2]
- self.assertEqual(homepage_section.h3.text, "Card Section")
-
- cards = homepage_section.find_all(class_="card")
-
- self.assertEqual(len(cards), 2)
- self.assertEqual(cards[0].h5.text, "Card 1")
- self.assertEqual(cards[0].a["href"], "/card-1")
- self.assertEqual(cards[1].p.text, "Subtitle 2")
-
- img = cards[1].find(class_="card-img-top")
-
- self.assertEqual(img["src"], "test.jpg")
- self.assertEqual(img["loading"], "lazy")
-
- # cleanup
- frappe.db.rollback()
-
def test_homepage_section_custom_html(self):
frappe.get_doc(
{
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 3a778fa..09f2c5d 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1691,6 +1691,10 @@
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) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) {
if (!me.frm.doc[fieldname]) {
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index 9288f51..a913844 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -13,20 +13,12 @@
erpnext.setup.slides_settings = [
{
- // Brand
- name: 'brand',
- icon: "fa fa-bookmark",
- title: __("The Brand"),
- // help: __('Upload your letter head and logo. (you can edit them later).'),
+ // Organization
+ name: 'organization',
+ title: __("Setup your organization"),
+ icon: "fa fa-building",
fields: [
{
- fieldtype: "Attach Image", fieldname: "attach_logo",
- label: __("Attach Logo"),
- description: __("100px by 100px"),
- is_private: 0,
- align: 'center'
- },
- {
fieldname: 'company_name',
label: __('Company Name'),
fieldtype: 'Data',
@@ -35,54 +27,9 @@
{
fieldname: 'company_abbr',
label: __('Company Abbreviation'),
- fieldtype: 'Data'
- }
- ],
- onload: function(slide) {
- this.bind_events(slide);
- },
- bind_events: function (slide) {
- slide.get_input("company_name").on("change", function () {
- var parts = slide.get_input("company_name").val().split(" ");
- var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
- slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
- }).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
-
- slide.get_input("company_abbr").on("change", function () {
- if (slide.get_input("company_abbr").val().length > 10) {
- frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
- slide.get_field("company_abbr").set_value("");
- }
- });
- },
- validate: function() {
- if ((this.values.company_name || "").toLowerCase() == "company") {
- frappe.msgprint(__("Company Name cannot be Company"));
- return false;
- }
- if (!this.values.company_abbr) {
- return false;
- }
- if (this.values.company_abbr.length > 10) {
- return false;
- }
- return true;
- }
- },
- {
- // Organisation
- name: 'organisation',
- title: __("Your Organization"),
- icon: "fa fa-building",
- fields: [
- {
- fieldname: 'company_tagline',
- label: __('What does it do?'),
fieldtype: 'Data',
- placeholder: __('e.g. "Build tools for builders"'),
- reqd: 1
+ hidden: 1
},
- { fieldname: 'bank_account', label: __('Bank Name'), fieldtype: 'Data', reqd: 1 },
{
fieldname: 'chart_of_accounts', label: __('Chart of Accounts'),
options: "", fieldtype: 'Select'
@@ -94,40 +41,24 @@
],
onload: function (slide) {
- this.load_chart_of_accounts(slide);
this.bind_events(slide);
+ this.load_chart_of_accounts(slide);
this.set_fy_dates(slide);
},
-
validate: function () {
- let me = this;
- let exist;
-
if (!this.validate_fy_dates()) {
return false;
}
- // Validate bank name
- if(me.values.bank_account) {
- frappe.call({
- async: false,
- method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",
- args: {
- "coa": me.values.chart_of_accounts,
- "bank_account": me.values.bank_account
- },
- callback: function (r) {
- if(r.message){
- exist = r.message;
- me.get_field("bank_account").set_value("");
- let message = __('Account {0} already exists. Please enter a different name for your bank account.',
- [me.values.bank_account]
- );
- frappe.msgprint(message);
- }
- }
- });
- return !exist; // Return False if exist = true
+ if ((this.values.company_name || "").toLowerCase() == "company") {
+ frappe.msgprint(__("Company Name cannot be Company"));
+ return false;
+ }
+ if (!this.values.company_abbr) {
+ return false;
+ }
+ if (this.values.company_abbr.length > 10) {
+ return false;
}
return true;
@@ -151,15 +82,15 @@
var country = frappe.wizard.values.country;
if (country) {
- var fy = erpnext.setup.fiscal_years[country];
- var current_year = moment(new Date()).year();
- var next_year = current_year + 1;
+ let fy = erpnext.setup.fiscal_years[country];
+ let current_year = moment(new Date()).year();
+ let next_year = current_year + 1;
if (!fy) {
fy = ["01-01", "12-31"];
next_year = current_year;
}
- var year_start_date = current_year + "-" + fy[0];
+ let year_start_date = current_year + "-" + fy[0];
if (year_start_date > frappe.datetime.get_today()) {
next_year = current_year;
current_year -= 1;
@@ -171,7 +102,7 @@
load_chart_of_accounts: function (slide) {
- var country = frappe.wizard.values.country;
+ let country = frappe.wizard.values.country;
if (country) {
frappe.call({
@@ -202,12 +133,25 @@
me.charts_modal(slide, chart_template);
});
+
+ slide.get_input("company_name").on("change", function () {
+ let parts = slide.get_input("company_name").val().split(" ");
+ let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
+ slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
+ }).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
+
+ slide.get_input("company_abbr").on("change", function () {
+ if (slide.get_input("company_abbr").val().length > 10) {
+ frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
+ slide.get_field("company_abbr").set_value("");
+ }
+ });
},
charts_modal: function(slide, chart_template) {
let parent = __('All Accounts');
- var dialog = new frappe.ui.Dialog({
+ let dialog = new frappe.ui.Dialog({
title: chart_template,
fields: [
{'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button',
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index d37b7bb..51dcd64 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -491,7 +491,20 @@
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",
@@ -588,7 +601,7 @@
})
}
- const dialog = new frappe.ui.Dialog({
+ new frappe.ui.Dialog({
title: __("Update Items"),
fields: [
{
@@ -624,24 +637,7 @@
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) {
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index 999ddc2..158ac1d 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -17,45 +17,79 @@
def search_by_term(search_term, warehouse, price_list):
result = search_for_serial_or_batch_or_barcode_number(search_term) or {}
- item_code = result.get("item_code") or search_term
- serial_no = result.get("serial_no") or ""
- batch_no = result.get("batch_no") or ""
- barcode = result.get("barcode") or ""
+ item_code = result.get("item_code", search_term)
+ serial_no = result.get("serial_no", "")
+ batch_no = result.get("batch_no", "")
+ barcode = result.get("barcode", "")
- if result:
- item_info = frappe.db.get_value(
- "Item",
- item_code,
- [
- "name as item_code",
- "item_name",
- "description",
- "stock_uom",
- "image as item_image",
- "is_stock_item",
- ],
- as_dict=1,
- )
+ if not result:
+ return
- item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
- price_list_rate, currency = frappe.db.get_value(
- "Item Price",
- {"price_list": price_list, "item_code": item_code},
- ["price_list_rate", "currency"],
- ) or [None, None]
+ item_doc = frappe.get_doc("Item", item_code)
- item_info.update(
+ if not item_doc:
+ return
+
+ item = {
+ "barcode": barcode,
+ "batch_no": batch_no,
+ "description": item_doc.description,
+ "is_stock_item": item_doc.is_stock_item,
+ "item_code": item_doc.name,
+ "item_image": item_doc.image,
+ "item_name": item_doc.item_name,
+ "serial_no": serial_no,
+ "stock_uom": item_doc.stock_uom,
+ "uom": item_doc.stock_uom,
+ }
+
+ if barcode:
+ barcode_info = next(filter(lambda x: x.barcode == barcode, item_doc.get("barcodes", [])), None)
+ if barcode_info and barcode_info.uom:
+ uom = next(filter(lambda x: x.uom == barcode_info.uom, item_doc.uoms), {})
+ item.update(
+ {
+ "uom": barcode_info.uom,
+ "conversion_factor": uom.get("conversion_factor", 1),
+ }
+ )
+
+ item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
+ item_stock_qty = item_stock_qty // item.get("conversion_factor")
+ item.update({"actual_qty": item_stock_qty})
+
+ price = frappe.get_list(
+ doctype="Item Price",
+ filters={
+ "price_list": price_list,
+ "item_code": item_code,
+ },
+ fields=["uom", "stock_uom", "currency", "price_list_rate"],
+ )
+
+ def __sort(p):
+ p_uom = p.get("uom")
+
+ if p_uom == item.get("uom"):
+ return 0
+ elif p_uom == item.get("stock_uom"):
+ return 1
+ else:
+ return 2
+
+ # sort by fallback preference. always pick exact uom match if available
+ price = sorted(price, key=__sort)
+
+ if len(price) > 0:
+ p = price.pop(0)
+ item.update(
{
- "serial_no": serial_no,
- "batch_no": batch_no,
- "barcode": barcode,
- "price_list_rate": price_list_rate,
- "currency": currency,
- "actual_qty": item_stock_qty,
+ "currency": p.get("currency"),
+ "price_list_rate": p.get("price_list_rate"),
}
)
- return {"items": [item_info]}
+ return {"items": [item]}
@frappe.whitelist()
@@ -121,33 +155,43 @@
as_dict=1,
)
- if items_data:
- items = [d.item_code for d in items_data]
- item_prices_data = frappe.get_all(
+ # return (empty) list if there are no results
+ if not items_data:
+ return result
+
+ for item in items_data:
+ uoms = frappe.get_doc("Item", item.item_code).get("uoms", [])
+
+ item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
+ item.uom = item.stock_uom
+
+ item_price = frappe.get_all(
"Item Price",
- fields=["item_code", "price_list_rate", "currency"],
- filters={"price_list": price_list, "item_code": ["in", items]},
+ fields=["price_list_rate", "currency", "uom"],
+ filters={
+ "price_list": price_list,
+ "item_code": item.item_code,
+ "selling": True,
+ },
)
- item_prices = {}
- for d in item_prices_data:
- item_prices[d.item_code] = d
+ if not item_price:
+ result.append(item)
- for item in items_data:
- item_code = item.item_code
- item_price = item_prices.get(item_code) or {}
- item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
+ for price in item_price:
+ uom = next(filter(lambda x: x.uom == price.uom, uoms), {})
- row = {}
- row.update(item)
- row.update(
+ if price.uom != item.stock_uom and uom and uom.conversion_factor:
+ item.actual_qty = item.actual_qty // uom.conversion_factor
+
+ result.append(
{
- "price_list_rate": item_price.get("price_list_rate"),
- "currency": item_price.get("currency"),
- "actual_qty": item_stock_qty,
+ **item,
+ "price_list_rate": price.get("price_list_rate"),
+ "currency": price.get("currency"),
+ "uom": price.uom or item.uom,
}
)
- result.append(row)
return {"items": result}
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 595b919..c442774 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -542,12 +542,12 @@
if (!this.frm.doc.customer)
return this.raise_customer_selection_alert();
- const { item_code, batch_no, serial_no, rate } = item;
+ const { item_code, batch_no, serial_no, rate, uom } = item;
if (!item_code)
return;
- const new_item = { item_code, batch_no, rate, [field]: value };
+ const new_item = { item_code, batch_no, rate, uom, [field]: value };
if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
@@ -649,6 +649,7 @@
const is_stock_item = resp[1];
frappe.dom.unfreeze();
+ const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold()
@@ -664,7 +665,7 @@
}
} else if (is_stock_item && available_qty < qty_needed) {
frappe.throw({
- message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
+ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.', [bold_item_code, bold_warehouse, bold_available_qty, bold_uom]),
indicator: 'orange'
});
frappe.utils.play_sound("error");
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index e7dd211..12cc629 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -609,7 +609,7 @@
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return `
<div class="item-qty-rate">
- <div class="item-qty"><span>${item_data.qty || 0}</span></div>
+ <div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.amount, currency)}</div>
<div class="item-amount">${format_currency(item_data.rate, currency)}</div>
@@ -618,7 +618,7 @@
} else {
return `
<div class="item-qty-rate">
- <div class="item-qty"><span>${item_data.qty || 0}</span></div>
+ <div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.rate, currency)}</div>
</div>
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index b5eb048..ec67bdf 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -78,7 +78,7 @@
get_item_html(item) {
const me = this;
// eslint-disable-next-line no-unused-vars
- const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item;
+ const { item_image, serial_no, batch_no, barcode, actual_qty, uom, price_list_rate } = item;
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
let indicator_color;
let qty_to_display = actual_qty;
@@ -118,7 +118,7 @@
return (
`<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
- data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
+ data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
data-rate="${escape(price_list_rate || 0)}"
title="${item.item_name}">
@@ -128,7 +128,7 @@
<div class="item-name">
${frappe.ellipsis(item.item_name, 18)}
</div>
- <div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0}</div>
+ <div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}</div>
</div>
</div>`
);
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index 40165c3..be75bd6 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -94,7 +94,7 @@
get_item_html(doc, item_data) {
return `<div class="item-row-wrapper">
<div class="item-name">${item_data.item_name}</div>
- <div class="item-qty">${item_data.qty || 0}</div>
+ <div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div>
<div class="item-rate-disc">${get_rate_discount_html()}</div>
</div>`;
diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index e10df2a..44c4d54 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -41,8 +41,20 @@
{"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 @@
},
{
"label": _("Billed Amount"),
- "fieldtype": "currency",
+ "fieldtype": "Currency",
"fieldname": "billed_amount",
+ "options": "currency",
"width": 120,
},
{
@@ -104,6 +117,13 @@
"options": "Company",
"width": 100,
},
+ {
+ "label": _("Currency"),
+ "fieldtype": "Link",
+ "fieldname": "currency",
+ "options": "Currency",
+ "hidden": 1,
+ },
]
@@ -141,31 +161,12 @@
"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_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 = {}
diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py
index aadc989..ace5cca 100644
--- a/erpnext/setup/setup_wizard/operations/company_setup.py
+++ b/erpnext/setup/setup_wizard/operations/company_setup.py
@@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.utils import cstr, getdate
-from .default_website import website_maker
def create_fiscal_year_and_company(args):
@@ -48,83 +47,6 @@
).insert()
-def create_email_digest():
- from frappe.utils.user import get_system_managers
-
- system_managers = get_system_managers(only_name=True)
-
- if not system_managers:
- return
-
- recipients = []
- for d in system_managers:
- recipients.append({"recipient": d})
-
- companies = frappe.db.sql_list("select name FROM `tabCompany`")
- for company in companies:
- if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company):
- edigest = frappe.get_doc(
- {
- "doctype": "Email Digest",
- "name": "Default Weekly Digest - " + company,
- "company": company,
- "frequency": "Weekly",
- "recipients": recipients,
- }
- )
-
- for df in edigest.meta.get("fields", {"fieldtype": "Check"}):
- if df.fieldname != "scheduler_errors":
- edigest.set(df.fieldname, 1)
-
- edigest.insert()
-
- # scheduler errors digest
- if companies:
- edigest = frappe.new_doc("Email Digest")
- edigest.update(
- {
- "name": "Scheduler Errors",
- "company": companies[0],
- "frequency": "Daily",
- "recipients": recipients,
- "scheduler_errors": 1,
- "enabled": 1,
- }
- )
- edigest.insert()
-
-
-def create_logo(args):
- if args.get("attach_logo"):
- attach_logo = args.get("attach_logo").split(",")
- if len(attach_logo) == 3:
- filename, filetype, content = attach_logo
- _file = frappe.get_doc(
- {
- "doctype": "File",
- "file_name": filename,
- "attached_to_doctype": "Website Settings",
- "attached_to_name": "Website Settings",
- "decode": True,
- }
- )
- _file.save()
- fileurl = _file.file_url
- frappe.db.set_value(
- "Website Settings",
- "Website Settings",
- "brand_html",
- "<img src='{0}' style='max-width: 40px; max-height: 25px;'> {1}".format(
- fileurl, args.get("company_name")
- ),
- )
-
-
-def create_website(args):
- website_maker(args)
-
-
def get_fy_details(fy_start_date, fy_end_date):
start_year = getdate(fy_start_date).year
if start_year == getdate(fy_end_date).year:
diff --git a/erpnext/setup/setup_wizard/operations/default_website.py b/erpnext/setup/setup_wizard/operations/default_website.py
deleted file mode 100644
index 40b02b3..0000000
--- a/erpnext/setup/setup_wizard/operations/default_website.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-from frappe import _
-from frappe.utils import nowdate
-
-
-class website_maker(object):
- def __init__(self, args):
- self.args = args
- self.company = args.company_name
- self.tagline = args.company_tagline
- self.user = args.get("email")
- self.make_web_page()
- self.make_website_settings()
- self.make_blog()
-
- def make_web_page(self):
- # home page
- homepage = frappe.get_doc("Homepage", "Homepage")
- homepage.company = self.company
- homepage.tag_line = self.tagline
- homepage.setup_items()
- homepage.save()
-
- def make_website_settings(self):
- # update in home page in settings
- website_settings = frappe.get_doc("Website Settings", "Website Settings")
- website_settings.home_page = "home"
- website_settings.brand_html = self.company
- website_settings.copyright = self.company
- website_settings.top_bar_items = []
- website_settings.append(
- "top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"}
- )
- website_settings.append(
- "top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"}
- )
- website_settings.append(
- "top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"}
- )
- website_settings.save()
-
- def make_blog(self):
- blog_category = frappe.get_doc(
- {"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")}
- ).insert()
-
- if not self.user:
- # Admin setup
- return
-
- blogger = frappe.new_doc("Blogger")
- user = frappe.get_doc("User", self.user)
- blogger.user = self.user
- blogger.full_name = user.first_name + (" " + user.last_name if user.last_name else "")
- blogger.short_name = user.first_name.lower()
- blogger.avatar = user.user_image
- blogger.insert()
-
- frappe.get_doc(
- {
- "doctype": "Blog Post",
- "title": "Welcome",
- "published": 1,
- "published_on": nowdate(),
- "blogger": blogger.name,
- "blog_category": blog_category.name,
- "blog_intro": "My First Blog",
- "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(),
- }
- ).insert()
-
-
-def test():
- frappe.delete_doc("Web Page", "test-company")
- frappe.delete_doc("Blog Post", "welcome")
- frappe.delete_doc("Blogger", "administrator")
- frappe.delete_doc("Blog Category", "general")
- website_maker(
- {
- "company": "Test Company",
- "company_tagline": "Better Tools for Everyone",
- "name": "Administrator",
- }
- )
- frappe.db.commit()
diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py
index bd86a5b..65b268e 100644
--- a/erpnext/setup/setup_wizard/setup_wizard.py
+++ b/erpnext/setup/setup_wizard/setup_wizard.py
@@ -5,7 +5,6 @@
import frappe
from frappe import _
-from .operations import company_setup
from .operations import install_fixtures as fixtures
@@ -35,7 +34,6 @@
"fail_msg": "Failed to set defaults",
"tasks": [
{"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")},
- {"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")},
],
},
{
@@ -60,12 +58,6 @@
fixtures.install_defaults(frappe._dict(args))
-def stage_four(args):
- company_setup.create_website(args)
- company_setup.create_email_digest()
- company_setup.create_logo(args)
-
-
def fin(args):
frappe.local.message_log = []
login_as_first_user(args)
@@ -81,5 +73,4 @@
stage_fixtures(args)
setup_company(args)
setup_defaults(args)
- stage_four(args)
fin(args)
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 1be528f..b09b715 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -42,7 +42,7 @@
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 @@
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);
});
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 9f409d4..72654e6 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -159,13 +159,18 @@
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)
.limit(1)
.run()
)
+ actual_qty = 0.0
if last_sle_qty:
actual_qty = last_sle_qty[0][0]
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index a1df764..9f9f5cb 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -228,6 +228,7 @@
def on_submit(self):
self.validate_packed_qty()
+ self.update_pick_list_status()
# Check for Approving Authority
frappe.get_doc("Authorization Control").validate_approving_authority(
@@ -313,6 +314,11 @@
if has_error:
raise frappe.ValidationError
+ def update_pick_list_status(self):
+ from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
+
+ update_pick_list_status(self.pick_list)
+
def check_next_docstatus(self):
submit_rv = frappe.db.sql(
"""select t1.name
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
index ba1023a..0310682 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
@@ -37,7 +37,7 @@
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
&& frm.doc.__onload.has_stock_ledger.length) {
let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
- 'type_of_transaction', 'condition'];
+ 'type_of_transaction', 'condition', 'mandatory_depends_on'];
frm.fields.forEach((field) => {
if (!in_list(allow_to_edit_fields, field.df.fieldname)) {
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
index 4397e11..eb6102a 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
@@ -24,6 +24,9 @@
"istable",
"applicable_condition_example_section",
"condition",
+ "conditional_mandatory_section",
+ "reqd",
+ "mandatory_depends_on",
"conditional_rule_examples_section",
"html_19"
],
@@ -153,11 +156,28 @@
"fieldname": "conditional_rule_examples_section",
"fieldtype": "Section Break",
"label": "Conditional Rule Examples"
+ },
+ {
+ "description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.",
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Small Text",
+ "label": "Mandatory Depends On"
+ },
+ {
+ "fieldname": "conditional_mandatory_section",
+ "fieldtype": "Section Break",
+ "label": "Mandatory Section"
+ },
+ {
+ "default": "0",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "label": "Mandatory"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2022-11-15 15:50:16.767105",
+ "modified": "2023-01-31 13:44:38.507698",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
index 009548a..db2b5d0 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
@@ -126,6 +126,8 @@
insert_after="inventory_dimension",
options=self.reference_document,
label=self.dimension_name,
+ reqd=self.reqd,
+ mandatory_depends_on=self.mandatory_depends_on,
),
]
@@ -142,6 +144,8 @@
"Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname}
) and not field_exists("Stock Ledger Entry", self.target_fieldname):
dimension_field = dimension_fields[1]
+ dimension_field["mandatory_depends_on"] = ""
+ dimension_field["reqd"] = 0
dimension_field["fieldname"] = self.target_fieldname
custom_fields["Stock Ledger Entry"] = dimension_field
diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
index edff3fd..28b1ed9 100644
--- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
@@ -85,6 +85,9 @@
condition="parent.purpose == 'Material Issue'",
)
+ inv_dim1.reqd = 0
+ inv_dim1.save()
+
create_inventory_dimension(
reference_document="Shelf",
type_of_transaction="Inward",
@@ -205,6 +208,48 @@
)
)
+ def test_check_mandatory_dimensions(self):
+ doc = create_inventory_dimension(
+ reference_document="Pallet",
+ type_of_transaction="Outward",
+ dimension_name="Pallet",
+ apply_to_all_doctypes=0,
+ document_type="Stock Entry Detail",
+ )
+
+ doc.reqd = 1
+ doc.save()
+
+ self.assertTrue(
+ frappe.db.get_value(
+ "Custom Field", {"fieldname": "pallet", "dt": "Stock Entry Detail", "reqd": 1}, "name"
+ )
+ )
+
+ doc.load_from_db
+ doc.reqd = 0
+ doc.save()
+
+ def test_check_mandatory_depends_on_dimensions(self):
+ doc = create_inventory_dimension(
+ reference_document="Pallet",
+ type_of_transaction="Outward",
+ dimension_name="Pallet",
+ apply_to_all_doctypes=0,
+ document_type="Stock Entry Detail",
+ )
+
+ doc.mandatory_depends_on = "t_warehouse"
+ doc.save()
+
+ self.assertTrue(
+ frappe.db.get_value(
+ "Custom Field",
+ {"fieldname": "pallet", "dt": "Stock Entry Detail", "mandatory_depends_on": "t_warehouse"},
+ "name",
+ )
+ )
+
def prepare_test_data():
if not frappe.db.exists("DocType", "Shelf"):
@@ -251,6 +296,22 @@
create_warehouse("Rack Warehouse")
+ if not frappe.db.exists("DocType", "Pallet"):
+ frappe.get_doc(
+ {
+ "doctype": "DocType",
+ "name": "Pallet",
+ "module": "Stock",
+ "custom": 1,
+ "naming_rule": "By fieldname",
+ "autoname": "field:pallet_name",
+ "fields": [{"label": "Pallet Name", "fieldname": "pallet_name", "fieldtype": "Data"}],
+ "permissions": [
+ {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
+ ],
+ }
+ ).insert(ignore_permissions=True)
+
def create_inventory_dimension(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index e61f0f5..5bcb05a 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -894,6 +894,12 @@
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);
+ }
+ ])
});
}
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 5f05de6..156e591 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -366,10 +366,11 @@
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) {
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index e1c3f0f..7259dc0 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -26,7 +26,8 @@
"locations",
"amended_from",
"print_settings_section",
- "group_same_items"
+ "group_same_items",
+ "status"
],
"fields": [
{
@@ -168,11 +169,26 @@
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Draft\nOpen\nCompleted\nCancelled",
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1,
+ "reqd": 1,
+ "search_index": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2022-07-19 11:03:04.442174",
+ "modified": "2023-01-24 10:33:43.244476",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
@@ -244,4 +260,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 808f19e..bf3b5dd 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -11,7 +11,8 @@
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case
-from frappe.query_builder.functions import IfNull, Locate, Sum
+from frappe.query_builder.custom import GROUP_CONCAT
+from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum
from frappe.utils import cint, floor, flt, today
from frappe.utils.nestedset import get_descendants_of
@@ -77,15 +78,32 @@
)
def on_submit(self):
+ self.update_status()
self.update_bundle_picked_qty()
self.update_reference_qty()
self.update_sales_order_picking_status()
def on_cancel(self):
+ self.update_status()
self.update_bundle_picked_qty()
self.update_reference_qty()
self.update_sales_order_picking_status()
+ def update_status(self, status=None, update_modified=True):
+ if not status:
+ if self.docstatus == 0:
+ status = "Draft"
+ elif self.docstatus == 1:
+ if self.status == "Draft":
+ status = "Open"
+ elif target_document_exists(self.name, self.purpose):
+ status = "Completed"
+ elif self.docstatus == 2:
+ status = "Cancelled"
+
+ if status:
+ frappe.db.set_value("Pick List", self.name, "status", status, update_modified=update_modified)
+
def update_reference_qty(self):
packed_items = []
so_items = []
@@ -162,6 +180,7 @@
def set_item_locations(self, save=False):
self.validate_for_qty()
items = self.aggregate_item_qty()
+ picked_items_details = self.get_picked_items_details(items)
self.item_location_map = frappe._dict()
from_warehouses = None
@@ -180,7 +199,11 @@
self.item_location_map.setdefault(
item_code,
get_available_item_locations(
- item_code, from_warehouses, self.item_count_map.get(item_code), self.company
+ item_code,
+ from_warehouses,
+ self.item_count_map.get(item_code),
+ self.company,
+ picked_item_details=picked_items_details.get(item_code),
),
)
@@ -309,6 +332,56 @@
already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
)
+ def get_picked_items_details(self, items):
+ picked_items = frappe._dict()
+
+ if items:
+ pi = frappe.qb.DocType("Pick List")
+ pi_item = frappe.qb.DocType("Pick List Item")
+ query = (
+ frappe.qb.from_(pi)
+ .inner_join(pi_item)
+ .on(pi.name == pi_item.parent)
+ .select(
+ pi_item.item_code,
+ pi_item.warehouse,
+ pi_item.batch_no,
+ Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
+ "picked_qty"
+ ),
+ Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
+ )
+ .where(
+ (pi_item.item_code.isin([x.item_code for x in items]))
+ & ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
+ & (pi.status != "Completed")
+ & (pi_item.docstatus != 2)
+ )
+ .groupby(
+ pi_item.item_code,
+ pi_item.warehouse,
+ pi_item.batch_no,
+ )
+ )
+
+ if self.name:
+ query = query.where(pi_item.parent != self.name)
+
+ items_data = query.run(as_dict=True)
+
+ for item_data in items_data:
+ key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse
+ serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None
+ data = {"picked_qty": item_data.picked_qty}
+ if serial_no:
+ data["serial_no"] = serial_no
+ if item_data.item_code not in picked_items:
+ picked_items[item_data.item_code] = {key: data}
+ else:
+ picked_items[item_data.item_code][key] = data
+
+ return picked_items
+
def _get_product_bundles(self) -> Dict[str, str]:
# Dict[so_item_row: item_code]
product_bundles = {}
@@ -346,29 +419,30 @@
return int(flt(min(possible_bundles), precision or 6))
+def update_pick_list_status(pick_list):
+ if pick_list:
+ doc = frappe.get_doc("Pick List", pick_list)
+ doc.run_method("update_status")
+
+
def get_picked_items_qty(items) -> List[Dict]:
- return frappe.db.sql(
- f"""
- SELECT
- sales_order_item,
- item_code,
- sales_order,
- SUM(stock_qty) AS stock_qty,
- SUM(picked_qty) AS picked_qty
- FROM
- `tabPick List Item`
- WHERE
- sales_order_item IN (
- {", ".join(frappe.db.escape(d) for d in items)}
- )
- AND docstatus = 1
- GROUP BY
- sales_order_item,
- sales_order
- FOR UPDATE
- """,
- as_dict=1,
- )
+ pi_item = frappe.qb.DocType("Pick List Item")
+ return (
+ frappe.qb.from_(pi_item)
+ .select(
+ pi_item.sales_order_item,
+ pi_item.item_code,
+ pi_item.sales_order,
+ Sum(pi_item.stock_qty).as_("stock_qty"),
+ Sum(pi_item.picked_qty).as_("picked_qty"),
+ )
+ .where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items)))
+ .groupby(
+ pi_item.sales_order_item,
+ pi_item.sales_order,
+ )
+ .for_update()
+ ).run(as_dict=True)
def validate_item_locations(pick_list):
@@ -434,31 +508,38 @@
def get_available_item_locations(
- item_code, from_warehouses, required_qty, company, ignore_validation=False
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ ignore_validation=False,
+ picked_item_details=None,
):
locations = []
+ total_picked_qty = (
+ sum([v.get("picked_qty") for k, v in picked_item_details.items()]) if picked_item_details else 0
+ )
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
if has_batch_no and has_serial_no:
locations = get_available_item_locations_for_serial_and_batched_item(
- item_code, from_warehouses, required_qty, company
+ item_code, from_warehouses, required_qty, company, total_picked_qty
)
elif has_serial_no:
locations = get_available_item_locations_for_serialized_item(
- item_code, from_warehouses, required_qty, company
+ item_code, from_warehouses, required_qty, company, total_picked_qty
)
elif has_batch_no:
locations = get_available_item_locations_for_batched_item(
- item_code, from_warehouses, required_qty, company
+ item_code, from_warehouses, required_qty, company, total_picked_qty
)
else:
locations = get_available_item_locations_for_other_item(
- item_code, from_warehouses, required_qty, company
+ item_code, from_warehouses, required_qty, company, total_picked_qty
)
total_qty_available = sum(location.get("qty") for location in locations)
-
remaining_qty = required_qty - total_qty_available
if remaining_qty > 0 and not ignore_validation:
@@ -469,25 +550,60 @@
title=_("Insufficient Stock"),
)
+ if picked_item_details:
+ for location in list(locations):
+ key = (
+ (location["warehouse"], location["batch_no"])
+ if location.get("batch_no")
+ else location["warehouse"]
+ )
+
+ if key in picked_item_details:
+ picked_detail = picked_item_details[key]
+
+ if picked_detail.get("serial_no") and location.get("serial_no"):
+ location["serial_no"] = list(
+ set(location["serial_no"]).difference(set(picked_detail["serial_no"]))
+ )
+ location["qty"] = len(location["serial_no"])
+ else:
+ location["qty"] -= picked_detail.get("picked_qty")
+
+ if location["qty"] < 1:
+ locations.remove(location)
+
+ total_qty_available = sum(location.get("qty") for location in locations)
+ remaining_qty = required_qty - total_qty_available
+
+ if remaining_qty > 0 and not ignore_validation:
+ frappe.msgprint(
+ _("{0} units of Item {1} is picked in another Pick List.").format(
+ remaining_qty, frappe.get_desk_link("Item", item_code)
+ ),
+ title=_("Already Picked"),
+ )
+
return locations
def get_available_item_locations_for_serialized_item(
- item_code, from_warehouses, required_qty, company
+ item_code, from_warehouses, required_qty, company, total_picked_qty=0
):
- filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]})
+ sn = frappe.qb.DocType("Serial No")
+ query = (
+ frappe.qb.from_(sn)
+ .select(sn.name, sn.warehouse)
+ .where((sn.item_code == item_code) & (sn.company == company))
+ .orderby(sn.purchase_date)
+ .limit(cint(required_qty + total_picked_qty))
+ )
if from_warehouses:
- filters.warehouse = ["in", from_warehouses]
+ query = query.where(sn.warehouse.isin(from_warehouses))
+ else:
+ query = query.where(Coalesce(sn.warehouse, "") != "")
- serial_nos = frappe.get_all(
- "Serial No",
- fields=["name", "warehouse"],
- filters=filters,
- limit=required_qty,
- order_by="purchase_date",
- as_list=1,
- )
+ serial_nos = query.run(as_list=True)
warehouse_serial_nos_map = frappe._dict()
for serial_no, warehouse in serial_nos:
@@ -501,7 +617,7 @@
def get_available_item_locations_for_batched_item(
- item_code, from_warehouses, required_qty, company
+ item_code, from_warehouses, required_qty, company, total_picked_qty=0
):
sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
@@ -521,6 +637,7 @@
.groupby(sle.warehouse, sle.batch_no, sle.item_code)
.having(Sum(sle.actual_qty) > 0)
.orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
+ .limit(cint(required_qty + total_picked_qty))
)
if from_warehouses:
@@ -530,53 +647,58 @@
def get_available_item_locations_for_serial_and_batched_item(
- item_code, from_warehouses, required_qty, company
+ item_code, from_warehouses, required_qty, company, total_picked_qty=0
):
# Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company
)
- filters = frappe._dict(
- {"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""}
- )
+ if locations:
+ sn = frappe.qb.DocType("Serial No")
+ conditions = (sn.item_code == item_code) & (sn.company == company)
- # Get Serial Nos by FIFO for Batch No
- for location in locations:
- filters.batch_no = location.batch_no
- filters.warehouse = location.warehouse
- location.qty = (
- required_qty if location.qty > required_qty else location.qty
- ) # if extra qty in batch
+ for location in locations:
+ location.qty = (
+ required_qty if location.qty > required_qty else location.qty
+ ) # if extra qty in batch
- serial_nos = frappe.get_list(
- "Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date"
- )
+ serial_nos = (
+ frappe.qb.from_(sn)
+ .select(sn.name)
+ .where(
+ (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
+ )
+ .orderby(sn.purchase_date)
+ .limit(cint(location.qty + total_picked_qty))
+ ).run(as_dict=True)
- serial_nos = [sn.name for sn in serial_nos]
- location.serial_no = serial_nos
+ serial_nos = [sn.name for sn in serial_nos]
+ location.serial_no = serial_nos
+ location.qty = len(serial_nos)
return locations
-def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company):
- # gets all items available in different warehouses
- warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")]
-
- filters = frappe._dict(
- {"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]}
+def get_available_item_locations_for_other_item(
+ item_code, from_warehouses, required_qty, company, total_picked_qty=0
+):
+ bin = frappe.qb.DocType("Bin")
+ query = (
+ frappe.qb.from_(bin)
+ .select(bin.warehouse, bin.actual_qty.as_("qty"))
+ .where((bin.item_code == item_code) & (bin.actual_qty > 0))
+ .orderby(bin.creation)
+ .limit(cint(required_qty + total_picked_qty))
)
if from_warehouses:
- filters.warehouse = ["in", from_warehouses]
+ query = query.where(bin.warehouse.isin(from_warehouses))
+ else:
+ wh = frappe.qb.DocType("Warehouse")
+ query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
- item_locations = frappe.get_all(
- "Bin",
- fields=["warehouse", "actual_qty as qty"],
- filters=filters,
- limit=required_qty,
- order_by="creation",
- )
+ item_locations = query.run(as_dict=True)
return item_locations
diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js
new file mode 100644
index 0000000..ad88b0a
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/pick_list_list.js
@@ -0,0 +1,14 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.listview_settings['Pick List'] = {
+ get_indicator: function (doc) {
+ const status_colors = {
+ "Draft": "grey",
+ "Open": "orange",
+ "Completed": "green",
+ "Cancelled": "red",
+ };
+ return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
+ },
+};
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 43acdf0..1254fe3 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -414,6 +414,7 @@
pick_list.submit()
delivery_note = create_delivery_note(pick_list.name)
+ pick_list.load_from_db()
self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty)
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
@@ -663,3 +664,147 @@
self.assertEqual(dn.items[0].rate, 42)
so.reload()
self.assertEqual(so.per_delivered, 100)
+
+ def test_pick_list_status(self):
+ warehouse = "_Test Warehouse - _TC"
+ item = make_item(properties={"maintain_stock": 1}).name
+ make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
+
+ so = make_sales_order(item_code=item, qty=10, rate=100)
+
+ pl = create_pick_list(so.name)
+ pl.save()
+ pl.reload()
+ self.assertEqual(pl.status, "Draft")
+
+ pl.submit()
+ pl.reload()
+ self.assertEqual(pl.status, "Open")
+
+ dn = create_delivery_note(pl.name)
+ dn.save()
+ pl.reload()
+ self.assertEqual(pl.status, "Open")
+
+ dn.submit()
+ pl.reload()
+ self.assertEqual(pl.status, "Completed")
+
+ dn.cancel()
+ pl.reload()
+ self.assertEqual(pl.status, "Completed")
+
+ pl.cancel()
+ pl.reload()
+ self.assertEqual(pl.status, "Cancelled")
+
+ def test_consider_existing_pick_list(self):
+ def create_items(items_properties):
+ items = []
+
+ for properties in items_properties:
+ properties.update({"maintain_stock": 1})
+ item_code = make_item(properties=properties).name
+ properties.update({"item_code": item_code})
+ items.append(properties)
+
+ return items
+
+ def create_stock_entries(items):
+ warehouses = ["Stores - _TC", "Finished Goods - _TC"]
+
+ for item in items:
+ for warehouse in warehouses:
+ se = make_stock_entry(
+ item=item.get("item_code"),
+ to_warehouse=warehouse,
+ qty=5,
+ )
+
+ def get_item_list(items, qty, warehouse="All Warehouses - _TC"):
+ return [
+ {
+ "item_code": item.get("item_code"),
+ "qty": qty,
+ "warehouse": warehouse,
+ }
+ for item in items
+ ]
+
+ def get_picked_items_details(pick_list_doc):
+ items_data = {}
+
+ for location in pick_list_doc.locations:
+ key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse
+ serial_no = [x for x in location.serial_no.split("\n") if x] if location.serial_no else None
+ data = {"picked_qty": location.picked_qty}
+ if serial_no:
+ data["serial_no"] = serial_no
+ if location.item_code not in items_data:
+ items_data[location.item_code] = {key: data}
+ else:
+ items_data[location.item_code][key] = data
+
+ return items_data
+
+ # Step - 1: Setup - Create Items and Stock Entries
+ items_properties = [
+ {
+ "valuation_rate": 100,
+ },
+ {
+ "valuation_rate": 200,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ },
+ {
+ "valuation_rate": 300,
+ "has_serial_no": 1,
+ "serial_no_series": "SNO.###",
+ },
+ {
+ "valuation_rate": 400,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "has_serial_no": 1,
+ "serial_no_series": "SNO.###",
+ },
+ ]
+
+ items = create_items(items_properties)
+ create_stock_entries(items)
+
+ # Step - 2: Create Sales Order [1]
+ so1 = make_sales_order(item_list=get_item_list(items, qty=6))
+
+ # Step - 3: Create and Submit Pick List [1] for Sales Order [1]
+ pl1 = create_pick_list(so1.name)
+ pl1.submit()
+
+ # Step - 4: Create Sales Order [2] with same Item(s) as Sales Order [1]
+ so2 = make_sales_order(item_list=get_item_list(items, qty=4))
+
+ # Step - 5: Create Pick List [2] for Sales Order [2]
+ pl2 = create_pick_list(so2.name)
+ pl2.save()
+
+ # Step - 6: Assert
+ picked_items_details = get_picked_items_details(pl1)
+
+ for location in pl2.locations:
+ key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse
+ item_data = picked_items_details.get(location.item_code, {}).get(key, {})
+ picked_qty = item_data.get("picked_qty", 0)
+ picked_serial_no = picked_items_details.get("serial_no", [])
+ bin_actual_qty = frappe.db.get_value(
+ "Bin", {"item_code": location.item_code, "warehouse": location.warehouse}, "actual_qty"
+ )
+
+ # Available Qty to pick should be equal to [Actual Qty - Picked Qty]
+ self.assertEqual(location.stock_qty, bin_actual_qty - picked_qty)
+
+ # Serial No should not be in the Picked Serial No list
+ if location.serial_no:
+ a = set(picked_serial_no)
+ b = set([x for x in location.serial_no.split("\n") if x])
+ self.assertSetEqual(b, b.difference(a))
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 3739cb8..af0d148 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -888,7 +888,7 @@
# Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0
for item in pr_doc.items:
- return_data = frappe.db.get_list(
+ return_data = frappe.get_all(
"Purchase Receipt",
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
filters=[
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 9321c2c..2a9f091 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -221,7 +221,7 @@
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 []
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 1755f28..8c20ca0 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -158,6 +158,7 @@
self.validate_subcontract_order()
self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status()
+ self.update_pick_list_status()
self.make_gl_entries()
@@ -2276,6 +2277,11 @@
update_subcontracting_order_status(self.subcontracting_order)
+ def update_pick_list_status(self):
+ from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
+
+ update_pick_list_status(self.pick_list)
+
def set_missing_values(self):
"Updates rate and availability of all the items of mapped doc."
self.set_transfer_qty()
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 38bf0a5..cc06bd7 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1662,6 +1662,48 @@
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)
diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
index b5c6764..abbb33b 100644
--- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
+++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
@@ -62,7 +62,7 @@
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 @@
"""return columns"""
columns = [
- _("Item") + ":Link/Item:180",
- _("Item Group") + "::100",
+ _("Item") + ":Link/Item:150",
+ _("Item Name") + ":Link/Item:150",
+ _("Item Group") + "::120",
_("Value") + ":Currency:120",
- _("Age") + ":Float:80",
+ _("Age") + ":Float:120",
]
return columns
@@ -123,7 +124,7 @@
def add_warehouse_column(columns, warehouse_list):
if len(warehouse_list) > 1:
- columns += [_("Total Qty") + ":Int:90"]
+ columns += [_("Total Qty") + ":Int:120"]
for wh in warehouse_list:
- columns += [_(wh.name) + ":Int:120"]
+ columns += [_(wh.name) + ":Int:100"]
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 5d75bfd..08fc6fb 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1050,7 +1050,7 @@
frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
-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"
@@ -1076,13 +1076,13 @@
posting_date < %(posting_date)s or
(
posting_date = %(posting_date)s and
- time_format(posting_time, %(time_format)s) < time_format(%(posting_time)s, %(time_format)s)
+ 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,
@@ -1179,7 +1179,7 @@
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",
@@ -1375,7 +1375,7 @@
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"
)
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
index aab2fc9..7ca1264 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
@@ -11,6 +11,7 @@
"Partial Material Transferred": "purple",
"Material Transferred": "blue",
"Closed": "red",
+ "Cancelled": "red",
};
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
},
diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py
index e1e12bd..185ec66 100644
--- a/erpnext/templates/pages/order.py
+++ b/erpnext/templates/pages/order.py
@@ -38,22 +38,27 @@
if not frappe.has_website_permission(context.doc):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
- # check for the loyalty program of the customer
- customer_loyalty_program = frappe.db.get_value(
- "Customer", context.doc.customer, "loyalty_program"
- )
- if customer_loyalty_program:
- from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
- get_loyalty_program_details_with_points,
+ context.available_loyalty_points = 0.0
+ if context.doc.get("customer"):
+ # check for the loyalty program of the customer
+ customer_loyalty_program = frappe.db.get_value(
+ "Customer", context.doc.customer, "loyalty_program"
)
- loyalty_program_details = get_loyalty_program_details_with_points(
- context.doc.customer, customer_loyalty_program
- )
- context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
+ if customer_loyalty_program:
+ from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
+ get_loyalty_program_details_with_points,
+ )
- # show Make Purchase Invoice button based on permission
- context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create")
+ loyalty_program_details = get_loyalty_program_details_with_points(
+ context.doc.customer, customer_loyalty_program
+ )
+ context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
+
+ context.show_make_pi_button = False
+ if context.doc.get("supplier"):
+ # show Make Purchase Invoice button based on permission
+ context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create")
def get_attachments(dt, dn):
diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv
index 173320d..2337bcb 100644
--- a/erpnext/translations/zh.csv
+++ b/erpnext/translations/zh.csv
@@ -3537,7 +3537,6 @@
Rules for applying different promotional schemes.,适用不同促销计划的规则。,
Shift,转移,
Show {0},显示{0},
-"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",命名系列中不允许使用除"-", "#", "।", "/", "{{" 和 "}}"之外的特殊字符 {0},
Target Details,目标细节,
{0} already has a Parent Procedure {1}.,{0}已有父程序{1}。,
API,应用程序界面,
diff --git a/erpnext/translations/zh_tw.csv b/erpnext/translations/zh_tw.csv
index 313908f..1b7e186 100644
--- a/erpnext/translations/zh_tw.csv
+++ b/erpnext/translations/zh_tw.csv
@@ -3311,7 +3311,6 @@
Rules for applying different promotional schemes.,適用不同促銷計劃的規則。,
Shift,轉移,
Show {0},顯示{0},
-"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",命名系列中不允許使用除 "-", "#", "।", "/", "{{" 和 "}}"之外的特殊字符 {0},
Target Details,目標細節,
API,API,
Annual,年刊,