Merge branch 'develop' into alternative-items-quotation

This commit is contained in:
Marica 2023-02-06 16:30:10 +05:30 committed by GitHub
commit bd7e6e264e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 2358 additions and 1612 deletions

View File

@ -3,52 +3,71 @@ import requests
from urllib.parse import urlparse from urllib.parse import urlparse
docs_repos = [ WEBSITE_REPOS = [
"frappe_docs",
"erpnext_documentation",
"erpnext_com", "erpnext_com",
"frappe_io", "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): def is_valid_url(url: str) -> bool:
for line in body.splitlines(): parts = urlparse(url)
for word in line.split(): return all((parts.scheme, parts.netloc, parts.path))
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com": def is_documentation_link(word: str) -> bool:
parts = parsed_url.path.split('/') if not word.startswith("http") or not is_valid_url(word):
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: return False
return True
elif parsed_url.netloc == "docs.erpnext.com": parsed_url = urlparse(word)
return True 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__": if __name__ == "__main__":
pr = sys.argv[1] exit_code, message = check_pull_request(sys.argv[1])
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr)) print(message)
sys.exit(exit_code)
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... 🏃")

View File

@ -32,8 +32,8 @@ repos:
- id: black - id: black
additional_dependencies: ['click==8.0.4'] additional_dependencies: ['click==8.0.4']
- repo: https://github.com/timothycrosley/isort - repo: https://github.com/PyCQA/isort
rev: 5.9.1 rev: 5.12.0
hooks: hooks:
- id: isort - id: isort
exclude: ".*setup.py$" exclude: ".*setup.py$"

View File

@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = {
accounts = nodes; accounts = nodes;
} }
const get_balances = frappe.call({ frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
method: 'erpnext.accounts.utils.get_account_balances', if(value) {
args: {
accounts: accounts,
company: cur_tree.args.company
},
});
get_balances.then(r => { const get_balances = frappe.call({
if (!r.message || r.message.length == 0) return; 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]; for (let account of r.message) {
if (!node || node.is_root) continue;
// show Dr if positive since balance is calculated as debit - credit else show Cr const node = cur_tree.nodes && cur_tree.nodes[account.value];
const balance = account.balance_in_account_currency || account.balance; if (!node || node.is_root) continue;
const dr_or_cr = balance > 0 ? "Dr": "Cr";
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) { // show Dr if positive since balance is calculated as debit - credit else show Cr
node.parent && node.parent.find('.balance-area').remove(); const balance = account.balance_in_account_currency || account.balance;
$('<span class="balance-area pull-right">' const dr_or_cr = balance > 0 ? "Dr": "Cr";
+ (account.balance_in_account_currency ? const format = (value, currency) => format_currency(Math.abs(value), currency);
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ format(account.balance, account.company_currency) if (account.balance!==undefined) {
+ " " + dr_or_cr node.parent && node.parent.find('.balance-area').remove();
+ '</span>').insertBefore(node.$ul); $('<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);
}
}
});
} }
}); });
}, },

View File

@ -1,38 +1,38 @@
{ {
"country_code": "de", "country_code": "de",
"name": "SKR03 mit Kontonummern", "name": "SKR03 mit Kontonummern",
"tree": { "tree": {
"Aktiva": { "Aktiva": {
"is_group": 1, "is_group": 1,
"root_type": "Asset", "root_type": "Asset",
"A - Anlagevermögen": { "A - Anlagevermögen": {
"is_group": 1, "is_group": 1,
"EDV-Software": { "EDV-Software": {
"account_number": "0027", "account_number": "0027",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Gesch\u00e4ftsausstattung": { "Geschäftsausstattung": {
"account_number": "0410", "account_number": "0410",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"B\u00fcroeinrichtung": { "Büroeinrichtung": {
"account_number": "0420", "account_number": "0420",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Darlehen": { "Darlehen": {
"account_number": "0565" "account_number": "0565"
}, },
"Maschinen": { "Maschinen": {
"account_number": "0210", "account_number": "0210",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Betriebsausstattung": { "Betriebsausstattung": {
"account_number": "0400", "account_number": "0400",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Ladeneinrichtung": { "Ladeneinrichtung": {
"account_number": "0430", "account_number": "0430",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Accumulated Depreciation": { "Accumulated Depreciation": {
"account_type": "Accumulated Depreciation" "account_type": "Accumulated Depreciation"
@ -60,36 +60,46 @@
"Durchlaufende Posten": { "Durchlaufende Posten": {
"account_number": "1590" "account_number": "1590"
}, },
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { "Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": {
"account_number": "1371" "account_number": "1371"
}, },
"Abziehbare Vorsteuer": { "Abziehbare Vorsteuer": {
"account_type": "Tax",
"is_group": 1, "is_group": 1,
"Abziehbare Vorsteuer 7%": { "Abziehbare Vorsteuer 7 %": {
"account_number": "1571" "account_number": "1571",
"account_type": "Tax",
"tax_rate": 7.0
}, },
"Abziehbare Vorsteuer 19%": { "Abziehbare Vorsteuer 19 %": {
"account_number": "1576" "account_number": "1576",
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Abziehbare Vorsteuer nach \u00a713b UStG 19%": { "Abziehbare Vorsteuer nach § 13b UStG 19 %": {
"account_number": "1577" "account_number": "1577",
}, "account_type": "Tax",
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { "tax_rate": 19.0
"account_number": "3120"
} }
} }
}, },
"III. Wertpapiere": { "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.": { "IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": {
"is_group": 1, "is_group": 1,
"Kasse": { "Kasse": {
"account_type": "Cash",
"is_group": 1, "is_group": 1,
"account_type": "Cash",
"Kasse": { "Kasse": {
"is_group": 1,
"account_number": "1000", "account_number": "1000",
"account_type": "Cash" "account_type": "Cash"
} }
@ -111,21 +121,21 @@
"C - Rechnungsabgrenzungsposten": { "C - Rechnungsabgrenzungsposten": {
"is_group": 1, "is_group": 1,
"Aktive Rechnungsabgrenzung": { "Aktive Rechnungsabgrenzung": {
"account_number": "0980" "account_number": "0980"
} }
}, },
"D - Aktive latente Steuern": { "D - Aktive latente Steuern": {
"is_group": 1, "is_group": 1,
"Aktive latente Steuern": { "Aktive latente Steuern": {
"account_number": "0983" "account_number": "0983"
} }
}, },
"E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": { "E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": {
"is_group": 1 "is_group": 1
} }
}, },
"Passiva": { "Passiva": {
"is_group": 1, "is_group": 1,
"root_type": "Liability", "root_type": "Liability",
"A. Eigenkapital": { "A. Eigenkapital": {
"is_group": 1, "is_group": 1,
@ -200,26 +210,32 @@
}, },
"Umsatzsteuer": { "Umsatzsteuer": {
"is_group": 1, "is_group": 1,
"account_type": "Tax", "Umsatzsteuer 7 %": {
"Umsatzsteuer 7%": { "account_number": "1771",
"account_number": "1771" "account_type": "Tax",
"tax_rate": 7.0
}, },
"Umsatzsteuer 19%": { "Umsatzsteuer 19 %": {
"account_number": "1776" "account_number": "1776",
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Umsatzsteuer-Vorauszahlung": { "Umsatzsteuer-Vorauszahlung": {
"account_number": "1780" "account_number": "1780",
"account_type": "Tax"
}, },
"Umsatzsteuer-Vorauszahlung 1/11": { "Umsatzsteuer-Vorauszahlung 1/11": {
"account_number": "1781" "account_number": "1781"
}, },
"Umsatzsteuer \u00a7 13b UStG 19%": { "Umsatzsteuer nach § 13b UStG 19 %": {
"account_number": "1787" "account_number": "1787",
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Umsatzsteuer Vorjahr": { "Umsatzsteuer Vorjahr": {
"account_number": "1790" "account_number": "1790"
}, },
"Umsatzsteuer fr\u00fchere Jahre": { "Umsatzsteuer frühere Jahre": {
"account_number": "1791" "account_number": "1791"
} }
} }
@ -234,44 +250,56 @@
"E. Passive latente Steuern": { "E. Passive latente Steuern": {
"is_group": 1 "is_group": 1
} }
}, },
"Erl\u00f6se u. Ertr\u00e4ge 2/8": { "Erlöse u. Erträge 2/8": {
"is_group": 1, "is_group": 1,
"root_type": "Income", "root_type": "Income",
"Erl\u00f6skonten 8": { "Erlöskonten 8": {
"is_group": 1, "is_group": 1,
"Erl\u00f6se": { "Erlöse": {
"account_number": "8200", "account_number": "8200",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Erl\u00f6se USt. 19%": { "Erlöse USt. 19 %": {
"account_number": "8400", "account_number": "8400",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Erl\u00f6se USt. 7%": { "Erlöse USt. 7 %": {
"account_number": "8300", "account_number": "8300",
"account_type": "Income Account" "account_type": "Income Account"
} }
}, },
"Ertragskonten 2": { "Ertragskonten 2": {
"is_group": 1, "is_group": 1,
"sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": { "sonstige Zinsen und ähnliche Erträge": {
"account_number": "2650", "account_number": "2650",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Au\u00dferordentliche Ertr\u00e4ge": { "Außerordentliche Erträge": {
"account_number": "2500", "account_number": "2500",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Sonstige Ertr\u00e4ge": { "Sonstige Erträge": {
"account_number": "2700", "account_number": "2700",
"account_type": "Income Account" "account_type": "Income Account"
} }
} }
}, },
"Aufwendungen 2/4": { "Aufwendungen 2/4": {
"is_group": 1, "is_group": 1,
"root_type": "Expense", "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": { "Wareneingang": {
"account_number": "3200" "account_number": "3200"
}, },
@ -298,234 +326,234 @@
"Gegenkonto 4996-4998": { "Gegenkonto 4996-4998": {
"account_number": "4999" "account_number": "4999"
}, },
"Abschreibungen": { "Abschreibungen": {
"is_group": 1, "is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": { "Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830", "account_number": "4830",
"account_type": "Accumulated Depreciation" "account_type": "Accumulated Depreciation"
}, },
"Abschreibungen auf Gebäude": { "Abschreibungen auf Gebäude": {
"account_number": "4831", "account_number": "4831",
"account_type": "Depreciation" "account_type": "Depreciation"
}, },
"Abschreibungen auf Kfz": { "Abschreibungen auf Kfz": {
"account_number": "4832", "account_number": "4832",
"account_type": "Depreciation" "account_type": "Depreciation"
}, },
"Sofortabschreibung GWG": { "Sofortabschreibung GWG": {
"account_number": "4855", "account_number": "4855",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Kfz-Kosten": { "Kfz-Kosten": {
"is_group": 1, "is_group": 1,
"Kfz-Steuer": { "Kfz-Steuer": {
"account_number": "4510", "account_number": "4510",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Kfz-Versicherungen": { "Kfz-Versicherungen": {
"account_number": "4520", "account_number": "4520",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"laufende Kfz-Betriebskosten": { "laufende Kfz-Betriebskosten": {
"account_number": "4530", "account_number": "4530",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Kfz-Reparaturen": { "Kfz-Reparaturen": {
"account_number": "4540", "account_number": "4540",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Fremdfahrzeuge": { "Fremdfahrzeuge": {
"account_number": "4570", "account_number": "4570",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"sonstige Kfz-Kosten": { "sonstige Kfz-Kosten": {
"account_number": "4580", "account_number": "4580",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Personalkosten": { "Personalkosten": {
"is_group": 1, "is_group": 1,
"Geh\u00e4lter": { "Gehälter": {
"account_number": "4120", "account_number": "4120",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"gesetzliche soziale Aufwendungen": { "gesetzliche soziale Aufwendungen": {
"account_number": "4130", "account_number": "4130",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Aufwendungen f\u00fcr Altersvorsorge": { "Aufwendungen für Altersvorsorge": {
"account_number": "4165", "account_number": "4165",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Verm\u00f6genswirksame Leistungen": { "Vermögenswirksame Leistungen": {
"account_number": "4170", "account_number": "4170",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Aushilfsl\u00f6hne": { "Aushilfslöhne": {
"account_number": "4190", "account_number": "4190",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Raumkosten": { "Raumkosten": {
"is_group": 1, "is_group": 1,
"Miete und Nebenkosten": { "Miete und Nebenkosten": {
"account_number": "4210", "account_number": "4210",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": { "Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
"account_number": "4240", "account_number": "4240",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Reinigung": { "Reinigung": {
"account_number": "4250", "account_number": "4250",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Reparatur/Instandhaltung": { "Reparatur/Instandhaltung": {
"is_group": 1, "is_group": 1,
"Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": { "Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": {
"account_number": "4805", "account_number": "4805",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Versicherungsbeitr\u00e4ge": { "Versicherungsbeiträge": {
"is_group": 1, "is_group": 1,
"Versicherungen": { "Versicherungen": {
"account_number": "4360", "account_number": "4360",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Beitr\u00e4ge": { "Beiträge": {
"account_number": "4380", "account_number": "4380",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"sonstige Ausgaben": { "sonstige Ausgaben": {
"account_number": "4390", "account_number": "4390",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": { "steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": {
"account_number": "4396", "account_number": "4396",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Werbe-/Reisekosten": { "Werbe-/Reisekosten": {
"is_group": 1, "is_group": 1,
"Werbekosten": { "Werbekosten": {
"account_number": "4610", "account_number": "4610",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Aufmerksamkeiten": { "Aufmerksamkeiten": {
"account_number": "4653", "account_number": "4653",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": { "nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": {
"account_number": "4665", "account_number": "4665",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Reisekosten Unternehmer": { "Reisekosten Unternehmer": {
"account_number": "4670", "account_number": "4670",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"verschiedene Kosten": { "verschiedene Kosten": {
"is_group": 1, "is_group": 1,
"Porto": { "Porto": {
"account_number": "4910", "account_number": "4910",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Telekom": { "Telekom": {
"account_number": "4920", "account_number": "4920",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Mobilfunk D2": { "Mobilfunk D2": {
"account_number": "4921", "account_number": "4921",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Internet": { "Internet": {
"account_number": "4922", "account_number": "4922",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"B\u00fcrobedarf": { "Bürobedarf": {
"account_number": "4930", "account_number": "4930",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Zeitschriften, B\u00fccher": { "Zeitschriften, Bücher": {
"account_number": "4940", "account_number": "4940",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Fortbildungskosten": { "Fortbildungskosten": {
"account_number": "4945", "account_number": "4945",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Buchf\u00fchrungskosten": { "Buchführungskosten": {
"account_number": "4955", "account_number": "4955",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Abschlu\u00df- u. Pr\u00fcfungskosten": { "Abschluß- u. Prüfungskosten": {
"account_number": "4957", "account_number": "4957",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Nebenkosten des Geldverkehrs": { "Nebenkosten des Geldverkehrs": {
"account_number": "4970", "account_number": "4970",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Werkzeuge und Kleinger\u00e4te": { "Werkzeuge und Kleingeräte": {
"account_number": "4985", "account_number": "4985",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Zinsaufwendungen": { "Zinsaufwendungen": {
"is_group": 1, "is_group": 1,
"Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": { "Zinsaufwendungen für kurzfristige Verbindlichkeiten": {
"account_number": "2110", "account_number": "2110",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Zinsaufwendungen f\u00fcr KFZ Finanzierung": { "Zinsaufwendungen für KFZ Finanzierung": {
"account_number": "2121", "account_number": "2121",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
} }
}, },
"Anfangsbestand 9": { "Anfangsbestand 9": {
"is_group": 1, "is_group": 1,
"root_type": "Equity", "root_type": "Equity",
"Saldenvortragskonten": { "Saldenvortragskonten": {
"is_group": 1, "is_group": 1,
"Saldenvortrag Sachkonten": { "Saldenvortrag Sachkonten": {
"account_number": "9000" "account_number": "9000"
}, },
"Saldenvortr\u00e4ge Debitoren": { "Saldenvorträge Debitoren": {
"account_number": "9008" "account_number": "9008"
}, },
"Saldenvortr\u00e4ge Kreditoren": { "Saldenvorträge Kreditoren": {
"account_number": "9009" "account_number": "9009"
} }
} }
}, },
"Privatkonten 1": { "Privatkonten 1": {
"is_group": 1, "is_group": 1,
"root_type": "Equity", "root_type": "Equity",
"Privatentnahmen/-einlagen": { "Privatentnahmen/-einlagen": {
"is_group": 1, "is_group": 1,
"Privatentnahme allgemein": { "Privatentnahme allgemein": {
"account_number": "1800" "account_number": "1800"
}, },
"Privatsteuern": { "Privatsteuern": {
"account_number": "1810" "account_number": "1810"
}, },
"Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": { "Sonderausgaben beschränkt abzugsfähig": {
"account_number": "1820" "account_number": "1820"
}, },
"Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": { "Sonderausgaben unbeschränkt abzugsfähig": {
"account_number": "1830" "account_number": "1830"
}, },
"Au\u00dfergew\u00f6hnliche Belastungen": { "Außergewöhnliche Belastungen": {
"account_number": "1850" "account_number": "1850"
}, },
"Privateinlagen": { "Privateinlagen": {
"account_number": "1890" "account_number": "1890"
} }
} }
} }
} }
} }

View File

@ -56,7 +56,9 @@
"acc_frozen_upto", "acc_frozen_upto",
"column_break_25", "column_break_25",
"frozen_accounts_modifier", "frozen_accounts_modifier",
"report_settings_sb" "report_settings_sb",
"tab_break_dpet",
"show_balance_in_coa"
], ],
"fields": [ "fields": [
{ {
@ -347,6 +349,17 @@
"fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account " "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", "icon": "icon-cog",
@ -354,7 +367,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-11-27 21:49:52.538655", "modified": "2023-01-02 12:07:42.434214",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -28,9 +28,14 @@ class InvalidDateError(frappe.ValidationError):
class CostCenterAllocation(Document): class CostCenterAllocation(Document):
def __init__(self, *args, **kwargs):
super(CostCenterAllocation, self).__init__(*args, **kwargs)
self._skip_from_date_validation = False
def validate(self): def validate(self):
self.validate_total_allocation_percentage() 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_backdated_allocation()
self.validate_main_cost_center() self.validate_main_cost_center()
self.validate_child_cost_centers() self.validate_child_cost_centers()

View File

@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", { frappe.ui.form.on("Journal Entry", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("bank_account", "account", "account"); 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) { refresh: function(frm) {

View File

@ -81,6 +81,7 @@ class JournalEntry(AccountsController):
self.check_credit_limit() self.check_credit_limit()
self.make_gl_entries() self.make_gl_entries()
self.update_advance_paid() self.update_advance_paid()
self.update_asset_value()
self.update_inter_company_jv() self.update_inter_company_jv()
self.update_invoice_discounting() self.update_invoice_discounting()
@ -225,6 +226,34 @@ class JournalEntry(AccountsController):
for d in to_remove: for d in to_remove:
self.remove(d) 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): def update_inter_company_jv(self):
if ( if (
self.voucher_type == "Inter Company Journal Entry" self.voucher_type == "Inter Company Journal Entry"
@ -283,20 +312,48 @@ class JournalEntry(AccountsController):
d.db_update() d.db_update()
def unlink_asset_reference(self): def unlink_asset_reference(self):
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"): 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) 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 asset.calculate_depreciation:
if s.journal_entry == self.name: je_found = False
s.db_set("journal_entry", None)
row.value_after_depreciation += s.depreciation_amount for row in asset.get("finance_books"):
row.db_update() 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): def unlink_inter_company_jv(self):
if ( if (

View File

@ -69,6 +69,10 @@ class PaymentReconciliation(Document):
def get_jv_entries(self): def get_jv_entries(self):
condition = self.get_conditions() condition = self.get_conditions()
if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' "
dr_or_cr = ( dr_or_cr = (
"credit_in_account_currency" "credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable" if erpnext.get_party_account_type(self.party_type) == "Receivable"

View File

@ -747,6 +747,73 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 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): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):

View File

@ -51,7 +51,7 @@ class PaymentRequest(Document):
if existing_payment_request_amount: if existing_payment_request_amount:
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) 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) ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total) > ref_amount: if existing_payment_request_amount + flt(self.grand_total) > ref_amount:

View File

@ -1426,6 +1426,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "apply_tds",
"fieldname": "tax_withholding_net_total", "fieldname": "tax_withholding_net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
@ -1435,12 +1436,13 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "apply_tds",
"fieldname": "base_tax_withholding_net_total", "fieldname": "base_tax_withholding_net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"label": "Base Tax Withholding Net Total", "label": "Base Tax Withholding Net Total",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1554,7 +1556,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-12 18:37:38.142688", "modified": "2023-01-28 19:18:56.586321",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -1776,6 +1776,8 @@
"width": "50%" "width": "50%"
}, },
{ {
"fetch_from": "sales_partner.commission_rate",
"fetch_if_empty": 1,
"fieldname": "commission_rate", "fieldname": "commission_rate",
"fieldtype": "Float", "fieldtype": "Float",
"hide_days": 1, "hide_days": 1,
@ -2141,7 +2143,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-12-12 18:34:33.409895", "modified": "2023-01-28 19:45:47.538163",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -211,7 +211,13 @@ def set_address_details(
else: else:
party_details.update(get_company_address(company)) 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: if party_details.company_address:
party_details.update( party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address) get_fetch_values(doctype, "company_address", party_details.company_address)
@ -544,7 +550,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
elif term.due_date_based_on == "Day(s) after the end of the invoice month": 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)) due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days))
else: 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 return due_date

View File

@ -526,7 +526,7 @@ def get_columns(filters):
"options": "GL Entry", "options": "GL Entry",
"hidden": 1, "hidden": 1,
}, },
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{ {
"label": _("Account"), "label": _("Account"),
"fieldname": "account", "fieldname": "account",
@ -538,13 +538,13 @@ def get_columns(filters):
"label": _("Debit ({0})").format(currency), "label": _("Debit ({0})").format(currency),
"fieldname": "debit", "fieldname": "debit",
"fieldtype": "Float", "fieldtype": "Float",
"width": 100, "width": 130,
}, },
{ {
"label": _("Credit ({0})").format(currency), "label": _("Credit ({0})").format(currency),
"fieldname": "credit", "fieldname": "credit",
"fieldtype": "Float", "fieldtype": "Float",
"width": 100, "width": 130,
}, },
{ {
"label": _("Balance ({0})").format(currency), "label": _("Balance ({0})").format(currency),

View File

@ -50,6 +50,20 @@ frappe.query_reports["Gross Profit"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Sales Person" "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, "tree": true,
"name_field": "parent", "name_field": "parent",

View File

@ -655,10 +655,35 @@ class GrossProfitGenerator(object):
return self.calculate_buying_amount_from_sle( return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code 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: else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code) 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): def get_average_buying_rate(self, row, item_code):
args = row args = row
@ -750,6 +775,13 @@ class GrossProfitGenerator(object):
if self.filters.get("item_code"): if self.filters.get("item_code"):
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" 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( self.si_list = frappe.db.sql(
""" """
select select
@ -760,7 +792,8 @@ class GrossProfitGenerator(object):
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, `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`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return, `tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,

View File

@ -302,3 +302,82 @@ class TestGrossProfit(FrappeTestCase):
columns, data = execute(filters=filters) columns, data = execute(filters=filters)
self.assertGreater(len(data), 0) 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])

View File

@ -221,34 +221,45 @@ frappe.ui.form.on('Asset', {
asset_values.push(flt(frm.doc.gross_purchase_amount) - asset_values.push(flt(frm.doc.gross_purchase_amount) -
flt(frm.doc.opening_accumulated_depreciation)); 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 = []; $.each(depr_schedule || [], function(i, v) {
x_intervals.push(v.schedule_date);
if (frm.doc.finance_books.length == 1) { var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount);
depr_schedule = (await frappe.call( if(v.journal_entry) {
"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", last_depreciation_date = v.schedule_date;
{ asset_values.push(asset_value);
asset_name: frm.doc.name, } else {
status: frm.doc.docstatus ? "Active" : "Draft", if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
finance_book: frm.doc.finance_books[0].finance_book || null asset_values.push(null);
} } else {
)).message; 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)) { if(in_list(["Scrapped", "Sold"], frm.doc.status)) {
x_intervals.push(frm.doc.disposal_date); x_intervals.push(frm.doc.disposal_date);

View File

@ -509,9 +509,15 @@
"group": "Depreciation", "group": "Depreciation",
"link_doctype": "Asset Depreciation Schedule", "link_doctype": "Asset Depreciation Schedule",
"link_fieldname": "asset" "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", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -36,7 +36,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_depr_schedule, get_depr_schedule,
make_draft_asset_depr_schedules, make_draft_asset_depr_schedules,
make_draft_asset_depr_schedules_if_not_present, make_draft_asset_depr_schedules_if_not_present,
set_draft_asset_depr_schedule_details,
update_draft_asset_depr_schedules, update_draft_asset_depr_schedules,
) )
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@ -240,17 +239,6 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") 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 # if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row): def check_is_pro_rata(self, row):
has_pro_rata = False has_pro_rata = False
@ -392,18 +380,23 @@ class Asset(AccountsController):
movement.cancel() movement.cancel()
def delete_depreciation_entries(self): def delete_depreciation_entries(self):
for row in self.get("finance_books"): if self.calculate_depreciation:
depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book) for row in self.get("finance_books"):
depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book)
for d in depr_schedule or []: for d in depr_schedule or []:
if d.journal_entry: if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel() frappe.get_doc("Journal Entry", d.journal_entry).cancel()
d.db_set("journal_entry", None) else:
depr_entries = self.get_manual_depreciation_entries()
self.db_set( for depr_entry in depr_entries or []:
"value_after_depreciation", frappe.get_doc("Journal Entry", depr_entry.name).cancel()
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
) self.db_set(
"value_after_depreciation",
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
)
def set_status(self, status=None): def set_status(self, status=None):
"""Get and update status""" """Get and update status"""
@ -434,6 +427,28 @@ class Asset(AccountsController):
status = "Cancelled" status = "Cancelled"
return status 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): def get_default_finance_book_idx(self):
if not self.get("default_finance_book") and self.company: if not self.get("default_finance_book") and self.company:
self.default_finance_book = erpnext.get_default_finance_book(self.company) self.default_finance_book = erpnext.get_default_finance_book(self.company)
@ -443,6 +458,43 @@ class Asset(AccountsController):
if d.finance_book == self.default_finance_book: if d.finance_book == self.default_finance_book:
return cint(d.idx) - 1 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): def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
if not purchase_document: if not purchase_document:
@ -603,7 +655,6 @@ def update_maintenance_status():
def make_post_gl_entry(): def make_post_gl_entry():
asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"]) asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"])
for asset_category in asset_categories: for asset_category in asset_categories:
@ -756,7 +807,7 @@ def make_journal_entry(asset_name):
depreciation_expense_account, depreciation_expense_account,
) = get_depreciation_accounts(asset) ) = 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"] "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
) )
depreciation_cost_center = asset.cost_center or depreciation_cost_center depreciation_cost_center = asset.cost_center or depreciation_cost_center
@ -821,6 +872,13 @@ def is_cwip_accounting_enabled(asset_category):
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) 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): def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1) period_start_date = add_months(date, cint(frequency) * -1)
@ -886,7 +944,7 @@ def update_existing_asset(asset, remaining_qty, new_asset_name):
) )
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) 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 accumulated_depreciation = 0
@ -938,7 +996,7 @@ def create_new_asset_after_split(asset, split_qty):
) )
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) 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 accumulated_depreciation = 0

View File

@ -4,7 +4,17 @@
import frappe import frappe
from frappe import _ 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 frappe.utils.user import get_users_with_role
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@ -400,6 +410,9 @@ def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_
row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation) 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: if orginal_schedule_date == posting_date_of_disposal:
return True return True
@ -520,18 +533,8 @@ def get_asset_details(asset, finance_book=None):
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center depreciation_cost_center = asset.cost_center or depreciation_cost_center
idx = 1 value_after_depreciation = asset.get_value_after_depreciation(finance_book)
if finance_book:
for d in asset.finance_books:
if d.finance_book == finance_book:
idx = d.idx
break
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) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
return ( return (

View File

@ -16,6 +16,7 @@ from frappe.utils import (
nowdate, 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.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import ( from erpnext.assets.doctype.asset.asset import (
make_sales_invoice, make_sales_invoice,
@ -28,7 +29,6 @@ from erpnext.assets.doctype.asset.depreciation import (
scrap_asset, scrap_asset,
) )
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
clear_depr_schedule,
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depr_schedule, get_depr_schedule,
) )
@ -924,11 +924,6 @@ class TestDepreciationBasics(AssetSetup):
def test_get_depreciation_amount(self): def test_get_depreciation_amount(self):
"""Tests if get_depreciation_amount() returns the right value.""" """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 = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31")
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
@ -943,7 +938,7 @@ class TestDepreciationBasics(AssetSetup):
}, },
) )
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) self.assertEqual(depreciation_amount, 30000)
def test_make_depr_schedule(self): def test_make_depr_schedule(self):
@ -1259,7 +1254,7 @@ class TestDepreciationBasics(AssetSetup):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active") 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) self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1)
@ -1308,19 +1303,19 @@ class TestDepreciationBasics(AssetSetup):
asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc( asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 1" 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) self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3)
asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc( asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 2" 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) self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3)
asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc( asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 3" 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) self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0)
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
@ -1503,6 +1498,36 @@ class TestDepreciationBasics(AssetSetup):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date)) 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(): def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"): if not frappe.db.exists("Asset Category", "Computers"):

View File

@ -10,6 +10,7 @@ from frappe import _
from frappe.utils import cint, flt, get_link_to_form from frappe.utils import cint, flt, get_link_to_form
import erpnext import erpnext
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import ( from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset, depreciate_asset,
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
@ -21,9 +22,6 @@ from erpnext.assets.doctype.asset_category.asset_category import get_asset_categ
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
make_new_active_asset_depr_schedules_and_cancel_current_ones, 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.controllers.stock_controller import StockController
from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@ -261,7 +259,9 @@ class AssetCapitalization(StockController):
for d in self.get("asset_items"): for d in self.get("asset_items"):
if d.asset: if d.asset:
finance_book = d.get("finance_book") or self.get("finance_book") 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_value = get_value_after_depreciation_on_disposal_date(
d.asset, self.posting_date, finance_book=finance_book d.asset, self.posting_date, finance_book=finance_book
) )
@ -713,7 +713,7 @@ def get_consumed_asset_details(args):
if args.asset: if args.asset:
out.current_asset_value = flt( 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( out.asset_value = get_value_after_depreciation_on_disposal_date(
args.asset, args.posting_date, finance_book=args.finance_book args.asset, args.posting_date, finance_book=args.finance_book

View File

@ -4,17 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import ( from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month
add_days,
add_months,
cint,
date_diff,
flt,
get_last_day,
is_last_day_of_the_month,
)
import erpnext
class AssetDepreciationSchedule(Document): class AssetDepreciationSchedule(Document):
@ -83,7 +73,256 @@ class AssetDepreciationSchedule(Document):
) )
asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name) 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): def make_draft_asset_depr_schedules_if_not_present(asset_doc):
@ -108,7 +347,7 @@ def make_draft_asset_depr_schedules(asset_doc):
def make_draft_asset_depr_schedule(asset_doc, row): def make_draft_asset_depr_schedule(asset_doc, row):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") 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() asset_depr_schedule_doc.insert()
@ -120,41 +359,11 @@ def update_draft_asset_depr_schedules(asset_doc):
if not asset_depr_schedule_doc: if not asset_depr_schedule_doc:
continue 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() 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): def convert_draft_asset_depr_schedules_into_active(asset_doc):
for row in asset_doc.get("finance_books"): for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book)
@ -192,8 +401,8 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) 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) new_asset_depr_schedule_doc.make_depr_schedule(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.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
new_asset_depr_schedule_doc.notes = notes new_asset_depr_schedule_doc.notes = notes
@ -208,8 +417,7 @@ def get_temp_asset_depr_schedule_doc(
): ):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
prepare_draft_asset_depr_schedule_data( asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
asset_depr_schedule_doc,
asset_doc, asset_doc,
row, row,
date_of_disposal, date_of_disposal,
@ -220,21 +428,6 @@ def get_temp_asset_depr_schedule_doc(
return asset_depr_schedule_doc 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() @frappe.whitelist()
def get_depr_schedule(asset_name, status, finance_book=None): def get_depr_schedule(asset_name, status, finance_book=None):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book) asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book)
@ -256,261 +449,16 @@ def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
return asset_depr_schedule_doc return asset_depr_schedule_doc
def make_depr_schedule( def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True finance_book_filter = ["finance_book", "is", "not set"]
): if finance_book:
if row.depreciation_method != "Manual" and not asset_depr_schedule_doc.get( finance_book_filter = ["finance_book", "=", finance_book]
"depreciation_schedule"
):
asset_depr_schedule_doc.depreciation_schedule = []
if not asset_doc.available_for_use_date: return frappe.db.get_value(
return doctype="Asset Depreciation Schedule",
filters=[
start = clear_depr_schedule(asset_depr_schedule_doc) ["asset", "=", asset_name],
finance_book_filter,
_make_depr_schedule( ["status", "=", status],
asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row ],
) )
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")
)

View File

@ -6,7 +6,10 @@ import unittest
import frappe import frappe
from frappe.utils import flt, nowdate 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 ( from erpnext.assets.doctype.asset.test_asset import (
create_asset, create_asset,
create_asset_data, create_asset_data,
@ -109,20 +112,20 @@ class TestAssetRepair(unittest.TestCase):
def test_increase_in_asset_value_due_to_stock_consumption(self): def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation=1, submit=1) 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_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1)
asset.reload() 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) 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): def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
asset = create_asset(calculate_depreciation=1, submit=1) 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_repair = create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1)
asset.reload() 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) self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
def test_purchase_invoice(self): def test_purchase_invoice(self):
@ -256,10 +259,6 @@ class TestAssetRepair(unittest.TestCase):
) )
def get_asset_value(asset):
return asset.finance_books[0].value_after_depreciation
def num_of_depreciations(asset): def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations return asset.finance_books[0].total_number_of_depreciations

View File

@ -47,7 +47,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
set_current_asset_value: function(frm) { set_current_asset_value: function(frm) {
if (frm.doc.asset) { if (frm.doc.asset) {
frm.call({ 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: { args: {
asset: frm.doc.asset, asset: frm.doc.asset,
finance_book: frm.doc.finance_book finance_book: frm.doc.finance_book

View File

@ -10,11 +10,10 @@ from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, 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 import get_depreciation_accounts
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depreciation_amount,
set_accumulated_depreciation,
) )
@ -46,7 +45,7 @@ class AssetValueAdjustment(Document):
def set_current_asset_value(self): def set_current_asset_value(self):
if not self.current_asset_value and self.asset: 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): def make_depreciation_entry(self):
asset = frappe.get_doc("Asset", self.asset) asset = frappe.get_doc("Asset", self.asset)
@ -163,7 +162,7 @@ class AssetValueAdjustment(Document):
depreciation_amount = days * rate_per_day depreciation_amount = days * rate_per_day
from_date = data.schedule_date from_date = data.schedule_date
else: else:
depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d)
if depreciation_amount: if depreciation_amount:
value_after_depreciation -= flt(depreciation_amount) value_after_depreciation -= flt(depreciation_amount)
@ -171,18 +170,9 @@ class AssetValueAdjustment(Document):
d.db_update() 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: for asset_data in depr_schedule:
if not asset_data.journal_entry: if not asset_data.journal_entry:
asset_data.db_update() asset_data.db_update()
new_asset_depr_schedule_doc.submit() 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")

View File

@ -6,13 +6,11 @@ import unittest
import frappe import frappe
from frappe.utils import add_days, get_last_day, nowdate 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.test_asset import create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc, 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 from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@ -46,7 +44,7 @@ class TestAssetValueAdjustment(unittest.TestCase):
) )
asset_doc.submit() 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) self.assertEqual(current_value, 100000.0)
def test_asset_depreciation_value_adjustment(self): def test_asset_depreciation_value_adjustment(self):
@ -79,7 +77,7 @@ class TestAssetValueAdjustment(unittest.TestCase):
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "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( adj_doc = make_asset_value_adjustment(
asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
) )

View File

@ -4,13 +4,16 @@
import frappe import frappe
from frappe import _ 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 ( from erpnext.accounts.report.financial_statements import (
get_fiscal_year_data, get_fiscal_year_data,
get_period_list, get_period_list,
validate_fiscal_year, 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): def execute(filters=None):
@ -85,6 +88,7 @@ def get_data(filters):
"asset_name", "asset_name",
"status", "status",
"department", "department",
"company",
"cost_center", "cost_center",
"calculate_depreciation", "calculate_depreciation",
"purchase_receipt", "purchase_receipt",
@ -98,8 +102,25 @@ def get_data(filters):
] ]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) 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: 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 = { row = {
"asset_id": asset.asset_id, "asset_id": asset.asset_id,
"asset_name": asset.asset_name, "asset_name": asset.asset_name,
@ -110,7 +131,7 @@ def get_data(filters):
or pi_supplier_map.get(asset.purchase_invoice), or pi_supplier_map.get(asset.purchase_invoice),
"gross_purchase_amount": asset.gross_purchase_amount, "gross_purchase_amount": asset.gross_purchase_amount,
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation, "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, "available_for_use_date": asset.available_for_use_date,
"location": asset.location, "location": asset.location,
"asset_category": asset.asset_category, "asset_category": asset.asset_category,
@ -122,21 +143,6 @@ def get_data(filters):
return data 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): def prepare_chart_data(data, filters):
labels_values_map = {} labels_values_map = {}
date_field = frappe.scrub(filters.date_based_on) date_field = frappe.scrub(filters.date_based_on)
@ -182,6 +188,15 @@ def prepare_chart_data(data, filters):
} }
def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
if asset.calculate_depreciation:
depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
else:
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
return depr_amount
def get_finance_book_value_map(filters): def get_finance_book_value_map(filters):
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
@ -203,6 +218,31 @@ def get_finance_book_value_map(filters):
) )
def get_manual_depreciation_amount_of_asset(asset, filters):
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
(_, _, depreciation_expense_account) = get_depreciation_accounts(asset)
gle = frappe.qb.DocType("GL Entry")
result = (
frappe.qb.from_(gle)
.select(Sum(gle.debit))
.where(gle.against_voucher == asset.asset_id)
.where(gle.account == depreciation_expense_account)
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.where(gle.posting_date <= date)
).run()
if result and result[0] and result[0][0]:
depr_amount = result[0][0]
else:
depr_amount = 0
return depr_amount
def get_purchase_receipt_supplier_map(): def get_purchase_receipt_supplier_map():
return frappe._dict( return frappe._dict(
frappe.db.sql( frappe.db.sql(

View File

@ -15,17 +15,6 @@ class TestBulkTransactionLog(unittest.TestCase):
create_customer() create_customer()
create_item() 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): def test_entry_in_log(self):
so_name = create_so() so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")

View File

@ -1221,6 +1221,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "apply_tds",
"fieldname": "tax_withholding_net_total", "fieldname": "tax_withholding_net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
@ -1230,12 +1231,13 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "apply_tds",
"fieldname": "base_tax_withholding_net_total", "fieldname": "base_tax_withholding_net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"label": "Base Tax Withholding Net Total", "label": "Base Tax Withholding Net Total",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1269,7 +1271,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-25 18:08:59.074182", "modified": "2023-01-28 18:59:16.322824",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -10,6 +10,7 @@ from frappe.utils import add_days, flt, getdate, nowdate
from frappe.utils.data import today from frappe.utils.data import today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry 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_inter_company_sales_order
from erpnext.buying.doctype.purchase_order.purchase_order import ( from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as make_pi_from_po, make_purchase_invoice as make_pi_from_po,
@ -685,6 +686,12 @@ class TestPurchaseOrder(FrappeTestCase):
else: else:
raise Exception 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): def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
po = create_purchase_order(do_not_save=1) po = create_purchase_order(do_not_save=1)
po.payment_terms_template = "_Test Payment Term Template" po.payment_terms_template = "_Test Payment Term Template"

View File

@ -29,6 +29,7 @@
"message_for_supplier", "message_for_supplier",
"terms_section_break", "terms_section_break",
"incoterm", "incoterm",
"named_place",
"tc_name", "tc_name",
"terms", "terms",
"printing_settings", "printing_settings",
@ -278,13 +279,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:26:33.770993", "modified": "2023-01-31 23:22:06.684694",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",

View File

@ -15,60 +15,4 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestProcurementTracker(FrappeTestCase): class TestProcurementTracker(FrappeTestCase):
def test_result_for_procurement_tracker(self): pass
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

View File

@ -712,6 +712,8 @@ class BuyingController(SubcontractingController):
asset.purchase_date = self.posting_date asset.purchase_date = self.posting_date
asset.supplier = self.supplier asset.supplier = self.supplier
elif self.docstatus == 2: elif self.docstatus == 2:
if asset.docstatus == 2:
continue
if asset.docstatus == 0: if asset.docstatus == 0:
asset.set(field, None) asset.set(field, None)
asset.supplier = None asset.supplier = None

View File

@ -305,7 +305,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] 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 # 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, doctype,
fields=fields, fields=fields,
filters=[ filters=[

View File

@ -312,7 +312,8 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Title", "label": "Title",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"fieldname": "language", "fieldname": "language",
@ -514,11 +515,10 @@
"idx": 5, "idx": 5,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2022-10-13 12:42:04.277879", "modified": "2023-01-24 18:20:05.044791",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead", "name": "Lead",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -282,6 +282,7 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
"contact_no": "phone_1", "contact_no": "phone_1",
"fax": "fax_1", "fax": "fax_1",
}, },
"field_no_map": ["disabled"],
} }
}, },
target_doc, target_doc,
@ -390,7 +391,7 @@ def get_lead_details(lead, posting_date=None, company=None):
{ {
"territory": lead.territory, "territory": lead.territory,
"customer_name": lead.company_name or lead.lead_name, "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_email": lead.email_id,
"contact_mobile": lead.mobile_no, "contact_mobile": lead.mobile_no,
"contact_phone": lead.phone, "contact_phone": lead.phone,

View File

@ -325,3 +325,5 @@ erpnext.patches.v14_0.setup_clear_repost_logs
erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
erpnext.patches.v14_0.update_entry_type_for_journal_entry erpnext.patches.v14_0.update_entry_type_for_journal_entry
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers 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

View File

@ -18,9 +18,11 @@ def create_new_cost_center_allocation_records(cc_allocations):
cca = frappe.new_doc("Cost Center Allocation") cca = frappe.new_doc("Cost Center Allocation")
cca.main_cost_center = main_cc cca.main_cost_center = main_cc
cca.valid_from = today() cca.valid_from = today()
cca._skip_from_date_validation = True
for child_cc, percentage in allocations.items(): for child_cc, percentage in allocations.items():
cca.append("allocation_percentages", ({"cost_center": child_cc, "percentage": percentage})) cca.append("allocation_percentages", ({"cost_center": child_cc, "percentage": percentage}))
cca.save() cca.save()
cca.submit() cca.submit()

View File

@ -2,7 +2,8 @@ import frappe
from frappe import qb from frappe import qb
from frappe.query_builder import Case, CustomFunction from frappe.query_builder import Case, CustomFunction
from frappe.query_builder.custom import ConstantColumn 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 ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_dimensions, get_dimensions,
@ -17,9 +18,9 @@ def create_accounting_dimension_fields():
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
def generate_name_for_payment_ledger_entries(gl_entries): def generate_name_for_payment_ledger_entries(gl_entries, start):
for index, entry in enumerate(gl_entries, 1): for index, entry in enumerate(gl_entries, 0):
entry.name = index entry.name = start + index
def get_columns(): def get_columns():
@ -81,6 +82,14 @@ def insert_chunk_into_payment_ledger(insert_query, gl_entries):
def execute(): 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"): if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
# create accounting dimension fields in Payment Ledger # create accounting dimension fields in Payment Ledger
create_accounting_dimension_fields() create_accounting_dimension_fields()
@ -89,52 +98,90 @@ def execute():
account = qb.DocType("Account") account = qb.DocType("Account")
ifelse = CustomFunction("IF", ["condition", "then", "else"]) ifelse = CustomFunction("IF", ["condition", "then", "else"])
gl_entries = ( # Get Records Count
qb.from_(gl) accounts = (
.inner_join(account) qb.from_(account)
.on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) .select(account.name)
.select( .where((account.account_type == "Receivable") | (account.account_type == "Payable"))
gl.star, .orderby(account.name)
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)
) )
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 if un_processed:
generate_name_for_payment_ledger_entries(gl_entries) print(f"Migrating {un_processed} GL Entries to Payment Ledger")
# split data into chunks processed = 0
chunk_size = 1000 last_update_percent = 0
try: batch_size = 5000
for i in range(0, len(gl_entries), chunk_size): last_name = None
insert_query = build_insert_query()
insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size]) while True:
frappe.db.commit() if last_name:
except Exception as err: where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0)
frappe.db.rollback() else:
ple = qb.DocType("Payment Ledger Entry") where_clause = gl.is_cancelled == 0
qb.from_(ple).delete().where(ple.docstatus >= 0).run()
frappe.db.commit() gl_entries = (
raise err 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")

View File

@ -1,81 +1,98 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.utils import create_batch from frappe.query_builder import CustomFunction
from frappe.query_builder.functions import Count, IfNull
from frappe.utils import flt
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
def execute(): def execute():
"""
Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry'
"""
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
gle = qb.DocType("GL Entry") gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
# get ple and their remarks from GL Entry # Get empty PLE records
pl_entries = ( un_processed = (
qb.from_(ple) qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run()
.left_join(gle) )[0][0]
.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)
)
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: ifelse = CustomFunction("IF", ["condition", "then", "else"])
# split into multiple batches, update and commit for each batch
processed = 0
last_percent_update = 0
batch_size = 1000 batch_size = 1000
for batch in create_batch(pl_entries, batch_size): last_name = None
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()
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")

View File

@ -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()

View File

@ -1,9 +1,5 @@
import frappe import frappe
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
set_draft_asset_depr_schedule_details,
)
def execute(): def execute():
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule") frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
@ -16,7 +12,7 @@ def execute():
for fb_row in finance_book_rows: for fb_row in finance_book_rows:
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") 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() asset_depr_schedule_doc.insert()

View File

@ -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()

View File

@ -10,62 +10,6 @@ from frappe.website.serve import get_response
class TestHomepageSection(unittest.TestCase): 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): def test_homepage_section_custom_html(self):
frappe.get_doc( frappe.get_doc(
{ {

View File

@ -1691,6 +1691,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var me = this; var me = this;
var valid = true; var valid = true;
if (frappe.flags.ignore_company_party_validation) {
return valid;
}
$.each(["company", "customer"], function(i, fieldname) { $.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(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) {
if (!me.frm.doc[fieldname]) { if (!me.frm.doc[fieldname]) {

View File

@ -13,19 +13,11 @@ frappe.setup.on("before_load", function () {
erpnext.setup.slides_settings = [ erpnext.setup.slides_settings = [
{ {
// Brand // Organization
name: 'brand', name: 'organization',
icon: "fa fa-bookmark", title: __("Setup your organization"),
title: __("The Brand"), icon: "fa fa-building",
// help: __('Upload your letter head and logo. (you can edit them later).'),
fields: [ fields: [
{
fieldtype: "Attach Image", fieldname: "attach_logo",
label: __("Attach Logo"),
description: __("100px by 100px"),
is_private: 0,
align: 'center'
},
{ {
fieldname: 'company_name', fieldname: 'company_name',
label: __('Company Name'), label: __('Company Name'),
@ -35,54 +27,9 @@ erpnext.setup.slides_settings = [
{ {
fieldname: 'company_abbr', fieldname: 'company_abbr',
label: __('Company Abbreviation'), 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', fieldtype: 'Data',
placeholder: __('e.g. "Build tools for builders"'), hidden: 1
reqd: 1
}, },
{ fieldname: 'bank_account', label: __('Bank Name'), fieldtype: 'Data', reqd: 1 },
{ {
fieldname: 'chart_of_accounts', label: __('Chart of Accounts'), fieldname: 'chart_of_accounts', label: __('Chart of Accounts'),
options: "", fieldtype: 'Select' options: "", fieldtype: 'Select'
@ -94,40 +41,24 @@ erpnext.setup.slides_settings = [
], ],
onload: function (slide) { onload: function (slide) {
this.load_chart_of_accounts(slide);
this.bind_events(slide); this.bind_events(slide);
this.load_chart_of_accounts(slide);
this.set_fy_dates(slide); this.set_fy_dates(slide);
}, },
validate: function () { validate: function () {
let me = this;
let exist;
if (!this.validate_fy_dates()) { if (!this.validate_fy_dates()) {
return false; return false;
} }
// Validate bank name if ((this.values.company_name || "").toLowerCase() == "company") {
if(me.values.bank_account) { frappe.msgprint(__("Company Name cannot be Company"));
frappe.call({ return false;
async: false, }
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", if (!this.values.company_abbr) {
args: { return false;
"coa": me.values.chart_of_accounts, }
"bank_account": me.values.bank_account if (this.values.company_abbr.length > 10) {
}, return false;
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
} }
return true; return true;
@ -151,15 +82,15 @@ erpnext.setup.slides_settings = [
var country = frappe.wizard.values.country; var country = frappe.wizard.values.country;
if (country) { if (country) {
var fy = erpnext.setup.fiscal_years[country]; let fy = erpnext.setup.fiscal_years[country];
var current_year = moment(new Date()).year(); let current_year = moment(new Date()).year();
var next_year = current_year + 1; let next_year = current_year + 1;
if (!fy) { if (!fy) {
fy = ["01-01", "12-31"]; fy = ["01-01", "12-31"];
next_year = current_year; 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()) { if (year_start_date > frappe.datetime.get_today()) {
next_year = current_year; next_year = current_year;
current_year -= 1; current_year -= 1;
@ -171,7 +102,7 @@ erpnext.setup.slides_settings = [
load_chart_of_accounts: function (slide) { load_chart_of_accounts: function (slide) {
var country = frappe.wizard.values.country; let country = frappe.wizard.values.country;
if (country) { if (country) {
frappe.call({ frappe.call({
@ -202,12 +133,25 @@ erpnext.setup.slides_settings = [
me.charts_modal(slide, chart_template); 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) { charts_modal: function(slide, chart_template) {
let parent = __('All Accounts'); let parent = __('All Accounts');
var dialog = new frappe.ui.Dialog({ let dialog = new frappe.ui.Dialog({
title: chart_template, title: chart_template,
fields: [ fields: [
{'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button', {'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button',

View File

@ -491,7 +491,20 @@ erpnext.utils.update_child_items = function(opts) {
const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`); const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`);
const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision; 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 = [{ const fields = [{
fieldtype:'Data', fieldtype:'Data',
fieldname:"docname", fieldname:"docname",
@ -588,7 +601,7 @@ erpnext.utils.update_child_items = function(opts) {
}) })
} }
const dialog = new frappe.ui.Dialog({ new frappe.ui.Dialog({
title: __("Update Items"), title: __("Update Items"),
fields: [ fields: [
{ {
@ -624,24 +637,7 @@ erpnext.utils.update_child_items = function(opts) {
refresh_field("items"); refresh_field("items");
}, },
primary_action_label: __('Update') primary_action_label: __('Update')
}); }).show();
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();
} }
erpnext.utils.map_current_doc = function(opts) { erpnext.utils.map_current_doc = function(opts) {

View File

@ -17,45 +17,79 @@ from erpnext.stock.utils import scan_barcode
def search_by_term(search_term, warehouse, price_list): def search_by_term(search_term, warehouse, price_list):
result = search_for_serial_or_batch_or_barcode_number(search_term) or {} result = search_for_serial_or_batch_or_barcode_number(search_term) or {}
item_code = result.get("item_code") or search_term item_code = result.get("item_code", search_term)
serial_no = result.get("serial_no") or "" serial_no = result.get("serial_no", "")
batch_no = result.get("batch_no") or "" batch_no = result.get("batch_no", "")
barcode = result.get("barcode") or "" barcode = result.get("barcode", "")
if result: if not result:
item_info = frappe.db.get_value( return
"Item",
item_code,
[
"name as item_code",
"item_name",
"description",
"stock_uom",
"image as item_image",
"is_stock_item",
],
as_dict=1,
)
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) item_doc = frappe.get_doc("Item", item_code)
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_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, "currency": p.get("currency"),
"batch_no": batch_no, "price_list_rate": p.get("price_list_rate"),
"barcode": barcode,
"price_list_rate": price_list_rate,
"currency": currency,
"actual_qty": item_stock_qty,
} }
) )
return {"items": [item_info]} return {"items": [item]}
@frappe.whitelist() @frappe.whitelist()
@ -121,33 +155,43 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
as_dict=1, as_dict=1,
) )
if items_data: # return (empty) list if there are no results
items = [d.item_code for d in items_data] if not items_data:
item_prices_data = frappe.get_all( 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", "Item Price",
fields=["item_code", "price_list_rate", "currency"], fields=["price_list_rate", "currency", "uom"],
filters={"price_list": price_list, "item_code": ["in", items]}, filters={
"price_list": price_list,
"item_code": item.item_code,
"selling": True,
},
) )
item_prices = {} if not item_price:
for d in item_prices_data: result.append(item)
item_prices[d.item_code] = d
for item in items_data: for price in item_price:
item_code = item.item_code uom = next(filter(lambda x: x.uom == price.uom, uoms), {})
item_price = item_prices.get(item_code) or {}
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
row = {} if price.uom != item.stock_uom and uom and uom.conversion_factor:
row.update(item) item.actual_qty = item.actual_qty // uom.conversion_factor
row.update(
result.append(
{ {
"price_list_rate": item_price.get("price_list_rate"), **item,
"currency": item_price.get("currency"), "price_list_rate": price.get("price_list_rate"),
"actual_qty": item_stock_qty, "currency": price.get("currency"),
"uom": price.uom or item.uom,
} }
) )
result.append(row)
return {"items": result} return {"items": result}

View File

@ -542,12 +542,12 @@ erpnext.PointOfSale.Controller = class {
if (!this.frm.doc.customer) if (!this.frm.doc.customer)
return this.raise_customer_selection_alert(); 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) if (!item_code)
return; 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) { if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
@ -649,6 +649,7 @@ erpnext.PointOfSale.Controller = class {
const is_stock_item = resp[1]; const is_stock_item = resp[1];
frappe.dom.unfreeze(); frappe.dom.unfreeze();
const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold(); const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold(); const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold() const bold_available_qty = available_qty.toString().bold()
@ -664,7 +665,7 @@ erpnext.PointOfSale.Controller = class {
} }
} else if (is_stock_item && available_qty < qty_needed) { } else if (is_stock_item && available_qty < qty_needed) {
frappe.throw({ 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' indicator: 'orange'
}); });
frappe.utils.play_sound("error"); frappe.utils.play_sound("error");

View File

@ -609,7 +609,7 @@ erpnext.PointOfSale.ItemCart = class {
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return ` return `
<div class="item-qty-rate"> <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-amount">
<div class="item-rate">${format_currency(item_data.amount, currency)}</div> <div class="item-rate">${format_currency(item_data.amount, currency)}</div>
<div class="item-amount">${format_currency(item_data.rate, currency)}</div> <div class="item-amount">${format_currency(item_data.rate, currency)}</div>
@ -618,7 +618,7 @@ erpnext.PointOfSale.ItemCart = class {
} else { } else {
return ` return `
<div class="item-qty-rate"> <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-amount">
<div class="item-rate">${format_currency(item_data.rate, currency)}</div> <div class="item-rate">${format_currency(item_data.rate, currency)}</div>
</div> </div>

View File

@ -78,7 +78,7 @@ erpnext.PointOfSale.ItemSelector = class {
get_item_html(item) { get_item_html(item) {
const me = this; const me = this;
// eslint-disable-next-line no-unused-vars // 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; const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
let indicator_color; let indicator_color;
let qty_to_display = actual_qty; let qty_to_display = actual_qty;
@ -118,7 +118,7 @@ erpnext.PointOfSale.ItemSelector = class {
return ( return (
`<div class="item-wrapper" `<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}" 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)}" data-rate="${escape(price_list_rate || 0)}"
title="${item.item_name}"> title="${item.item_name}">
@ -128,7 +128,7 @@ erpnext.PointOfSale.ItemSelector = class {
<div class="item-name"> <div class="item-name">
${frappe.ellipsis(item.item_name, 18)} ${frappe.ellipsis(item.item_name, 18)}
</div> </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>
</div>` </div>`
); );

View File

@ -94,7 +94,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
get_item_html(doc, item_data) { get_item_html(doc, item_data) {
return `<div class="item-row-wrapper"> return `<div class="item-row-wrapper">
<div class="item-name">${item_data.item_name}</div> <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 class="item-rate-disc">${get_rate_discount_html()}</div>
</div>`; </div>`;

View File

@ -41,8 +41,20 @@ def get_columns(filters):
{"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150}, {"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150},
{"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150}, {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150},
{"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100}, {"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"), "label": _("Sales Order"),
"fieldtype": "Link", "fieldtype": "Link",
@ -93,8 +105,9 @@ def get_columns(filters):
}, },
{ {
"label": _("Billed Amount"), "label": _("Billed Amount"),
"fieldtype": "currency", "fieldtype": "Currency",
"fieldname": "billed_amount", "fieldname": "billed_amount",
"options": "currency",
"width": 120, "width": 120,
}, },
{ {
@ -104,6 +117,13 @@ def get_columns(filters):
"options": "Company", "options": "Company",
"width": 100, "width": 100,
}, },
{
"label": _("Currency"),
"fieldtype": "Link",
"fieldname": "currency",
"options": "Currency",
"hidden": 1,
},
] ]
@ -141,31 +161,12 @@ def get_data(filters):
"billed_amount": flt(record.get("billed_amt")), "billed_amount": flt(record.get("billed_amt")),
"company": record.get("company"), "company": record.get("company"),
} }
row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency")
data.append(row) data.append(row)
return data 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(): def get_customer_details():
details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"]) details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"])
customer_details = {} customer_details = {}
@ -187,29 +188,50 @@ def get_item_details():
def get_sales_order_details(company_list, filters): 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( query = (
""" frappe.qb.from_(db_so)
SELECT .inner_join(db_so_item)
so_item.item_code, so_item.description, so_item.qty, .on(db_so_item.parent == db_so.name)
so_item.uom, so_item.base_rate, so_item.base_amount, .select(
so.name, so.transaction_date, so.customer,so.territory, db_so.name,
so.project, so_item.delivered_qty, db_so.customer,
so_item.billed_amt, so.company db_so.transaction_date,
FROM db_so.territory,
`tabSales Order` so, `tabSales Order Item` so_item db_so.project,
WHERE db_so.company,
so.name = so_item.parent db_so_item.item_code,
AND so.company in ({0}) db_so_item.description,
AND so.docstatus = 1 {1} db_so_item.qty,
""".format( db_so_item.uom,
",".join(["%s"] * len(company_list)), conditions db_so_item.base_rate,
), db_so_item.base_amount,
tuple(company_list), db_so_item.delivered_qty,
as_dict=1, (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): def get_chart_data(data):
item_wise_sales_map = {} item_wise_sales_map = {}

View File

@ -4,7 +4,6 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr, getdate from frappe.utils import cstr, getdate
from .default_website import website_maker
def create_fiscal_year_and_company(args): def create_fiscal_year_and_company(args):
@ -48,83 +47,6 @@ def enable_shopping_cart(args): # nosemgrep
).insert() ).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): def get_fy_details(fy_start_date, fy_end_date):
start_year = getdate(fy_start_date).year start_year = getdate(fy_start_date).year
if start_year == getdate(fy_end_date).year: if start_year == getdate(fy_end_date).year:

View File

@ -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()

View File

@ -5,7 +5,6 @@
import frappe import frappe
from frappe import _ from frappe import _
from .operations import company_setup
from .operations import install_fixtures as fixtures from .operations import install_fixtures as fixtures
@ -35,7 +34,6 @@ def get_setup_stages(args=None):
"fail_msg": "Failed to set defaults", "fail_msg": "Failed to set defaults",
"tasks": [ "tasks": [
{"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, {"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 @@ def setup_defaults(args):
fixtures.install_defaults(frappe._dict(args)) 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): def fin(args):
frappe.local.message_log = [] frappe.local.message_log = []
login_as_first_user(args) login_as_first_user(args)
@ -81,5 +73,4 @@ def setup_complete(args=None):
stage_fixtures(args) stage_fixtures(args)
setup_company(args) setup_company(args)
setup_defaults(args) setup_defaults(args)
stage_four(args)
fin(args) fin(args)

View File

@ -42,7 +42,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
let warehouse = unescape(element.attr('data-warehouse')); let warehouse = unescape(element.attr('data-warehouse'));
let actual_qty = unescape(element.attr('data-actual_qty')); let actual_qty = unescape(element.attr('data-actual_qty'));
let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry'))); 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) { if (disable_quick_entry) {
open_stock_entry(item, warehouse, entry_type); open_stock_entry(item, warehouse, entry_type);
@ -63,11 +63,19 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
function open_stock_entry(item, warehouse, entry_type) { function open_stock_entry(item, warehouse, entry_type) {
frappe.model.with_doctype('Stock Entry', function () { frappe.model.with_doctype('Stock Entry', function () {
var doc = frappe.model.get_new_doc('Stock Entry'); 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'); var row = frappe.model.add_child(doc, 'items');
row.item_code = item; 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); frappe.set_route('Form', doc.doctype, doc.name);
}); });

View File

@ -159,13 +159,18 @@ def update_qty(bin_name, args):
last_sle_qty = ( last_sle_qty = (
frappe.qb.from_(sle) frappe.qb.from_(sle)
.select(sle.qty_after_transaction) .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(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
.orderby(sle.creation, order=Order.desc) .orderby(sle.creation, order=Order.desc)
.limit(1) .limit(1)
.run() .run()
) )
actual_qty = 0.0
if last_sle_qty: if last_sle_qty:
actual_qty = last_sle_qty[0][0] actual_qty = last_sle_qty[0][0]

View File

@ -228,6 +228,7 @@ class DeliveryNote(SellingController):
def on_submit(self): def on_submit(self):
self.validate_packed_qty() self.validate_packed_qty()
self.update_pick_list_status()
# Check for Approving Authority # Check for Approving Authority
frappe.get_doc("Authorization Control").validate_approving_authority( frappe.get_doc("Authorization Control").validate_approving_authority(
@ -313,6 +314,11 @@ class DeliveryNote(SellingController):
if has_error: if has_error:
raise frappe.ValidationError 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): def check_next_docstatus(self):
submit_rv = frappe.db.sql( submit_rv = frappe.db.sql(
"""select t1.name """select t1.name

View File

@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', {
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
&& frm.doc.__onload.has_stock_ledger.length) { && frm.doc.__onload.has_stock_ledger.length) {
let allow_to_edit_fields = ['disabled', 'fetch_from_parent', let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
'type_of_transaction', 'condition']; 'type_of_transaction', 'condition', 'mandatory_depends_on'];
frm.fields.forEach((field) => { frm.fields.forEach((field) => {
if (!in_list(allow_to_edit_fields, field.df.fieldname)) { if (!in_list(allow_to_edit_fields, field.df.fieldname)) {

View File

@ -24,6 +24,9 @@
"istable", "istable",
"applicable_condition_example_section", "applicable_condition_example_section",
"condition", "condition",
"conditional_mandatory_section",
"reqd",
"mandatory_depends_on",
"conditional_rule_examples_section", "conditional_rule_examples_section",
"html_19" "html_19"
], ],
@ -153,11 +156,28 @@
"fieldname": "conditional_rule_examples_section", "fieldname": "conditional_rule_examples_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Conditional Rule Examples" "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, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-11-15 15:50:16.767105", "modified": "2023-01-31 13:44:38.507698",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Inventory Dimension", "name": "Inventory Dimension",

View File

@ -126,6 +126,8 @@ class InventoryDimension(Document):
insert_after="inventory_dimension", insert_after="inventory_dimension",
options=self.reference_document, options=self.reference_document,
label=self.dimension_name, label=self.dimension_name,
reqd=self.reqd,
mandatory_depends_on=self.mandatory_depends_on,
), ),
] ]
@ -142,6 +144,8 @@ class InventoryDimension(Document):
"Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname}
) and not field_exists("Stock Ledger Entry", self.target_fieldname): ) and not field_exists("Stock Ledger Entry", self.target_fieldname):
dimension_field = dimension_fields[1] dimension_field = dimension_fields[1]
dimension_field["mandatory_depends_on"] = ""
dimension_field["reqd"] = 0
dimension_field["fieldname"] = self.target_fieldname dimension_field["fieldname"] = self.target_fieldname
custom_fields["Stock Ledger Entry"] = dimension_field custom_fields["Stock Ledger Entry"] = dimension_field

View File

@ -85,6 +85,9 @@ class TestInventoryDimension(FrappeTestCase):
condition="parent.purpose == 'Material Issue'", condition="parent.purpose == 'Material Issue'",
) )
inv_dim1.reqd = 0
inv_dim1.save()
create_inventory_dimension( create_inventory_dimension(
reference_document="Shelf", reference_document="Shelf",
type_of_transaction="Inward", type_of_transaction="Inward",
@ -205,6 +208,48 @@ class TestInventoryDimension(FrappeTestCase):
) )
) )
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(): def prepare_test_data():
if not frappe.db.exists("DocType", "Shelf"): if not frappe.db.exists("DocType", "Shelf"):
@ -251,6 +296,22 @@ def prepare_test_data():
create_warehouse("Rack Warehouse") 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): def create_inventory_dimension(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -894,6 +894,12 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.uom = frm.doc.stock_uom; new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description; 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);
}
])
}); });
} }

View File

@ -366,10 +366,11 @@ frappe.ui.form.on('Material Request', {
frappe.ui.form.on("Material Request Item", { frappe.ui.form.on("Material Request Item", {
qty: function (frm, doctype, name) { qty: function (frm, doctype, name) {
var d = locals[doctype][name]; const item = locals[doctype][name];
if (flt(d.qty) < flt(d.min_order_qty)) { if (flt(item.qty) < flt(item.min_order_qty)) {
frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum 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) { from_warehouse: function(frm, doctype, name) {

View File

@ -26,7 +26,8 @@
"locations", "locations",
"amended_from", "amended_from",
"print_settings_section", "print_settings_section",
"group_same_items" "group_same_items",
"status"
], ],
"fields": [ "fields": [
{ {
@ -168,11 +169,26 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Customer Name", "label": "Customer Name",
"read_only": 1 "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, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-07-19 11:03:04.442174", "modified": "2023-01-24 10:33:43.244476",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List", "name": "Pick List",
@ -244,4 +260,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -11,7 +11,8 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import map_child_doc from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case 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 import cint, floor, flt, today
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
@ -77,15 +78,32 @@ class PickList(Document):
) )
def on_submit(self): def on_submit(self):
self.update_status()
self.update_bundle_picked_qty() self.update_bundle_picked_qty()
self.update_reference_qty() self.update_reference_qty()
self.update_sales_order_picking_status() self.update_sales_order_picking_status()
def on_cancel(self): def on_cancel(self):
self.update_status()
self.update_bundle_picked_qty() self.update_bundle_picked_qty()
self.update_reference_qty() self.update_reference_qty()
self.update_sales_order_picking_status() 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): def update_reference_qty(self):
packed_items = [] packed_items = []
so_items = [] so_items = []
@ -162,6 +180,7 @@ class PickList(Document):
def set_item_locations(self, save=False): def set_item_locations(self, save=False):
self.validate_for_qty() self.validate_for_qty()
items = self.aggregate_item_qty() items = self.aggregate_item_qty()
picked_items_details = self.get_picked_items_details(items)
self.item_location_map = frappe._dict() self.item_location_map = frappe._dict()
from_warehouses = None from_warehouses = None
@ -180,7 +199,11 @@ class PickList(Document):
self.item_location_map.setdefault( self.item_location_map.setdefault(
item_code, item_code,
get_available_item_locations( 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 @@ class PickList(Document):
already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)), 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]: def _get_product_bundles(self) -> Dict[str, str]:
# Dict[so_item_row: item_code] # Dict[so_item_row: item_code]
product_bundles = {} product_bundles = {}
@ -346,29 +419,30 @@ class PickList(Document):
return int(flt(min(possible_bundles), precision or 6)) 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]: def get_picked_items_qty(items) -> List[Dict]:
return frappe.db.sql( pi_item = frappe.qb.DocType("Pick List Item")
f""" return (
SELECT frappe.qb.from_(pi_item)
sales_order_item, .select(
item_code, pi_item.sales_order_item,
sales_order, pi_item.item_code,
SUM(stock_qty) AS stock_qty, pi_item.sales_order,
SUM(picked_qty) AS picked_qty Sum(pi_item.stock_qty).as_("stock_qty"),
FROM Sum(pi_item.picked_qty).as_("picked_qty"),
`tabPick List Item` )
WHERE .where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items)))
sales_order_item IN ( .groupby(
{", ".join(frappe.db.escape(d) for d in items)} pi_item.sales_order_item,
) pi_item.sales_order,
AND docstatus = 1 )
GROUP BY .for_update()
sales_order_item, ).run(as_dict=True)
sales_order
FOR UPDATE
""",
as_dict=1,
)
def validate_item_locations(pick_list): def validate_item_locations(pick_list):
@ -434,31 +508,38 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
def get_available_item_locations( 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 = [] 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_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") has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
if has_batch_no and has_serial_no: if has_batch_no and has_serial_no:
locations = get_available_item_locations_for_serial_and_batched_item( 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: elif has_serial_no:
locations = get_available_item_locations_for_serialized_item( 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: elif has_batch_no:
locations = get_available_item_locations_for_batched_item( 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: else:
locations = get_available_item_locations_for_other_item( 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) total_qty_available = sum(location.get("qty") for location in locations)
remaining_qty = required_qty - total_qty_available remaining_qty = required_qty - total_qty_available
if remaining_qty > 0 and not ignore_validation: if remaining_qty > 0 and not ignore_validation:
@ -469,25 +550,60 @@ def get_available_item_locations(
title=_("Insufficient Stock"), 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 return locations
def get_available_item_locations_for_serialized_item( 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: 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_nos = query.run(as_list=True)
"Serial No",
fields=["name", "warehouse"],
filters=filters,
limit=required_qty,
order_by="purchase_date",
as_list=1,
)
warehouse_serial_nos_map = frappe._dict() warehouse_serial_nos_map = frappe._dict()
for serial_no, warehouse in serial_nos: for serial_no, warehouse in serial_nos:
@ -501,7 +617,7 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item( 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") sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch") batch = frappe.qb.DocType("Batch")
@ -521,6 +637,7 @@ def get_available_item_locations_for_batched_item(
.groupby(sle.warehouse, sle.batch_no, sle.item_code) .groupby(sle.warehouse, sle.batch_no, sle.item_code)
.having(Sum(sle.actual_qty) > 0) .having(Sum(sle.actual_qty) > 0)
.orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse) .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: if from_warehouses:
@ -530,53 +647,58 @@ def get_available_item_locations_for_batched_item(
def get_available_item_locations_for_serial_and_batched_item( 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 # Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item( locations = get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company
) )
filters = frappe._dict( if locations:
{"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""} 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:
for location in locations: location.qty = (
filters.batch_no = location.batch_no required_qty if location.qty > required_qty else location.qty
filters.warehouse = location.warehouse ) # if extra qty in batch
location.qty = (
required_qty if location.qty > required_qty else location.qty
) # if extra qty in batch
serial_nos = frappe.get_list( serial_nos = (
"Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date" 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] serial_nos = [sn.name for sn in serial_nos]
location.serial_no = serial_nos location.serial_no = serial_nos
location.qty = len(serial_nos)
return locations return locations
def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): def get_available_item_locations_for_other_item(
# gets all items available in different warehouses item_code, from_warehouses, required_qty, company, total_picked_qty=0
warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")] ):
bin = frappe.qb.DocType("Bin")
filters = frappe._dict( query = (
{"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]} 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: 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( item_locations = query.run(as_dict=True)
"Bin",
fields=["warehouse", "actual_qty as qty"],
filters=filters,
limit=required_qty,
order_by="creation",
)
return item_locations return item_locations

View File

@ -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];
},
};

View File

@ -414,6 +414,7 @@ class TestPickList(FrappeTestCase):
pick_list.submit() pick_list.submit()
delivery_note = create_delivery_note(pick_list.name) 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[0].qty, delivery_note.items[0].qty)
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
@ -663,3 +664,147 @@ class TestPickList(FrappeTestCase):
self.assertEqual(dn.items[0].rate, 42) self.assertEqual(dn.items[0].rate, 42)
so.reload() so.reload()
self.assertEqual(so.per_delivered, 100) 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))

View File

@ -888,7 +888,7 @@ def update_billing_percentage(pr_doc, update_modified=True):
# Update Billing % based on pending accepted qty # Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0 total_amount, total_billed_amount = 0, 0
for item in pr_doc.items: for item in pr_doc.items:
return_data = frappe.db.get_list( return_data = frappe.get_all(
"Purchase Receipt", "Purchase Receipt",
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
filters=[ filters=[

View File

@ -221,7 +221,7 @@ class QualityInspection(Document):
def item_query(doctype, txt, searchfield, start, page_len, filters): def item_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond 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): if not from_doctype or not frappe.db.exists("DocType", from_doctype):
return [] return []

View File

@ -158,6 +158,7 @@ class StockEntry(StockController):
self.validate_subcontract_order() self.validate_subcontract_order()
self.update_subcontract_order_supplied_items() self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status() self.update_subcontracting_order_status()
self.update_pick_list_status()
self.make_gl_entries() self.make_gl_entries()
@ -2276,6 +2277,11 @@ class StockEntry(StockController):
update_subcontracting_order_status(self.subcontracting_order) 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): def set_missing_values(self):
"Updates rate and availability of all the items of mapped doc." "Updates rate and availability of all the items of mapped doc."
self.set_transfer_qty() self.set_transfer_qty()

View File

@ -1662,6 +1662,48 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(BatchExpiredError, se.save) 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): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -62,7 +62,7 @@ def execute(filters=None):
continue continue
total_stock_value = sum(item_value[(item, item_group)]) 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"] fifo_queue = item_ageing[item]["fifo_queue"]
average_age = 0.00 average_age = 0.00
@ -89,10 +89,11 @@ def get_columns(filters):
"""return columns""" """return columns"""
columns = [ columns = [
_("Item") + ":Link/Item:180", _("Item") + ":Link/Item:150",
_("Item Group") + "::100", _("Item Name") + ":Link/Item:150",
_("Item Group") + "::120",
_("Value") + ":Currency:120", _("Value") + ":Currency:120",
_("Age") + ":Float:80", _("Age") + ":Float:120",
] ]
return columns return columns
@ -123,7 +124,7 @@ def get_warehouse_list(filters):
def add_warehouse_column(columns, warehouse_list): def add_warehouse_column(columns, warehouse_list):
if len(warehouse_list) > 1: if len(warehouse_list) > 1:
columns += [_("Total Qty") + ":Int:90"] columns += [_("Total Qty") + ":Int:120"]
for wh in warehouse_list: for wh in warehouse_list:
columns += [_(wh.name) + ":Int:120"] columns += [_(wh.name) + ":Int:100"]

View File

@ -1050,7 +1050,7 @@ class update_entries_after(object):
frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) 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""" """get stock ledger entries filtered by specific posting datetime conditions"""
args["time_format"] = "%H:%i:%s" args["time_format"] = "%H:%i:%s"
@ -1076,13 +1076,13 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
posting_date < %(posting_date)s or posting_date < %(posting_date)s or
( (
posting_date = %(posting_date)s and 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 order by timestamp(posting_date, posting_time) desc, creation desc
limit 1 limit 1
for update""".format( for update""".format(
voucher_condition=voucher_condition operator=operator, voucher_condition=voucher_condition
), ),
args, args,
as_dict=1, as_dict=1,
@ -1179,7 +1179,7 @@ def get_stock_ledger_entries(
def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
return frappe.db.get_value( return frappe.db.get_value(
"Stock Ledger Entry", "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", "item_code",
"warehouse", "warehouse",
@ -1375,7 +1375,7 @@ def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = flt(args.actual_qty) stock_reco_qty_shift = flt(args.actual_qty)
else: else:
# reco is being submitted # 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" "qty_after_transaction"
) )

View File

@ -11,6 +11,7 @@ frappe.listview_settings['Subcontracting Order'] = {
"Partial Material Transferred": "purple", "Partial Material Transferred": "purple",
"Material Transferred": "blue", "Material Transferred": "blue",
"Closed": "red", "Closed": "red",
"Cancelled": "red",
}; };
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
}, },

View File

@ -38,22 +38,27 @@ def get_context(context):
if not frappe.has_website_permission(context.doc): if not frappe.has_website_permission(context.doc):
frappe.throw(_("Not Permitted"), frappe.PermissionError) frappe.throw(_("Not Permitted"), frappe.PermissionError)
# check for the loyalty program of the customer context.available_loyalty_points = 0.0
customer_loyalty_program = frappe.db.get_value( if context.doc.get("customer"):
"Customer", context.doc.customer, "loyalty_program" # check for the loyalty program of the customer
) customer_loyalty_program = frappe.db.get_value(
if customer_loyalty_program: "Customer", context.doc.customer, "loyalty_program"
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points,
) )
loyalty_program_details = get_loyalty_program_details_with_points( if customer_loyalty_program:
context.doc.customer, customer_loyalty_program from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
) get_loyalty_program_details_with_points,
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) )
# show Make Purchase Invoice button based on permission loyalty_program_details = get_loyalty_program_details_with_points(
context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create") 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): def get_attachments(dt, dn):

View File

@ -3537,7 +3537,6 @@ Quality Feedback Template,质量反馈模板,
Rules for applying different promotional schemes.,适用不同促销计划的规则。, Rules for applying different promotional schemes.,适用不同促销计划的规则。,
Shift,转移, Shift,转移,
Show {0},显示{0}, Show {0},显示{0},
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",命名系列中不允许使用除&quot;-&quot;, &quot;#&quot;, &quot;।&quot;, &quot;/&quot;, &quot;{{&quot; 和 &quot;}}&quot;之外的特殊字符 {0},
Target Details,目标细节, Target Details,目标细节,
{0} already has a Parent Procedure {1}.,{0}已有父程序{1}。, {0} already has a Parent Procedure {1}.,{0}已有父程序{1}。,
API,应用程序界面, API,应用程序界面,

Can't render this file because it is too large.

View File

@ -3311,7 +3311,6 @@ Quality Feedback Template,質量反饋模板,
Rules for applying different promotional schemes.,適用不同促銷計劃的規則。, Rules for applying different promotional schemes.,適用不同促銷計劃的規則。,
Shift,轉移, Shift,轉移,
Show {0},顯示{0}, Show {0},顯示{0},
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",命名系列中不允許使用除 &quot;-&quot;, &quot;#&quot;, &quot;।&quot;, &quot;/&quot;, &quot;{{&quot; 和 &quot;}}&quot;之外的特殊字符 {0},
Target Details,目標細節, Target Details,目標細節,
API,API, API,API,
Annual,年刊, Annual,年刊,

Can't render this file because it has a wrong number of fields in line 3314.