diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 73aae33e936..d70977c07e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,8 +32,8 @@ repos: - id: black additional_dependencies: ['click==8.0.4'] - - repo: https://github.com/timothycrosley/isort - rev: 5.9.1 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - id: isort exclude: ".*setup.py$" diff --git a/CODEOWNERS b/CODEOWNERS index e406f8f56ee..c4ea16328e6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,7 +4,7 @@ # the repo. Unless a later match takes precedence, erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar -erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar +erpnext/assets/ @anandbaburajan @deepeshgarg007 erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar @@ -16,6 +16,7 @@ erpnext/maintenance/ @rohitwaghchaure @s-aga-r erpnext/manufacturing/ @rohitwaghchaure @s-aga-r erpnext/quality_management/ @rohitwaghchaure @s-aga-r erpnext/stock/ @rohitwaghchaure @s-aga-r +erpnext/subcontracting @rohitwaghchaure @s-aga-r erpnext/crm/ @NagariaHussain erpnext/education/ @rutwikhdev diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 8ae90ceb383..d537adfcbfd 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = { accounts = nodes; } - const get_balances = frappe.call({ - method: 'erpnext.accounts.utils.get_account_balances', - args: { - accounts: accounts, - company: cur_tree.args.company - }, - }); + frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { + if(value) { - get_balances.then(r => { - if (!r.message || r.message.length == 0) return; + const get_balances = frappe.call({ + method: 'erpnext.accounts.utils.get_account_balances', + args: { + accounts: accounts, + company: cur_tree.args.company + }, + }); - for (let account of r.message) { + get_balances.then(r => { + if (!r.message || r.message.length == 0) return; - const node = cur_tree.nodes && cur_tree.nodes[account.value]; - if (!node || node.is_root) continue; + for (let account of r.message) { - // show Dr if positive since balance is calculated as debit - credit else show Cr - const balance = account.balance_in_account_currency || account.balance; - const dr_or_cr = balance > 0 ? "Dr": "Cr"; - const format = (value, currency) => format_currency(Math.abs(value), currency); + const node = cur_tree.nodes && cur_tree.nodes[account.value]; + if (!node || node.is_root) continue; - if (account.balance!==undefined) { - node.parent && node.parent.find('.balance-area').remove(); - $('' - + (account.balance_in_account_currency ? - (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") - + format(account.balance, account.company_currency) - + " " + dr_or_cr - + '').insertBefore(node.$ul); - } + // show Dr if positive since balance is calculated as debit - credit else show Cr + const balance = account.balance_in_account_currency || account.balance; + const dr_or_cr = balance > 0 ? "Dr": "Cr"; + const format = (value, currency) => format_currency(Math.abs(value), currency); + + if (account.balance!==undefined) { + node.parent && node.parent.find('.balance-area').remove(); + $('' + + (account.balance_in_account_currency ? + (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") + + format(account.balance, account.company_currency) + + " " + dr_or_cr + + '').insertBefore(node.$ul); + } + } + }); } }); }, diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index ee501f664b6..741d4283e2f 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -1,38 +1,38 @@ { - "country_code": "de", - "name": "SKR03 mit Kontonummern", - "tree": { - "Aktiva": { - "is_group": 1, + "country_code": "de", + "name": "SKR03 mit Kontonummern", + "tree": { + "Aktiva": { + "is_group": 1, "root_type": "Asset", - "A - Anlagevermögen": { - "is_group": 1, - "EDV-Software": { - "account_number": "0027", - "account_type": "Fixed Asset" - }, - "Gesch\u00e4ftsausstattung": { - "account_number": "0410", - "account_type": "Fixed Asset" - }, - "B\u00fcroeinrichtung": { - "account_number": "0420", - "account_type": "Fixed Asset" - }, - "Darlehen": { - "account_number": "0565" - }, - "Maschinen": { - "account_number": "0210", - "account_type": "Fixed Asset" - }, - "Betriebsausstattung": { - "account_number": "0400", - "account_type": "Fixed Asset" - }, - "Ladeneinrichtung": { - "account_number": "0430", - "account_type": "Fixed Asset" + "A - Anlagevermögen": { + "is_group": 1, + "EDV-Software": { + "account_number": "0027", + "account_type": "Fixed Asset" + }, + "Geschäftsausstattung": { + "account_number": "0410", + "account_type": "Fixed Asset" + }, + "Büroeinrichtung": { + "account_number": "0420", + "account_type": "Fixed Asset" + }, + "Darlehen": { + "account_number": "0565" + }, + "Maschinen": { + "account_number": "0210", + "account_type": "Fixed Asset" + }, + "Betriebsausstattung": { + "account_number": "0400", + "account_type": "Fixed Asset" + }, + "Ladeneinrichtung": { + "account_number": "0430", + "account_type": "Fixed Asset" }, "Accumulated Depreciation": { "account_type": "Accumulated Depreciation" @@ -60,36 +60,46 @@ "Durchlaufende Posten": { "account_number": "1590" }, - "Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { + "Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": { "account_number": "1371" }, "Abziehbare Vorsteuer": { - "account_type": "Tax", "is_group": 1, - "Abziehbare Vorsteuer 7%": { - "account_number": "1571" + "Abziehbare Vorsteuer 7 %": { + "account_number": "1571", + "account_type": "Tax", + "tax_rate": 7.0 }, - "Abziehbare Vorsteuer 19%": { - "account_number": "1576" + "Abziehbare Vorsteuer 19 %": { + "account_number": "1576", + "account_type": "Tax", + "tax_rate": 19.0 }, - "Abziehbare Vorsteuer nach \u00a713b UStG 19%": { - "account_number": "1577" - }, - "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { - "account_number": "3120" + "Abziehbare Vorsteuer nach § 13b UStG 19 %": { + "account_number": "1577", + "account_type": "Tax", + "tax_rate": 19.0 } } }, "III. Wertpapiere": { - "is_group": 1 + "is_group": 1, + "Anteile an verbundenen Unternehmen (Umlaufvermögen)": { + "account_number": "1340" + }, + "Anteile an herrschender oder mit Mehrheit beteiligter Gesellschaft": { + "account_number": "1344" + }, + "Sonstige Wertpapiere": { + "account_number": "1348" + } }, "IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": { "is_group": 1, "Kasse": { - "account_type": "Cash", "is_group": 1, + "account_type": "Cash", "Kasse": { - "is_group": 1, "account_number": "1000", "account_type": "Cash" } @@ -111,21 +121,21 @@ "C - Rechnungsabgrenzungsposten": { "is_group": 1, "Aktive Rechnungsabgrenzung": { - "account_number": "0980" + "account_number": "0980" } }, "D - Aktive latente Steuern": { "is_group": 1, "Aktive latente Steuern": { - "account_number": "0983" + "account_number": "0983" } }, "E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": { "is_group": 1 } - }, - "Passiva": { - "is_group": 1, + }, + "Passiva": { + "is_group": 1, "root_type": "Liability", "A. Eigenkapital": { "is_group": 1, @@ -200,26 +210,32 @@ }, "Umsatzsteuer": { "is_group": 1, - "account_type": "Tax", - "Umsatzsteuer 7%": { - "account_number": "1771" + "Umsatzsteuer 7 %": { + "account_number": "1771", + "account_type": "Tax", + "tax_rate": 7.0 }, - "Umsatzsteuer 19%": { - "account_number": "1776" + "Umsatzsteuer 19 %": { + "account_number": "1776", + "account_type": "Tax", + "tax_rate": 19.0 }, "Umsatzsteuer-Vorauszahlung": { - "account_number": "1780" + "account_number": "1780", + "account_type": "Tax" }, "Umsatzsteuer-Vorauszahlung 1/11": { "account_number": "1781" }, - "Umsatzsteuer \u00a7 13b UStG 19%": { - "account_number": "1787" + "Umsatzsteuer nach § 13b UStG 19 %": { + "account_number": "1787", + "account_type": "Tax", + "tax_rate": 19.0 }, "Umsatzsteuer Vorjahr": { "account_number": "1790" }, - "Umsatzsteuer fr\u00fchere Jahre": { + "Umsatzsteuer frühere Jahre": { "account_number": "1791" } } @@ -234,44 +250,56 @@ "E. Passive latente Steuern": { "is_group": 1 } - }, - "Erl\u00f6se u. Ertr\u00e4ge 2/8": { - "is_group": 1, - "root_type": "Income", - "Erl\u00f6skonten 8": { + }, + "Erlöse u. Erträge 2/8": { + "is_group": 1, + "root_type": "Income", + "Erlöskonten 8": { "is_group": 1, - "Erl\u00f6se": { - "account_number": "8200", - "account_type": "Income Account" - }, - "Erl\u00f6se USt. 19%": { - "account_number": "8400", - "account_type": "Income Account" - }, - "Erl\u00f6se USt. 7%": { - "account_number": "8300", - "account_type": "Income Account" - } - }, - "Ertragskonten 2": { - "is_group": 1, - "sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": { - "account_number": "2650", - "account_type": "Income Account" - }, - "Au\u00dferordentliche Ertr\u00e4ge": { - "account_number": "2500", - "account_type": "Income Account" - }, - "Sonstige Ertr\u00e4ge": { - "account_number": "2700", - "account_type": "Income Account" - } - } - }, - "Aufwendungen 2/4": { - "is_group": 1, + "Erlöse": { + "account_number": "8200", + "account_type": "Income Account" + }, + "Erlöse USt. 19 %": { + "account_number": "8400", + "account_type": "Income Account" + }, + "Erlöse USt. 7 %": { + "account_number": "8300", + "account_type": "Income Account" + } + }, + "Ertragskonten 2": { + "is_group": 1, + "sonstige Zinsen und ähnliche Erträge": { + "account_number": "2650", + "account_type": "Income Account" + }, + "Außerordentliche Erträge": { + "account_number": "2500", + "account_type": "Income Account" + }, + "Sonstige Erträge": { + "account_number": "2700", + "account_type": "Income Account" + } + } + }, + "Aufwendungen 2/4": { + "is_group": 1, "root_type": "Expense", + "Fremdleistungen": { + "account_number": "3100", + "account_type": "Expense Account" + }, + "Fremdleistungen ohne Vorsteuer": { + "account_number": "3109", + "account_type": "Expense Account" + }, + "Bauleistungen eines im Inland ansässigen Unternehmers 19 % Vorsteuer und 19 % Umsatzsteuer": { + "account_number": "3120", + "account_type": "Expense Account" + }, "Wareneingang": { "account_number": "3200" }, @@ -298,234 +326,234 @@ "Gegenkonto 4996-4998": { "account_number": "4999" }, - "Abschreibungen": { - "is_group": 1, + "Abschreibungen": { + "is_group": 1, "Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": { - "account_number": "4830", - "account_type": "Accumulated Depreciation" + "account_number": "4830", + "account_type": "Accumulated Depreciation" }, "Abschreibungen auf Gebäude": { - "account_number": "4831", - "account_type": "Depreciation" + "account_number": "4831", + "account_type": "Depreciation" }, "Abschreibungen auf Kfz": { - "account_number": "4832", - "account_type": "Depreciation" + "account_number": "4832", + "account_type": "Depreciation" }, "Sofortabschreibung GWG": { - "account_number": "4855", - "account_type": "Expense Account" + "account_number": "4855", + "account_type": "Expense Account" } - }, - "Kfz-Kosten": { - "is_group": 1, - "Kfz-Steuer": { - "account_number": "4510", - "account_type": "Expense Account" - }, - "Kfz-Versicherungen": { - "account_number": "4520", - "account_type": "Expense Account" - }, - "laufende Kfz-Betriebskosten": { - "account_number": "4530", - "account_type": "Expense Account" - }, - "Kfz-Reparaturen": { - "account_number": "4540", - "account_type": "Expense Account" - }, - "Fremdfahrzeuge": { - "account_number": "4570", - "account_type": "Expense Account" - }, - "sonstige Kfz-Kosten": { - "account_number": "4580", - "account_type": "Expense Account" - } - }, - "Personalkosten": { - "is_group": 1, - "Geh\u00e4lter": { - "account_number": "4120", - "account_type": "Expense Account" - }, - "gesetzliche soziale Aufwendungen": { - "account_number": "4130", - "account_type": "Expense Account" - }, - "Aufwendungen f\u00fcr Altersvorsorge": { - "account_number": "4165", - "account_type": "Expense Account" - }, - "Verm\u00f6genswirksame Leistungen": { - "account_number": "4170", - "account_type": "Expense Account" - }, - "Aushilfsl\u00f6hne": { - "account_number": "4190", - "account_type": "Expense Account" - } - }, - "Raumkosten": { - "is_group": 1, - "Miete und Nebenkosten": { - "account_number": "4210", - "account_type": "Expense Account" - }, - "Gas, Wasser, Strom (Verwaltung, Vertrieb)": { - "account_number": "4240", - "account_type": "Expense Account" - }, - "Reinigung": { - "account_number": "4250", - "account_type": "Expense Account" - } - }, - "Reparatur/Instandhaltung": { - "is_group": 1, - "Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": { - "account_number": "4805", - "account_type": "Expense Account" - } - }, - "Versicherungsbeitr\u00e4ge": { - "is_group": 1, - "Versicherungen": { - "account_number": "4360", - "account_type": "Expense Account" - }, - "Beitr\u00e4ge": { - "account_number": "4380", - "account_type": "Expense Account" - }, - "sonstige Ausgaben": { - "account_number": "4390", - "account_type": "Expense Account" - }, - "steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": { - "account_number": "4396", - "account_type": "Expense Account" - } - }, - "Werbe-/Reisekosten": { - "is_group": 1, - "Werbekosten": { - "account_number": "4610", - "account_type": "Expense Account" - }, - "Aufmerksamkeiten": { - "account_number": "4653", - "account_type": "Expense Account" - }, - "nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": { - "account_number": "4665", - "account_type": "Expense Account" - }, - "Reisekosten Unternehmer": { - "account_number": "4670", - "account_type": "Expense Account" - } - }, - "verschiedene Kosten": { - "is_group": 1, - "Porto": { - "account_number": "4910", - "account_type": "Expense Account" - }, - "Telekom": { - "account_number": "4920", - "account_type": "Expense Account" - }, - "Mobilfunk D2": { - "account_number": "4921", - "account_type": "Expense Account" - }, - "Internet": { - "account_number": "4922", - "account_type": "Expense Account" - }, - "B\u00fcrobedarf": { - "account_number": "4930", - "account_type": "Expense Account" - }, - "Zeitschriften, B\u00fccher": { - "account_number": "4940", - "account_type": "Expense Account" - }, - "Fortbildungskosten": { - "account_number": "4945", - "account_type": "Expense Account" - }, - "Buchf\u00fchrungskosten": { - "account_number": "4955", - "account_type": "Expense Account" - }, - "Abschlu\u00df- u. Pr\u00fcfungskosten": { - "account_number": "4957", - "account_type": "Expense Account" - }, - "Nebenkosten des Geldverkehrs": { - "account_number": "4970", - "account_type": "Expense Account" - }, - "Werkzeuge und Kleinger\u00e4te": { - "account_number": "4985", - "account_type": "Expense Account" - } - }, - "Zinsaufwendungen": { - "is_group": 1, - "Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": { - "account_number": "2110", - "account_type": "Expense Account" - }, - "Zinsaufwendungen f\u00fcr KFZ Finanzierung": { - "account_number": "2121", - "account_type": "Expense Account" - } - } - }, - "Anfangsbestand 9": { - "is_group": 1, - "root_type": "Equity", - "Saldenvortragskonten": { - "is_group": 1, - "Saldenvortrag Sachkonten": { - "account_number": "9000" - }, - "Saldenvortr\u00e4ge Debitoren": { - "account_number": "9008" - }, - "Saldenvortr\u00e4ge Kreditoren": { - "account_number": "9009" - } - } - }, - "Privatkonten 1": { - "is_group": 1, - "root_type": "Equity", - "Privatentnahmen/-einlagen": { - "is_group": 1, - "Privatentnahme allgemein": { - "account_number": "1800" - }, - "Privatsteuern": { - "account_number": "1810" - }, - "Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": { - "account_number": "1820" - }, - "Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": { - "account_number": "1830" - }, - "Au\u00dfergew\u00f6hnliche Belastungen": { - "account_number": "1850" - }, - "Privateinlagen": { - "account_number": "1890" - } - } - } - } + }, + "Kfz-Kosten": { + "is_group": 1, + "Kfz-Steuer": { + "account_number": "4510", + "account_type": "Expense Account" + }, + "Kfz-Versicherungen": { + "account_number": "4520", + "account_type": "Expense Account" + }, + "laufende Kfz-Betriebskosten": { + "account_number": "4530", + "account_type": "Expense Account" + }, + "Kfz-Reparaturen": { + "account_number": "4540", + "account_type": "Expense Account" + }, + "Fremdfahrzeuge": { + "account_number": "4570", + "account_type": "Expense Account" + }, + "sonstige Kfz-Kosten": { + "account_number": "4580", + "account_type": "Expense Account" + } + }, + "Personalkosten": { + "is_group": 1, + "Gehälter": { + "account_number": "4120", + "account_type": "Expense Account" + }, + "gesetzliche soziale Aufwendungen": { + "account_number": "4130", + "account_type": "Expense Account" + }, + "Aufwendungen für Altersvorsorge": { + "account_number": "4165", + "account_type": "Expense Account" + }, + "Vermögenswirksame Leistungen": { + "account_number": "4170", + "account_type": "Expense Account" + }, + "Aushilfslöhne": { + "account_number": "4190", + "account_type": "Expense Account" + } + }, + "Raumkosten": { + "is_group": 1, + "Miete und Nebenkosten": { + "account_number": "4210", + "account_type": "Expense Account" + }, + "Gas, Wasser, Strom (Verwaltung, Vertrieb)": { + "account_number": "4240", + "account_type": "Expense Account" + }, + "Reinigung": { + "account_number": "4250", + "account_type": "Expense Account" + } + }, + "Reparatur/Instandhaltung": { + "is_group": 1, + "Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": { + "account_number": "4805", + "account_type": "Expense Account" + } + }, + "Versicherungsbeiträge": { + "is_group": 1, + "Versicherungen": { + "account_number": "4360", + "account_type": "Expense Account" + }, + "Beiträge": { + "account_number": "4380", + "account_type": "Expense Account" + }, + "sonstige Ausgaben": { + "account_number": "4390", + "account_type": "Expense Account" + }, + "steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": { + "account_number": "4396", + "account_type": "Expense Account" + } + }, + "Werbe-/Reisekosten": { + "is_group": 1, + "Werbekosten": { + "account_number": "4610", + "account_type": "Expense Account" + }, + "Aufmerksamkeiten": { + "account_number": "4653", + "account_type": "Expense Account" + }, + "nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": { + "account_number": "4665", + "account_type": "Expense Account" + }, + "Reisekosten Unternehmer": { + "account_number": "4670", + "account_type": "Expense Account" + } + }, + "verschiedene Kosten": { + "is_group": 1, + "Porto": { + "account_number": "4910", + "account_type": "Expense Account" + }, + "Telekom": { + "account_number": "4920", + "account_type": "Expense Account" + }, + "Mobilfunk D2": { + "account_number": "4921", + "account_type": "Expense Account" + }, + "Internet": { + "account_number": "4922", + "account_type": "Expense Account" + }, + "Bürobedarf": { + "account_number": "4930", + "account_type": "Expense Account" + }, + "Zeitschriften, Bücher": { + "account_number": "4940", + "account_type": "Expense Account" + }, + "Fortbildungskosten": { + "account_number": "4945", + "account_type": "Expense Account" + }, + "Buchführungskosten": { + "account_number": "4955", + "account_type": "Expense Account" + }, + "Abschluß- u. Prüfungskosten": { + "account_number": "4957", + "account_type": "Expense Account" + }, + "Nebenkosten des Geldverkehrs": { + "account_number": "4970", + "account_type": "Expense Account" + }, + "Werkzeuge und Kleingeräte": { + "account_number": "4985", + "account_type": "Expense Account" + } + }, + "Zinsaufwendungen": { + "is_group": 1, + "Zinsaufwendungen für kurzfristige Verbindlichkeiten": { + "account_number": "2110", + "account_type": "Expense Account" + }, + "Zinsaufwendungen für KFZ Finanzierung": { + "account_number": "2121", + "account_type": "Expense Account" + } + } + }, + "Anfangsbestand 9": { + "is_group": 1, + "root_type": "Equity", + "Saldenvortragskonten": { + "is_group": 1, + "Saldenvortrag Sachkonten": { + "account_number": "9000" + }, + "Saldenvorträge Debitoren": { + "account_number": "9008" + }, + "Saldenvorträge Kreditoren": { + "account_number": "9009" + } + } + }, + "Privatkonten 1": { + "is_group": 1, + "root_type": "Equity", + "Privatentnahmen/-einlagen": { + "is_group": 1, + "Privatentnahme allgemein": { + "account_number": "1800" + }, + "Privatsteuern": { + "account_number": "1810" + }, + "Sonderausgaben beschränkt abzugsfähig": { + "account_number": "1820" + }, + "Sonderausgaben unbeschränkt abzugsfähig": { + "account_number": "1830" + }, + "Außergewöhnliche Belastungen": { + "account_number": "1850" + }, + "Privateinlagen": { + "account_number": "1890" + } + } + } + } } diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 1e2e2acd79a..3f985b640bf 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -56,7 +56,9 @@ "acc_frozen_upto", "column_break_25", "frozen_accounts_modifier", - "report_settings_sb" + "report_settings_sb", + "tab_break_dpet", + "show_balance_in_coa" ], "fields": [ { @@ -347,6 +349,17 @@ "fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldtype": "Check", "label": "Allow multi-currency invoices against single party account " + }, + { + "fieldname": "tab_break_dpet", + "fieldtype": "Tab Break", + "label": "Chart Of Accounts" + }, + { + "default": "1", + "fieldname": "show_balance_in_coa", + "fieldtype": "Check", + "label": "Show Balances in Chart Of Accounts" } ], "icon": "icon-cog", @@ -354,7 +367,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-11-27 21:49:52.538655", + "modified": "2023-01-02 12:07:42.434214", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py index d25016fe596..54ffe21a152 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py @@ -28,9 +28,14 @@ class InvalidDateError(frappe.ValidationError): class CostCenterAllocation(Document): + def __init__(self, *args, **kwargs): + super(CostCenterAllocation, self).__init__(*args, **kwargs) + self._skip_from_date_validation = False + def validate(self): self.validate_total_allocation_percentage() - self.validate_from_date_based_on_existing_gle() + if not self._skip_from_date_validation: + self.validate_from_date_based_on_existing_gle() self.validate_backdated_allocation() self.validate_main_cost_center() self.validate_child_cost_centers() diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 331adb4b8e3..b4df0a5270c 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -40,7 +40,7 @@ class Dunning(AccountsController): def on_cancel(self): if self.dunning_amount: - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def make_gl_entries(self): diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 30a32015f5d..21f27aedc51 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry']; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ea8b7d831b2..1de1b382b44 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -81,6 +81,7 @@ class JournalEntry(AccountsController): self.check_credit_limit() self.make_gl_entries() self.update_advance_paid() + self.update_asset_value() self.update_inter_company_jv() self.update_invoice_discounting() @@ -225,6 +226,29 @@ class JournalEntry(AccountsController): for d in to_remove: self.remove(d) + def update_asset_value(self): + if self.voucher_type != "Depreciation Entry": + return + + processed_assets = [] + + for d in self.get("accounts"): + if ( + d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets + ): + processed_assets.append(d.reference_name) + + asset = frappe.get_doc("Asset", d.reference_name) + + if asset.calculate_depreciation: + continue + + depr_value = d.debit or d.credit + + asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value) + + asset.set_status() + def update_inter_company_jv(self): if ( self.voucher_type == "Inter Company Journal Entry" @@ -283,20 +307,45 @@ class JournalEntry(AccountsController): d.db_update() def unlink_asset_reference(self): + if self.voucher_type != "Depreciation Entry": + return + + processed_assets = [] + for d in self.get("accounts"): - if d.reference_type == "Asset" and d.reference_name: + if ( + d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets + ): + processed_assets.append(d.reference_name) + asset = frappe.get_doc("Asset", d.reference_name) - for row in asset.get("finance_books"): - depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) - for s in depr_schedule or []: - if s.journal_entry == self.name: - s.db_set("journal_entry", None) + if asset.calculate_depreciation: + je_found = False - row.value_after_depreciation += s.depreciation_amount - row.db_update() + for row in asset.get("finance_books"): + if je_found: + break - asset.set_status() + depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) + + for s in depr_schedule or []: + if s.journal_entry == self.name: + s.db_set("journal_entry", None) + + row.value_after_depreciation += s.depreciation_amount + row.db_update() + + asset.set_status() + + je_found = True + break + else: + depr_value = d.debit or d.credit + + asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value) + + asset.set_status() def unlink_inter_company_jv(self): if ( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 4a7a57b6275..3927ecae43d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -239,7 +239,7 @@ "depends_on": "paid_from", "fieldname": "paid_from_account_currency", "fieldtype": "Link", - "label": "Account Currency", + "label": "Account Currency (From)", "options": "Currency", "print_hide": 1, "read_only": 1, @@ -249,7 +249,7 @@ "depends_on": "paid_from", "fieldname": "paid_from_account_balance", "fieldtype": "Currency", - "label": "Account Balance", + "label": "Account Balance (From)", "options": "paid_from_account_currency", "print_hide": 1, "read_only": 1 @@ -272,7 +272,7 @@ "depends_on": "paid_to", "fieldname": "paid_to_account_currency", "fieldtype": "Link", - "label": "Account Currency", + "label": "Account Currency (To)", "options": "Currency", "print_hide": 1, "read_only": 1, @@ -282,7 +282,7 @@ "depends_on": "paid_to", "fieldname": "paid_to_account_balance", "fieldtype": "Currency", - "label": "Account Balance", + "label": "Account Balance (To)", "options": "paid_to_account_currency", "print_hide": 1, "read_only": 1 @@ -304,7 +304,7 @@ { "fieldname": "source_exchange_rate", "fieldtype": "Float", - "label": "Exchange Rate", + "label": "Source Exchange Rate", "precision": "9", "print_hide": 1, "reqd": 1 @@ -334,7 +334,7 @@ { "fieldname": "target_exchange_rate", "fieldtype": "Float", - "label": "Exchange Rate", + "label": "Target Exchange Rate", "precision": "9", "print_hide": 1, "reqd": 1 @@ -633,14 +633,14 @@ "depends_on": "eval:doc.party_type == 'Supplier'", "fieldname": "purchase_taxes_and_charges_template", "fieldtype": "Link", - "label": "Taxes and Charges Template", + "label": "Purchase Taxes and Charges Template", "options": "Purchase Taxes and Charges Template" }, { "depends_on": "eval: doc.party_type == 'Customer'", "fieldname": "sales_taxes_and_charges_template", "fieldtype": "Link", - "label": "Taxes and Charges Template", + "label": "Sales Taxes and Charges Template", "options": "Sales Taxes and Charges Template" }, { @@ -733,7 +733,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-12-08 16:25:43.824051", + "modified": "2023-02-14 04:52:30.478523", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 12c0b7a7bf7..675a3287fa4 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -69,6 +69,10 @@ class PaymentReconciliation(Document): def get_jv_entries(self): condition = self.get_conditions() + + if self.get("cost_center"): + condition += f" and t2.cost_center = '{self.cost_center}' " + dr_or_cr = ( "credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" @@ -230,7 +234,7 @@ class PaymentReconciliation(Document): def allocate_entries(self, args): self.validate_entries() - invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices")) + invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments")) default_exchange_gain_loss_account = frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" ) @@ -249,6 +253,9 @@ class PaymentReconciliation(Document): pay["amount"] = 0 inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number")) + if pay.get("reference_type") in ["Sales Invoice", "Purchase Invoice"]: + pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name")) + res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) res.difference_account = default_exchange_gain_loss_account res.exchange_rate = inv.get("exchange_rate") @@ -403,13 +410,21 @@ class PaymentReconciliation(Document): if not self.get("payments"): frappe.throw(_("No records found in the Payments table")) - def get_invoice_exchange_map(self, invoices): + def get_invoice_exchange_map(self, invoices, payments): sales_invoices = [ d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice" ] + + sales_invoices.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Sales Invoice"] + ) purchase_invoices = [ d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice" ] + purchase_invoices.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Purchase Invoice"] + ) + invoice_exchange_map = frappe._dict() if sales_invoices: diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 2ba90b4da9f..f9dda0593b0 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -473,6 +473,11 @@ class TestPaymentReconciliation(FrappeTestCase): invoices = [x.as_dict() for x in pr.get("invoices")] payments = [x.as_dict() for x in pr.get("payments")] pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Cr Note and Invoice are of the same currency. There shouldn't any difference amount. + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() pr.get_unreconciled_entries() @@ -506,6 +511,11 @@ class TestPaymentReconciliation(FrappeTestCase): payments = [x.as_dict() for x in pr.get("payments")] pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocation[0].allocated_amount = allocated_amount + + # Cr Note and Invoice are of the same currency. There shouldn't any difference amount. + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() # assert outstanding @@ -747,6 +757,73 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("payments")), 0) + def test_cost_center_filter_on_vouchers(self): + """ + Test Cost Center filter is applied on Invoices, Payment Entries and Journals + """ + transaction_date = nowdate() + rate = 100 + + # 'Main - PR' Cost Center + si1 = self.create_sales_invoice( + qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True + ) + si1.cost_center = self.main_cc.name + si1.submit() + + pe1 = self.create_payment_entry(posting_date=transaction_date, amount=rate) + pe1.cost_center = self.main_cc.name + pe1 = pe1.save().submit() + + je1 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date) + je1.accounts[0].cost_center = self.main_cc.name + je1.accounts[1].cost_center = self.main_cc.name + je1.accounts[1].party_type = "Customer" + je1.accounts[1].party = self.customer + je1 = je1.save().submit() + + # 'Sub - PR' Cost Center + si2 = self.create_sales_invoice( + qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True + ) + si2.cost_center = self.sub_cc.name + si2.submit() + + pe2 = self.create_payment_entry(posting_date=transaction_date, amount=rate) + pe2.cost_center = self.sub_cc.name + pe2 = pe2.save().submit() + + je2 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date) + je2.accounts[0].cost_center = self.sub_cc.name + je2.accounts[1].cost_center = self.sub_cc.name + je2.accounts[1].party_type = "Customer" + je2.accounts[1].party = self.customer + je2 = je2.save().submit() + + pr = self.create_payment_reconciliation() + pr.cost_center = self.main_cc.name + + pr.get_unreconciled_entries() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si1.name) + self.assertEqual(len(pr.get("payments")), 2) + payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] + self.assertCountEqual(payment_vouchers, [pe1.name, je1.name]) + + # Change cost center + pr.cost_center = self.sub_cc.name + + pr.get_unreconciled_entries() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si2.name) + self.assertEqual(len(pr.get("payments")), 2) + payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] + self.assertCountEqual(payment_vouchers, [je2.name, pe2.name]) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 4fc12dbc167..52eb29b3bbd 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -45,21 +45,20 @@ class PaymentRequest(Document): frappe.throw(_("To create a Payment Request reference document is required")) def validate_payment_request_amount(self): - existing_payment_request_amount = get_existing_payment_request_amount( - self.reference_doctype, self.reference_name + existing_payment_request_amount = flt( + get_existing_payment_request_amount(self.reference_doctype, self.reference_name) ) - if existing_payment_request_amount: - ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart": - ref_amount = get_amount(ref_doc, self.payment_account) + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": + ref_amount = get_amount(ref_doc, self.payment_account) - if existing_payment_request_amount + flt(self.grand_total) > ref_amount: - frappe.throw( - _("Total Payment Request amount cannot be greater than {0} amount").format( - self.reference_doctype - ) + if existing_payment_request_amount + flt(self.grand_total) > ref_amount: + frappe.throw( + _("Total Payment Request amount cannot be greater than {0} amount").format( + self.reference_doctype ) + ) def validate_currency(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index ce9ce647db0..a63039e0e3a 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -472,7 +472,7 @@ "description": "If rate is zero them item will be treated as \"Free Item\"", "fieldname": "free_item_rate", "fieldtype": "Currency", - "label": "Rate" + "label": "Free Item Rate" }, { "collapsible": 1, @@ -608,7 +608,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2022-10-13 19:05:35.056304", + "modified": "2023-02-14 04:53:34.887358", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 6281400fbcc..54caf6f8b02 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1426,6 +1426,7 @@ }, { "default": "0", + "depends_on": "apply_tds", "fieldname": "tax_withholding_net_total", "fieldtype": "Currency", "hidden": 1, @@ -1435,12 +1436,13 @@ "read_only": 1 }, { + "depends_on": "apply_tds", "fieldname": "base_tax_withholding_net_total", "fieldtype": "Currency", "hidden": 1, "label": "Base Tax Withholding Net Total", "no_copy": 1, - "options": "currency", + "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 }, @@ -1554,7 +1556,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:37:38.142688", + "modified": "2023-01-28 19:18:56.586321", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 4729d9c3db9..2f4e45e6187 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1776,6 +1776,8 @@ "width": "50%" }, { + "fetch_from": "sales_partner.commission_rate", + "fetch_if_empty": 1, "fieldname": "commission_rate", "fieldtype": "Float", "hide_days": 1, @@ -2141,7 +2143,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-12-12 18:34:33.409895", + "modified": "2023-01-28 19:45:47.538163", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index baeed03a0f7..01cfb58decf 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -211,7 +211,13 @@ def set_address_details( else: party_details.update(get_company_address(company)) - if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]: + if doctype and doctype in [ + "Delivery Note", + "Sales Invoice", + "Sales Order", + "Quotation", + "POS Invoice", + ]: if party_details.company_address: party_details.update( get_fetch_values(doctype, "company_address", party_details.company_address) @@ -544,7 +550,7 @@ 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": due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days)) else: - due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months)) + due_date = max(due_date, get_last_day(add_months(due_date, term.credit_months))) return due_date diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 43b95dca80e..58276970232 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -135,6 +135,34 @@ def get_assets(filters): where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != '' group by a.asset_category union + SELECT a.asset_category, + ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then + gle.debit + else + 0 + end), 0) as accumulated_depreciation_as_on_from_date, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then + gle.debit + else + 0 + end), 0) as depreciation_eliminated_during_the_period, + ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s + and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then + gle.debit + else + 0 + end), 0) as depreciation_amount_during_the_period + from `tabGL Entry` gle + join `tabAsset` a on + gle.against_voucher = a.name + join `tabAsset Category Account` aca on + aca.parent = a.asset_category and aca.company_name = %(company)s + join `tabCompany` company on + company.name = %(company)s + where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + group by a.asset_category + union SELECT a.asset_category, ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then 0 diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index fc231279359..27b84c4e776 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -526,7 +526,7 @@ def get_columns(filters): "options": "GL Entry", "hidden": 1, }, - {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, { "label": _("Account"), "fieldname": "account", @@ -538,13 +538,13 @@ def get_columns(filters): "label": _("Debit ({0})").format(currency), "fieldname": "debit", "fieldtype": "Float", - "width": 100, + "width": 130, }, { "label": _("Credit ({0})").format(currency), "fieldname": "credit", "fieldtype": "Float", - "width": 100, + "width": 130, }, { "label": _("Balance ({0})").format(currency), diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 615804ef623..e89d42977be 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -50,6 +50,20 @@ frappe.query_reports["Gross Profit"] = { "fieldtype": "Link", "options": "Sales Person" }, + { + "fieldname": "warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function () { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: [ + ["Warehouse", "company", "=", company] + ] + }; + }, + }, ], "tree": true, "name_field": "parent", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 130b7150fba..e23265b5e79 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -655,10 +655,35 @@ class GrossProfitGenerator(object): return self.calculate_buying_amount_from_sle( row, my_sle, parenttype, parent, item_row, item_code ) + elif row.sales_order and row.so_detail: + incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code) + if incoming_amount: + return incoming_amount else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) - return 0.0 + return flt(row.qty) * self.get_average_buying_rate(row, item_code) + + def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code): + from frappe.query_builder.functions import Sum + + delivery_note = frappe.qb.DocType("Delivery Note") + delivery_note_item = frappe.qb.DocType("Delivery Note Item") + + query = ( + frappe.qb.from_(delivery_note) + .inner_join(delivery_note_item) + .on(delivery_note.name == delivery_note_item.parent) + .select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) + .where(delivery_note.docstatus == 1) + .where(delivery_note_item.item_code == item_code) + .where(delivery_note_item.against_sales_order == sales_order) + .where(delivery_note_item.so_detail == so_detail) + .groupby(delivery_note_item.item_code) + ) + + incoming_amount = query.run() + return flt(incoming_amount[0][0]) if incoming_amount else 0 def get_average_buying_rate(self, row, item_code): args = row @@ -750,6 +775,13 @@ class GrossProfitGenerator(object): if self.filters.get("item_code"): conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" + if self.filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + if warehouse_details: + conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)" + self.si_list = frappe.db.sql( """ select @@ -760,7 +792,8 @@ class GrossProfitGenerator(object): `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, - `tabSales Invoice Item`.brand, `tabSales Invoice Item`.dn_detail, + `tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail, + `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return, diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index fa11a41df4a..21681bef5b5 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -302,3 +302,82 @@ class TestGrossProfit(FrappeTestCase): columns, data = execute(filters=filters) self.assertGreater(len(data), 0) + + def test_order_connected_dn_and_inv(self): + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + """ + Test gp calculation when invoice and delivery note aren't directly connected. + SO -- INV + | + DN + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=3, + basic_rate=100, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": item.item_code, + "s_warehouse": item.s_warehouse, + "t_warehouse": item.t_warehouse, + "qty": 10, + "basic_rate": 200, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + so = make_sales_order( + customer=self.customer, + company=self.company, + warehouse=self.warehouse, + item=self.item, + qty=4, + do_not_save=False, + do_not_submit=False, + ) + + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + + make_delivery_note(so.name).submit() + sinv = make_sales_invoice(so.name).submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 4.0, + "avg._selling_rate": 100.0, + "valuation_rate": 125.0, + "selling_amount": 400.0, + "buying_amount": 500.0, + "gross_profit": -100.0, + "gross_profit_%": -25.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry, gp_entry[0]) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a03de9e1940..2608c03ffe5 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1512,9 +1512,12 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa ref_doc = frappe.get_doc(voucher_type, voucher_no) # Didn't use db_set for optimisation purpose - ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] + ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0 frappe.db.set_value( - voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"] + voucher_type, + voucher_no, + "outstanding_amount", + outstanding["outstanding_in_account_currency"] or 0.0, ) ref_doc.set_status(update=True) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 8f5b85d1b2c..49513851361 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -209,23 +209,20 @@ frappe.ui.form.on('Asset', { return } - var x_intervals = [frm.doc.purchase_date]; + var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: 'Date' })]; var asset_values = [frm.doc.gross_purchase_amount]; - var last_depreciation_date = frm.doc.purchase_date; - if(frm.doc.opening_accumulated_depreciation) { - last_depreciation_date = frappe.datetime.add_months(frm.doc.next_depreciation_date, - -1*frm.doc.frequency_of_depreciation); + if(frm.doc.calculate_depreciation) { + if(frm.doc.opening_accumulated_depreciation) { + var depreciation_date = frappe.datetime.add_months( + frm.doc.finance_books[0].depreciation_start_date, + -1 * frm.doc.finance_books[0].frequency_of_depreciation + ); + x_intervals.push(frappe.format(depreciation_date, { fieldtype: 'Date' })); + asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); + } - x_intervals.push(last_depreciation_date); - asset_values.push(flt(frm.doc.gross_purchase_amount) - - flt(frm.doc.opening_accumulated_depreciation)); - } - - let depr_schedule = []; - - if (frm.doc.finance_books.length == 1) { - depr_schedule = (await frappe.call( + let depr_schedule = (await frappe.call( "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", { asset_name: frm.doc.name, @@ -233,27 +230,41 @@ frappe.ui.form.on('Asset', { finance_book: frm.doc.finance_books[0].finance_book || null } )).message; + + $.each(depr_schedule || [], function(i, v) { + x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' })); + var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount')); + if(v.journal_entry) { + asset_values.push(asset_value); + } else { + if (in_list(["Scrapped", "Sold"], frm.doc.status)) { + asset_values.push(null); + } else { + asset_values.push(asset_value) + } + } + }); + } else { + if(frm.doc.opening_accumulated_depreciation) { + x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' })); + asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); + } + + let depr_entries = (await frappe.call({ + method: "get_manual_depreciation_entries", + doc: frm.doc, + })).message; + + $.each(depr_entries || [], function(i, v) { + x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' })); + let last_asset_value = asset_values[asset_values.length - 1] + asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount'))); + }); } - $.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) - } - } - }); - if(in_list(["Scrapped", "Sold"], frm.doc.status)) { - x_intervals.push(frm.doc.disposal_date); + x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: 'Date' })); asset_values.push(0); - last_depreciation_date = frm.doc.disposal_date; } frm.dashboard.render_graph({ diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 8a64a953172..ea575fd71f1 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -509,9 +509,15 @@ "group": "Depreciation", "link_doctype": "Asset Depreciation Schedule", "link_fieldname": "asset" + }, + { + "group": "Journal Entry", + "link_doctype": "Journal Entry", + "link_fieldname": "reference_name", + "table_fieldname": "accounts" } ], - "modified": "2023-01-17 00:25:30.387242", + "modified": "2023-02-02 00:03:11.706427", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index df05d5e6325..e1d58a0264c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -36,7 +36,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched get_depr_schedule, make_draft_asset_depr_schedules, make_draft_asset_depr_schedules_if_not_present, - set_draft_asset_depr_schedule_details, update_draft_asset_depr_schedules, ) from erpnext.controllers.accounts_controller import AccountsController @@ -240,17 +239,6 @@ class Asset(AccountsController): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - def _get_value_after_depreciation(self, finance_book): - # value_after_depreciation - current Asset value - if self.docstatus == 1 and finance_book.value_after_depreciation: - value_after_depreciation = flt(finance_book.value_after_depreciation) - else: - value_after_depreciation = flt(self.gross_purchase_amount) - flt( - self.opening_accumulated_depreciation - ) - - return value_after_depreciation - # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False @@ -392,18 +380,23 @@ class Asset(AccountsController): movement.cancel() def delete_depreciation_entries(self): - for row in self.get("finance_books"): - depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book) + if self.calculate_depreciation: + for row in self.get("finance_books"): + depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book) - for d in depr_schedule or []: - if d.journal_entry: - frappe.get_doc("Journal Entry", d.journal_entry).cancel() - d.db_set("journal_entry", None) + for d in depr_schedule or []: + if d.journal_entry: + frappe.get_doc("Journal Entry", d.journal_entry).cancel() + else: + depr_entries = self.get_manual_depreciation_entries() - self.db_set( - "value_after_depreciation", - (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)), - ) + for depr_entry in depr_entries or []: + frappe.get_doc("Journal Entry", depr_entry.name).cancel() + + self.db_set( + "value_after_depreciation", + (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)), + ) def set_status(self, status=None): """Get and update status""" @@ -420,11 +413,14 @@ class Asset(AccountsController): if self.journal_entry_for_scrap: status = "Scrapped" - elif self.finance_books: - idx = self.get_default_finance_book_idx() or 0 + else: + expected_value_after_useful_life = 0 + value_after_depreciation = self.value_after_depreciation - expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life - value_after_depreciation = self.finance_books[idx].value_after_depreciation + if self.calculate_depreciation: + idx = self.get_default_finance_book_idx() or 0 + expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life + value_after_depreciation = self.finance_books[idx].value_after_depreciation if flt(value_after_depreciation) <= expected_value_after_useful_life: status = "Fully Depreciated" @@ -434,6 +430,19 @@ class Asset(AccountsController): status = "Cancelled" return status + def get_value_after_depreciation(self, finance_book=None): + if not self.calculate_depreciation: + return flt(self.value_after_depreciation, self.precision("gross_purchase_amount")) + + if not finance_book: + return flt( + self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount") + ) + + for row in self.get("finance_books"): + if finance_book == row.finance_book: + return flt(row.value_after_depreciation, self.precision("gross_purchase_amount")) + def get_default_finance_book_idx(self): if not self.get("default_finance_book") and self.company: self.default_finance_book = erpnext.get_default_finance_book(self.company) @@ -443,6 +452,44 @@ class Asset(AccountsController): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 + @frappe.whitelist() + def get_manual_depreciation_entries(self): + (_, _, depreciation_expense_account) = get_depreciation_accounts(self) + + gle = frappe.qb.DocType("GL Entry") + + records = ( + frappe.qb.from_(gle) + .select(gle.voucher_no.as_("name"), gle.debit.as_("value"), gle.posting_date) + .where(gle.against_voucher == self.name) + .where(gle.account == depreciation_expense_account) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .orderby(gle.posting_date) + .orderby(gle.creation) + ).run(as_dict=True) + + return records + + @erpnext.allow_regional + def get_depreciation_amount(self, depreciable_value, fb_row): + if fb_row.depreciation_method in ("Straight Line", "Manual"): + # if the Depreciation Schedule is being prepared for the first time + if not self.flags.increase_in_asset_life: + depreciation_amount = ( + flt(self.gross_purchase_amount) - flt(fb_row.expected_value_after_useful_life) + ) / flt(fb_row.total_number_of_depreciations) + + # if the Depreciation Schedule is being modified after Asset Repair + else: + depreciation_amount = ( + flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life) + ) / (date_diff(self.to_date, self.available_for_use_date) / 365) + else: + depreciation_amount = flt(depreciable_value * (flt(fb_row.rate_of_depreciation) / 100)) + + return depreciation_amount + def validate_make_gl_entry(self): purchase_document = self.get_purchase_document() if not purchase_document: @@ -603,7 +650,6 @@ def update_maintenance_status(): def make_post_gl_entry(): - asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"]) for asset_category in asset_categories: @@ -756,7 +802,7 @@ def make_journal_entry(asset_name): depreciation_expense_account, ) = get_depreciation_accounts(asset) - depreciation_cost_center, depreciation_series = frappe.db.get_value( + depreciation_cost_center, depreciation_series = frappe.get_cached_value( "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] ) depreciation_cost_center = asset.cost_center or depreciation_cost_center @@ -821,6 +867,13 @@ def is_cwip_accounting_enabled(asset_category): return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) +@frappe.whitelist() +def get_asset_value_after_depreciation(asset_name, finance_book=None): + asset = frappe.get_doc("Asset", asset_name) + + return asset.get_value_after_depreciation(finance_book) + + def get_total_days(date, frequency): period_start_date = add_months(date, cint(frequency) * -1) @@ -886,7 +939,7 @@ def update_existing_asset(asset, remaining_qty, new_asset_name): ) new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, asset, row) + new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, row) accumulated_depreciation = 0 @@ -938,7 +991,7 @@ def create_new_asset_after_split(asset, split_qty): ) new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, new_asset, row) + new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row) accumulated_depreciation = 0 diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 5337fd64eea..fb6e174fbae 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,7 +4,17 @@ import frappe from frappe import _ -from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today +from frappe.utils import ( + add_months, + cint, + flt, + get_last_day, + get_link_to_form, + getdate, + is_last_day_of_the_month, + nowdate, + today, +) from frappe.utils.user import get_users_with_role from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -158,7 +168,7 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None): row.value_after_depreciation -= d.depreciation_amount row.db_update() - frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful") + asset.db_set("depr_entry_posting_status", "Successful") asset.set_status() @@ -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) ) + if is_last_day_of_the_month(row.depreciation_start_date): + orginal_schedule_date = get_last_day(orginal_schedule_date) + if orginal_schedule_date == posting_date_of_disposal: return True @@ -520,18 +533,8 @@ def get_asset_details(asset, finance_book=None): disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center - idx = 1 - if finance_book: - for d in asset.finance_books: - if d.finance_book == finance_book: - idx = d.idx - break + value_after_depreciation = asset.get_value_after_depreciation(finance_book) - value_after_depreciation = ( - asset.finance_books[idx - 1].value_after_depreciation - if asset.calculate_depreciation - else asset.value_after_depreciation - ) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) return ( diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 51a2b528979..9a152638f90 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -16,6 +16,7 @@ from frappe.utils import ( nowdate, ) +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.assets.doctype.asset.asset import ( make_sales_invoice, @@ -28,7 +29,6 @@ from erpnext.assets.doctype.asset.depreciation import ( scrap_asset, ) from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - clear_depr_schedule, get_asset_depr_schedule_doc, get_depr_schedule, ) @@ -924,11 +924,6 @@ class TestDepreciationBasics(AssetSetup): def test_get_depreciation_amount(self): """Tests if get_depreciation_amount() returns the right value.""" - - from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - get_depreciation_amount, - ) - asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31") asset.calculate_depreciation = 1 @@ -943,7 +938,7 @@ 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) 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") - clear_depr_schedule(asset_depr_schedule_doc) + asset_depr_schedule_doc.clear_depr_schedule() self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1) @@ -1308,19 +1303,19 @@ class TestDepreciationBasics(AssetSetup): asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc( asset.name, "Active", "Test Finance Book 1" ) - clear_depr_schedule(asset_depr_schedule_doc_1) + asset_depr_schedule_doc_1.clear_depr_schedule() self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3) asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc( asset.name, "Active", "Test Finance Book 2" ) - clear_depr_schedule(asset_depr_schedule_doc_2) + asset_depr_schedule_doc_2.clear_depr_schedule() self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3) asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc( asset.name, "Active", "Test Finance Book 3" ) - clear_depr_schedule(asset_depr_schedule_doc_3) + asset_depr_schedule_doc_3.clear_depr_schedule() self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0) def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): @@ -1503,6 +1498,36 @@ class TestDepreciationBasics(AssetSetup): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date)) + def test_manual_depreciation_for_existing_asset(self): + asset = create_asset( + item_code="Macbook Pro", + is_existing_asset=1, + purchase_date="2020-01-30", + available_for_use_date="2020-01-30", + submit=1, + ) + + self.assertEqual(asset.status, "Submitted") + self.assertEqual(asset.get("value_after_depreciation"), 100000) + + jv = make_journal_entry( + "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False + ) + for d in jv.accounts: + d.reference_type = "Asset" + d.reference_name = asset.name + jv.voucher_type = "Depreciation Entry" + jv.insert() + jv.submit() + + asset.reload() + self.assertEqual(asset.get("value_after_depreciation"), 99900) + + jv.cancel() + + asset.reload() + self.assertEqual(asset.get("value_after_depreciation"), 100000) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 821accf96ac..5b910dbb2e6 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -10,6 +10,7 @@ from frappe import _ from frappe.utils import cint, flt, get_link_to_form import erpnext +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.depreciation import ( depreciate_asset, get_gl_entries_on_asset_disposal, @@ -21,9 +22,6 @@ from erpnext.assets.doctype.asset_category.asset_category import get_asset_categ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( make_new_active_asset_depr_schedules_and_cancel_current_ones, ) -from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( - get_current_asset_value, -) from erpnext.controllers.stock_controller import StockController from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults @@ -261,7 +259,9 @@ class AssetCapitalization(StockController): for d in self.get("asset_items"): if d.asset: finance_book = d.get("finance_book") or self.get("finance_book") - d.current_asset_value = flt(get_current_asset_value(d.asset, finance_book=finance_book)) + d.current_asset_value = flt( + get_asset_value_after_depreciation(d.asset, finance_book=finance_book) + ) d.asset_value = get_value_after_depreciation_on_disposal_date( d.asset, self.posting_date, finance_book=finance_book ) @@ -713,7 +713,7 @@ def get_consumed_asset_details(args): if args.asset: out.current_asset_value = flt( - get_current_asset_value(args.asset, finance_book=args.finance_book) + get_asset_value_after_depreciation(args.asset, finance_book=args.finance_book) ) out.asset_value = get_value_after_depreciation_on_disposal_date( args.asset, args.posting_date, finance_book=args.finance_book diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 1446a6e7a2d..6f026625441 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -4,17 +4,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import ( - add_days, - add_months, - cint, - date_diff, - flt, - get_last_day, - is_last_day_of_the_month, -) - -import erpnext +from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month class AssetDepreciationSchedule(Document): @@ -83,7 +73,267 @@ class AssetDepreciationSchedule(Document): ) 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 = _get_value_after_depreciation_for_making_schedule(asset_doc, 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 _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row): + if asset_doc.docstatus == 1 and fb_row.value_after_depreciation: + value_after_depreciation = flt(fb_row.value_after_depreciation) + else: + value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt( + asset_doc.opening_accumulated_depreciation + ) + + return value_after_depreciation def make_draft_asset_depr_schedules_if_not_present(asset_doc): @@ -108,7 +358,7 @@ def make_draft_asset_depr_schedules(asset_doc): def make_draft_asset_depr_schedule(asset_doc, row): asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") - prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row) + asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) asset_depr_schedule_doc.insert() @@ -120,41 +370,11 @@ def update_draft_asset_depr_schedules(asset_doc): if not asset_depr_schedule_doc: continue - prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row) + asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) asset_depr_schedule_doc.save() -def prepare_draft_asset_depr_schedule_data( - asset_depr_schedule_doc, - asset_doc, - row, - date_of_disposal=None, - date_of_return=None, - update_asset_finance_book_row=True, -): - set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row) - make_depr_schedule( - asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row - ) - set_accumulated_depreciation(asset_depr_schedule_doc, row, date_of_disposal, date_of_return) - - -def set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row): - asset_depr_schedule_doc.asset = asset_doc.name - asset_depr_schedule_doc.finance_book = row.finance_book - asset_depr_schedule_doc.finance_book_id = row.idx - asset_depr_schedule_doc.opening_accumulated_depreciation = ( - asset_doc.opening_accumulated_depreciation - ) - asset_depr_schedule_doc.depreciation_method = row.depreciation_method - asset_depr_schedule_doc.total_number_of_depreciations = row.total_number_of_depreciations - asset_depr_schedule_doc.frequency_of_depreciation = row.frequency_of_depreciation - asset_depr_schedule_doc.rate_of_depreciation = row.rate_of_depreciation - asset_depr_schedule_doc.expected_value_after_useful_life = row.expected_value_after_useful_life - asset_depr_schedule_doc.status = "Draft" - - def convert_draft_asset_depr_schedules_into_active(asset_doc): for row in asset_doc.get("finance_books"): asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) @@ -192,8 +412,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) - make_depr_schedule(new_asset_depr_schedule_doc, asset_doc, row, date_of_disposal) - set_accumulated_depreciation(new_asset_depr_schedule_doc, row, date_of_disposal, date_of_return) + new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal) + new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return) new_asset_depr_schedule_doc.notes = notes @@ -208,8 +428,7 @@ def get_temp_asset_depr_schedule_doc( ): asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") - prepare_draft_asset_depr_schedule_data( - asset_depr_schedule_doc, + asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( asset_doc, row, date_of_disposal, @@ -220,21 +439,6 @@ def get_temp_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() def get_depr_schedule(asset_name, status, finance_book=None): asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book) @@ -256,261 +460,16 @@ def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): return asset_depr_schedule_doc -def make_depr_schedule( - asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True -): - if row.depreciation_method != "Manual" and not asset_depr_schedule_doc.get( - "depreciation_schedule" - ): - asset_depr_schedule_doc.depreciation_schedule = [] +def get_asset_depr_schedule_name(asset_name, status, finance_book=None): + finance_book_filter = ["finance_book", "is", "not set"] + if finance_book: + finance_book_filter = ["finance_book", "=", finance_book] - if not asset_doc.available_for_use_date: - return - - start = clear_depr_schedule(asset_depr_schedule_doc) - - _make_depr_schedule( - asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row + return frappe.db.get_value( + doctype="Asset Depreciation Schedule", + filters=[ + ["asset", "=", asset_name], + finance_book_filter, + ["status", "=", status], + ], ) - - -def clear_depr_schedule(asset_depr_schedule_doc): - start = 0 - num_of_depreciations_completed = 0 - depr_schedule = [] - - for schedule in asset_depr_schedule_doc.get("depreciation_schedule"): - if schedule.journal_entry: - num_of_depreciations_completed += 1 - depr_schedule.append(schedule) - else: - start = num_of_depreciations_completed - break - - asset_depr_schedule_doc.depreciation_schedule = depr_schedule - - return start - - -def _make_depr_schedule( - asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row -): - asset_doc.validate_asset_finance_books(row) - - value_after_depreciation = asset_doc._get_value_after_depreciation(row) - row.value_after_depreciation = value_after_depreciation - - if update_asset_finance_book_row: - row.db_update() - - number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( - asset_doc.number_of_depreciations_booked - ) - - has_pro_rata = asset_doc.check_is_pro_rata(row) - if has_pro_rata: - number_of_pending_depreciations += 1 - - skip_row = False - should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date) - - for n in range(start, number_of_pending_depreciations): - # If depreciation is already completed (for double declining balance) - if skip_row: - continue - - depreciation_amount = get_depreciation_amount(asset_doc, value_after_depreciation, row) - - if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation)) - - if should_get_last_day: - schedule_date = get_last_day(schedule_date) - - # schedule date will be a year later from start date - # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, -row.frequency_of_depreciation + 1) - - # if asset is being sold or scrapped - if date_of_disposal: - from_date = asset_doc.available_for_use_date - if asset_depr_schedule_doc.depreciation_schedule: - from_date = asset_depr_schedule_doc.depreciation_schedule[-1].schedule_date - - depreciation_amount, days, months = asset_doc.get_pro_rata_amt( - row, depreciation_amount, from_date, date_of_disposal - ) - - if depreciation_amount > 0: - add_depr_schedule_row( - asset_depr_schedule_doc, - date_of_disposal, - depreciation_amount, - row.depreciation_method, - ) - - break - - # For first row - if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0: - from_date = add_days( - asset_doc.available_for_use_date, -1 - ) # needed to calc depr amount for available_for_use_date too - depreciation_amount, days, months = asset_doc.get_pro_rata_amt( - row, depreciation_amount, from_date, row.depreciation_start_date - ) - - # For first depr schedule date will be the start date - # so monthly schedule date is calculated by removing - # month difference between use date and start date - monthly_schedule_date = add_months(row.depreciation_start_date, -months + 1) - - # For last row - elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - if not asset_doc.flags.increase_in_asset_life: - # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission - asset_doc.to_date = add_months( - asset_doc.available_for_use_date, - (n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), - ) - - depreciation_amount_without_pro_rata = depreciation_amount - - depreciation_amount, days, months = asset_doc.get_pro_rata_amt( - row, depreciation_amount, schedule_date, asset_doc.to_date - ) - - depreciation_amount = get_adjusted_depreciation_amount( - asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount - ) - - monthly_schedule_date = add_months(schedule_date, 1) - schedule_date = add_days(schedule_date, days) - last_schedule_date = schedule_date - - if not depreciation_amount: - continue - value_after_depreciation -= flt( - depreciation_amount, asset_doc.precision("gross_purchase_amount") - ) - - # Adjust depreciation amount in the last period based on the expected value after useful life - if row.expected_value_after_useful_life and ( - ( - n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != row.expected_value_after_useful_life - ) - or value_after_depreciation < row.expected_value_after_useful_life - ): - depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life - skip_row = True - - if depreciation_amount > 0: - add_depr_schedule_row( - asset_depr_schedule_doc, - schedule_date, - depreciation_amount, - row.depreciation_method, - ) - - -# to ensure that final accumulated depreciation amount is accurate -def get_adjusted_depreciation_amount( - asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row -): - if not asset_depr_schedule_doc.opening_accumulated_depreciation: - depreciation_amount_for_first_row = get_depreciation_amount_for_first_row( - asset_depr_schedule_doc - ) - - if ( - depreciation_amount_for_first_row + depreciation_amount_for_last_row - != depreciation_amount_without_pro_rata - ): - depreciation_amount_for_last_row = ( - depreciation_amount_without_pro_rata - depreciation_amount_for_first_row - ) - - return depreciation_amount_for_last_row - - -def get_depreciation_amount_for_first_row(asset_depr_schedule_doc): - return asset_depr_schedule_doc.get("depreciation_schedule")[0].depreciation_amount - - -@erpnext.allow_regional -def get_depreciation_amount(asset_doc, depreciable_value, row): - if row.depreciation_method in ("Straight Line", "Manual"): - # if the Depreciation Schedule is being prepared for the first time - if not asset_doc.flags.increase_in_asset_life: - depreciation_amount = ( - flt(asset_doc.gross_purchase_amount) - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations) - - # if the Depreciation Schedule is being modified after Asset Repair - else: - depreciation_amount = ( - flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) - ) / (date_diff(asset_doc.to_date, asset_doc.available_for_use_date) / 365) - else: - depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) - - return depreciation_amount - - -def add_depr_schedule_row( - asset_depr_schedule_doc, - schedule_date, - depreciation_amount, - depreciation_method, -): - asset_depr_schedule_doc.append( - "depreciation_schedule", - { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": depreciation_method, - }, - ) - - -def set_accumulated_depreciation( - asset_depr_schedule_doc, - row, - date_of_disposal=None, - date_of_return=None, - ignore_booked_entry=False, -): - straight_line_idx = [ - d.idx - for d in asset_depr_schedule_doc.get("depreciation_schedule") - if d.depreciation_method == "Straight Line" - ] - - accumulated_depreciation = flt(asset_depr_schedule_doc.opening_accumulated_depreciation) - value_after_depreciation = flt(row.value_after_depreciation) - - for i, d in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): - if ignore_booked_entry and d.journal_entry: - continue - - depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) - value_after_depreciation -= flt(depreciation_amount) - - # for the last row, if depreciation method = Straight Line - if ( - straight_line_idx - and i == max(straight_line_idx) - 1 - and not date_of_disposal - and not date_of_return - ): - depreciation_amount += flt( - value_after_depreciation - flt(row.expected_value_after_useful_life), - d.precision("depreciation_amount"), - ) - - d.depreciation_amount = depreciation_amount - accumulated_depreciation += d.depreciation_amount - d.accumulated_depreciation_amount = flt( - accumulated_depreciation, d.precision("accumulated_depreciation_amount") - ) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9a05a74ef9d..a7172a72c6f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -91,6 +91,9 @@ class AssetRepair(AccountsController): make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.asset_doc.save() + def after_delete(self): + frappe.get_doc("Asset", self.asset).set_status() + def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index ff72aa94b99..a9d0b257552 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -6,7 +6,10 @@ import unittest import frappe from frappe.utils import flt, nowdate -from erpnext.assets.doctype.asset.asset import get_asset_account +from erpnext.assets.doctype.asset.asset import ( + get_asset_account, + get_asset_value_after_depreciation, +) from erpnext.assets.doctype.asset.test_asset import ( create_asset, create_asset_data, @@ -109,20 +112,20 @@ class TestAssetRepair(unittest.TestCase): def test_increase_in_asset_value_due_to_stock_consumption(self): asset = create_asset(calculate_depreciation=1, submit=1) - initial_asset_value = get_asset_value(asset) + initial_asset_value = get_asset_value_after_depreciation(asset.name) asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1) asset.reload() - increase_in_asset_value = get_asset_value(asset) - initial_asset_value + increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): asset = create_asset(calculate_depreciation=1, submit=1) - initial_asset_value = get_asset_value(asset) + initial_asset_value = get_asset_value_after_depreciation(asset.name) asset_repair = create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) asset.reload() - increase_in_asset_value = get_asset_value(asset) - initial_asset_value + increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) def test_purchase_invoice(self): @@ -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): return asset.finance_books[0].total_number_of_depreciations diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index 36f510b18ee..ae0e1bda020 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -47,7 +47,7 @@ frappe.ui.form.on('Asset Value Adjustment', { set_current_asset_value: function(frm) { if (frm.doc.asset) { frm.call({ - method: "erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment.get_current_asset_value", + method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation", args: { asset: frm.doc.asset, finance_book: frm.doc.finance_book diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 6cfbe53cf6a..31d6ffab5fb 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -10,11 +10,10 @@ from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_asset_depr_schedule_doc, - get_depreciation_amount, - set_accumulated_depreciation, ) @@ -46,7 +45,7 @@ class AssetValueAdjustment(Document): def set_current_asset_value(self): if not self.current_asset_value and self.asset: - self.current_asset_value = get_current_asset_value(self.asset, self.finance_book) + self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book) def make_depreciation_entry(self): asset = frappe.get_doc("Asset", self.asset) @@ -163,7 +162,7 @@ class AssetValueAdjustment(Document): depreciation_amount = days * rate_per_day from_date = data.schedule_date else: - depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) + depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d) if depreciation_amount: value_after_depreciation -= flt(depreciation_amount) @@ -171,18 +170,9 @@ class AssetValueAdjustment(Document): d.db_update() - set_accumulated_depreciation(new_asset_depr_schedule_doc, d, ignore_booked_entry=True) + new_asset_depr_schedule_doc.set_accumulated_depreciation(d, ignore_booked_entry=True) for asset_data in depr_schedule: if not asset_data.journal_entry: asset_data.db_update() new_asset_depr_schedule_doc.submit() - - -@frappe.whitelist() -def get_current_asset_value(asset, finance_book=None): - cond = {"parent": asset, "parenttype": "Asset"} - if finance_book: - cond.update({"finance_book": finance_book}) - - return frappe.db.get_value("Asset Finance Book", cond, "value_after_depreciation") diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 03dcea96c53..0b3dcba024c 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -6,13 +6,11 @@ import unittest import frappe from frappe.utils import add_days, get_last_day, nowdate +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.test_asset import create_asset_data from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_asset_depr_schedule_doc, ) -from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( - get_current_asset_value, -) from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -46,7 +44,7 @@ class TestAssetValueAdjustment(unittest.TestCase): ) asset_doc.submit() - current_value = get_current_asset_value(asset_doc.name) + current_value = get_asset_value_after_depreciation(asset_doc.name) self.assertEqual(current_value, 100000.0) def test_asset_depreciation_value_adjustment(self): @@ -79,7 +77,7 @@ class TestAssetValueAdjustment(unittest.TestCase): first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") self.assertEquals(first_asset_depr_schedule.status, "Active") - current_value = get_current_asset_value(asset_doc.name) + current_value = get_asset_value_after_depreciation(asset_doc.name) adj_doc = make_asset_value_adjustment( asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 ) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index faffd1134d3..51a6a86e9f6 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum from frappe.utils import cstr, flt, formatdate, getdate from erpnext.accounts.report.financial_statements import ( @@ -11,6 +12,8 @@ from erpnext.accounts.report.financial_statements import ( get_period_list, validate_fiscal_year, ) +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation +from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts def execute(filters=None): @@ -85,6 +88,7 @@ def get_data(filters): "asset_name", "status", "department", + "company", "cost_center", "calculate_depreciation", "purchase_receipt", @@ -98,8 +102,21 @@ def get_data(filters): ] assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) + assets_linked_to_fb = frappe.db.get_all( + doctype="Asset Finance Book", + filters={"finance_book": filters.finance_book or ("is", "not set")}, + pluck="parent", + ) + for asset in assets_record: - asset_value = get_asset_value(asset, filters.finance_book) + if filters.finance_book: + if asset.asset_id not in assets_linked_to_fb: + continue + else: + if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb: + continue + + asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book) row = { "asset_id": asset.asset_id, "asset_name": asset.asset_name, @@ -110,7 +127,7 @@ def get_data(filters): or pi_supplier_map.get(asset.purchase_invoice), "gross_purchase_amount": asset.gross_purchase_amount, "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, - "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, + "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters), "available_for_use_date": asset.available_for_use_date, "location": asset.location, "asset_category": asset.asset_category, @@ -122,21 +139,6 @@ def get_data(filters): return data -def get_asset_value(asset, finance_book=None): - if not asset.calculate_depreciation: - return flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - - finance_book_filter = ["finance_book", "is", "not set"] - if finance_book: - finance_book_filter = ["finance_book", "=", finance_book] - - return frappe.db.get_value( - doctype="Asset Finance Book", - filters=[["parent", "=", asset.asset_id], finance_book_filter], - fieldname="value_after_depreciation", - ) - - def prepare_chart_data(data, filters): labels_values_map = {} date_field = frappe.scrub(filters.date_based_on) @@ -182,6 +184,15 @@ def prepare_chart_data(data, filters): } +def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters): + if asset.calculate_depreciation: + depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0 + else: + depr_amount = get_manual_depreciation_amount_of_asset(asset, filters) + + return flt(depr_amount, 2) + + def get_finance_book_value_map(filters): date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date @@ -203,6 +214,31 @@ def get_finance_book_value_map(filters): ) +def get_manual_depreciation_amount_of_asset(asset, filters): + date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date + + (_, _, depreciation_expense_account) = get_depreciation_accounts(asset) + + gle = frappe.qb.DocType("GL Entry") + + result = ( + frappe.qb.from_(gle) + .select(Sum(gle.debit)) + .where(gle.against_voucher == asset.asset_id) + .where(gle.account == depreciation_expense_account) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .where(gle.posting_date <= date) + ).run() + + if result and result[0] and result[0][0]: + depr_amount = result[0][0] + else: + depr_amount = 0 + + return depr_amount + + def get_purchase_receipt_supplier_map(): return frappe._dict( frappe.db.sql( diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py index 646dba51ce9..c673be89b3f 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py @@ -15,17 +15,6 @@ class TestBulkTransactionLog(unittest.TestCase): create_customer() create_item() - def test_for_single_record(self): - so_name = create_so() - transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") - data = frappe.db.get_list( - "Sales Invoice", - filters={"posting_date": date.today(), "customer": "Bulk Customer"}, - fields=["*"], - ) - if not data: - self.fail("No Sales Invoice Created !") - def test_entry_in_log(self): so_name = create_so() transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index e1dd6797815..29afc8476e4 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1221,6 +1221,7 @@ }, { "default": "0", + "depends_on": "apply_tds", "fieldname": "tax_withholding_net_total", "fieldtype": "Currency", "hidden": 1, @@ -1230,12 +1231,13 @@ "read_only": 1 }, { + "depends_on": "apply_tds", "fieldname": "base_tax_withholding_net_total", "fieldtype": "Currency", "hidden": 1, "label": "Base Tax Withholding Net Total", "no_copy": 1, - "options": "currency", + "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 }, @@ -1269,7 +1271,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-12-25 18:08:59.074182", + "modified": "2023-01-28 18:59:16.322824", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index f0360b27dc0..920486a78ef 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -10,6 +10,7 @@ from frappe.utils import add_days, flt, getdate, nowdate from frappe.utils.data import today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.party import get_due_date_from_template from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, @@ -685,6 +686,12 @@ class TestPurchaseOrder(FrappeTestCase): else: raise Exception + def test_default_payment_terms(self): + due_date = get_due_date_from_template( + "_Test Payment Term Template 1", "2023-02-03", None + ).strftime("%Y-%m-%d") + self.assertEqual(due_date, "2023-03-31") + def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): po = create_purchase_order(do_not_save=1) po.payment_terms_template = "_Test Payment Term Template" diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index a9f5afb2e98..2f0b7862a82 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -124,12 +124,11 @@ frappe.ui.form.on("Request for Quotation",{ frappe.urllib.get_full_url( "/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" + new URLSearchParams({ - doctype: frm.doc.doctype, name: frm.doc.name, supplier: data.supplier, print_format: data.print_format || "Standard", language: data.language || frappe.boot.lang, - letter_head: data.letter_head || frm.doc.letter_head || "", + letterhead: data.letter_head || frm.doc.letter_head || "", }).toString() ) ); diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 019d45b5683..bd65b0c805e 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -29,6 +29,7 @@ "message_for_supplier", "terms_section_break", "incoterm", + "named_place", "tc_name", "terms", "printing_settings", @@ -278,13 +279,19 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-11-17 17:26:33.770993", + "modified": "2023-01-31 23:22:06.684694", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 8e9ded98421..7927beb8233 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -3,6 +3,7 @@ import json +from typing import Optional import frappe from frappe import _ @@ -388,24 +389,26 @@ def create_rfq_items(sq_doc, supplier, data): @frappe.whitelist() -def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None): - # permissions get checked in `download_pdf` - if doc := get_rfq_doc(doctype, name, supplier): - download_pdf( - doctype, - name, - print_format, - doc=doc, - language=language, - letter_head=letter_head or None, - ) - - -def get_rfq_doc(doctype, name, supplier): +def get_pdf( + name: str, + supplier: str, + print_format: Optional[str] = None, + language: Optional[str] = None, + letterhead: Optional[str] = None, +): + doc = frappe.get_doc("Request for Quotation", name) if supplier: - doc = frappe.get_doc(doctype, name) doc.update_supplier_part_no(supplier) - return doc + + # permissions get checked in `download_pdf` + download_pdf( + doc.doctype, + doc.name, + print_format, + doc=doc, + language=language, + letterhead=letterhead or None, + ) @frappe.whitelist() diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 064b806e953..d250e6f18a9 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -8,6 +8,7 @@ from frappe.utils import nowdate from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( create_supplier_quotation, + get_pdf, make_supplier_quotation_from_rfq, ) from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq @@ -124,6 +125,11 @@ class TestRequestforQuotation(FrappeTestCase): rfq.status = "Draft" rfq.submit() + def test_get_pdf(self): + rfq = make_request_for_quotation() + get_pdf(rfq.name, rfq.get("suppliers")[0].supplier) + self.assertEqual(frappe.local.response.type, "pdf") + def make_request_for_quotation(**args): """ diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index 47a66ad46f2..9b53421319d 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -15,60 +15,4 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse class TestProcurementTracker(FrappeTestCase): - def test_result_for_procurement_tracker(self): - filters = {"company": "_Test Procurement Company", "cost_center": "Main - _TPC"} - expected_data = self.generate_expected_data() - report = execute(filters) - - length = len(report[1]) - self.assertEqual(expected_data, report[1][length - 1]) - - def generate_expected_data(self): - if not frappe.db.exists("Company", "_Test Procurement Company"): - frappe.get_doc( - dict( - doctype="Company", - company_name="_Test Procurement Company", - abbr="_TPC", - default_currency="INR", - country="Pakistan", - ) - ).insert() - warehouse = create_warehouse("_Test Procurement Warehouse", company="_Test Procurement Company") - mr = make_material_request( - company="_Test Procurement Company", warehouse=warehouse, cost_center="Main - _TPC" - ) - po = make_purchase_order(mr.name) - po.supplier = "_Test Supplier" - po.get("items")[0].cost_center = "Main - _TPC" - po.submit() - pr = make_purchase_receipt(po.name) - pr.get("items")[0].cost_center = "Main - _TPC" - pr.submit() - date_obj = datetime.date(datetime.now()) - - po.load_from_db() - - expected_data = { - "material_request_date": date_obj, - "cost_center": "Main - _TPC", - "project": None, - "requesting_site": "_Test Procurement Warehouse - _TPC", - "requestor": "Administrator", - "material_request_no": mr.name, - "item_code": "_Test Item", - "quantity": 10.0, - "unit_of_measurement": "_Test UOM", - "status": "To Bill", - "purchase_order_date": date_obj, - "purchase_order": po.name, - "supplier": "_Test Supplier", - "estimated_cost": 0.0, - "actual_cost": 0.0, - "purchase_order_amt": po.net_total, - "purchase_order_amt_in_company_currency": po.base_net_total, - "expected_delivery_date": date_obj, - "actual_delivery_date": date_obj, - } - - return expected_data + pass diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 54f0d94914b..4f7d9ad92e8 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -712,6 +712,8 @@ class BuyingController(SubcontractingController): asset.purchase_date = self.posting_date asset.supplier = self.supplier elif self.docstatus == 2: + if asset.docstatus == 2: + continue if asset.docstatus == 0: asset.set(field, None) asset.supplier = None diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 8bd09982bf4..fc6793a9bbc 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -252,6 +252,7 @@ def get_already_returned_items(doc): child.parent = par.name and par.docstatus = 1 and par.is_return = 1 and par.return_against = %s group by item_code + for update """.format( column, doc.doctype, doc.doctype ), @@ -305,7 +306,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)] # Used retrun against and supplier and is_retrun because there is an index added for it - data = frappe.db.get_list( + data = frappe.get_all( doctype, fields=fields, filters=[ diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index a9561fe2dac..cc80f6ca984 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -409,7 +409,14 @@ class SubcontractingController(StockController): if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: new_rm_obj = None for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): - if batch_qty >= qty: + if batch_qty >= qty or ( + rm_obj.consumed_qty == 0 + and self.backflush_based_on == "BOM" + and len(self.available_materials[key]["batch_no"]) == 1 + ): + if rm_obj.consumed_qty == 0: + self.__set_consumed_qty(rm_obj, qty) + self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) self.available_materials[key]["batch_no"][batch_no] -= qty return diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 8f8a086d99e..077e7fa4af0 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -312,7 +312,8 @@ "fieldtype": "Data", "hidden": 1, "label": "Title", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "language", @@ -514,11 +515,10 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2022-10-13 12:42:04.277879", + "modified": "2023-01-24 18:20:05.044791", "modified_by": "Administrator", "module": "CRM", "name": "Lead", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index b0ff5d4c3b8..2a588d8d13f 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -282,6 +282,7 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False): "contact_no": "phone_1", "fax": "fax_1", }, + "field_no_map": ["disabled"], } }, target_doc, @@ -390,7 +391,7 @@ def get_lead_details(lead, posting_date=None, company=None): { "territory": lead.territory, "customer_name": lead.company_name or lead.lead_name, - "contact_display": " ".join(filter(None, [lead.salutation, lead.lead_name])), + "contact_display": " ".join(filter(None, [lead.lead_name])), "contact_email": lead.email_id, "contact_mobile": lead.mobile_no, "contact_phone": lead.phone, diff --git a/erpnext/crm/doctype/lead_source/lead_source.json b/erpnext/crm/doctype/lead_source/lead_source.json index 723c6d993d7..c3cedcc7a63 100644 --- a/erpnext/crm/doctype/lead_source/lead_source.json +++ b/erpnext/crm/doctype/lead_source/lead_source.json @@ -26,10 +26,11 @@ } ], "links": [], - "modified": "2021-02-08 12:51:48.971517", + "modified": "2023-02-10 00:51:44.973957", "modified_by": "Administrator", "module": "CRM", "name": "Lead Source", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -58,5 +59,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.json b/erpnext/crm/doctype/sales_stage/sales_stage.json index 77aa559b771..caf8ff5b36b 100644 --- a/erpnext/crm/doctype/sales_stage/sales_stage.json +++ b/erpnext/crm/doctype/sales_stage/sales_stage.json @@ -18,10 +18,11 @@ } ], "links": [], - "modified": "2020-05-20 12:22:01.866472", + "modified": "2023-02-10 01:40:23.713390", "modified_by": "Administrator", "module": "CRM", "name": "Sales Stage", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -40,5 +41,7 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1 + "states": [], + "track_changes": 1, + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js index a227b6d7973..458c79a1ea8 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js @@ -11,6 +11,40 @@ frappe.query_reports["Loan Interest Report"] = { "options": "Company", "default": frappe.defaults.get_user_default("Company"), "reqd": 1 - } + }, + { + "fieldname":"applicant_type", + "label": __("Applicant Type"), + "fieldtype": "Select", + "options": ["Customer", "Employee"], + "reqd": 1, + "default": "Customer", + on_change: function() { + frappe.query_report.set_filter_value('applicant', ""); + } + }, + { + "fieldname": "applicant", + "label": __("Applicant"), + "fieldtype": "Dynamic Link", + "get_options": function() { + var applicant_type = frappe.query_report.get_filter_value('applicant_type'); + var applicant = frappe.query_report.get_filter_value('applicant'); + if(applicant && !applicant_type) { + frappe.throw(__("Please select Applicant Type first")); + } + return applicant_type; + } + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + }, + { + "fieldname":"to_date", + "label": __("From Date"), + "fieldtype": "Date", + }, ] }; diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index 9186ce61743..58a7880a459 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -13,12 +13,12 @@ from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applic def execute(filters=None): - columns = get_columns(filters) + columns = get_columns() data = get_active_loan_details(filters) return columns, data -def get_columns(filters): +def get_columns(): columns = [ {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160}, @@ -70,6 +70,13 @@ def get_columns(filters): "options": "currency", "width": 120, }, + { + "label": _("Accrued Principal"), + "fieldname": "accrued_principal", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, { "label": _("Total Repayment"), "fieldname": "total_repayment", @@ -137,11 +144,16 @@ def get_columns(filters): def get_active_loan_details(filters): - - filter_obj = {"status": ("!=", "Closed")} + filter_obj = { + "status": ("!=", "Closed"), + "docstatus": 1, + } if filters.get("company"): filter_obj.update({"company": filters.get("company")}) + if filters.get("applicant"): + filter_obj.update({"applicant": filters.get("applicant")}) + loan_details = frappe.get_all( "Loan", fields=[ @@ -167,8 +179,8 @@ def get_active_loan_details(filters): sanctioned_amount_map = get_sanctioned_amount_map() penal_interest_rate_map = get_penal_interest_rate_map() - payments = get_payments(loan_list) - accrual_map = get_interest_accruals(loan_list) + payments = get_payments(loan_list, filters) + accrual_map = get_interest_accruals(loan_list, filters) currency = erpnext.get_company_currency(filters.get("company")) for loan in loan_details: @@ -183,6 +195,7 @@ def get_active_loan_details(filters): - flt(loan.written_off_amount), "total_repayment": flt(payments.get(loan.loan)), "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), + "accrued_principal": flt(accrual_map.get(loan.loan, {}).get("accrued_principal")), "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), "penalty_interest": penal_interest_rate_map.get(loan.loan_type), @@ -212,20 +225,35 @@ def get_sanctioned_amount_map(): ) -def get_payments(loans): +def get_payments(loans, filters): + query_filters = {"against_loan": ("in", loans)} + + if filters.get("from_date"): + query_filters.update({"posting_date": (">=", filters.get("from_date"))}) + + if filters.get("to_date"): + query_filters.update({"posting_date": ("<=", filters.get("to_date"))}) + return frappe._dict( frappe.get_all( "Loan Repayment", fields=["against_loan", "sum(amount_paid)"], - filters={"against_loan": ("in", loans)}, + filters=query_filters, group_by="against_loan", as_list=1, ) ) -def get_interest_accruals(loans): +def get_interest_accruals(loans, filters): accrual_map = {} + query_filters = {"loan": ("in", loans)} + + if filters.get("from_date"): + query_filters.update({"posting_date": (">=", filters.get("from_date"))}) + + if filters.get("to_date"): + query_filters.update({"posting_date": ("<=", filters.get("to_date"))}) interest_accruals = frappe.get_all( "Loan Interest Accrual", @@ -236,8 +264,9 @@ def get_interest_accruals(loans): "penalty_amount", "paid_interest_amount", "accrual_type", + "payable_principal_amount", ], - filters={"loan": ("in", loans)}, + filters=query_filters, order_by="posting_date desc", ) @@ -246,6 +275,7 @@ def get_interest_accruals(loans): entry.loan, { "accrued_interest": 0.0, + "accrued_principal": 0.0, "undue_interest": 0.0, "interest_outstanding": 0.0, "last_accrual_date": "", @@ -270,6 +300,7 @@ def get_interest_accruals(loans): accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount + accrual_map[entry.loan]["accrued_principal"] += entry.payable_principal_amount if last_accrual_date and getdate(entry.posting_date) == last_accrual_date: accrual_map[entry.loan]["penalty"] = entry.penalty_amount diff --git a/erpnext/loan_management/workspace/loans/loans.json b/erpnext/loan_management/workspace/loans/loans.json new file mode 100644 index 00000000000..c65be4efae9 --- /dev/null +++ b/erpnext/loan_management/workspace/loans/loans.json @@ -0,0 +1,315 @@ +{ + "charts": [], + "content": "[{\"id\":\"_38WStznya\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"t7o_K__1jB\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"id\":\"IRiNDC6w1p\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"id\":\"xbbo0FYbq0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"7ZL4Bro-Vi\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yhyioTViZ3\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"oYFn4b1kSw\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"id\":\"vZepJF5tl9\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"id\":\"k-393Mjhqe\",\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"id\":\"6crJ0DBiBJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"id\":\"Um5YwxVLRJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", + "creation": "2020-03-12 16:35:55.299820", + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "loan", + "idx": 0, + "is_hidden": 0, + "label": "Loans", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Loan", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Type", + "link_count": 0, + "link_to": "Loan Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Application", + "link_count": 0, + "link_to": "Loan Application", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan", + "link_count": 0, + "link_to": "Loan", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Loan Processes", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Process Loan Security Shortfall", + "link_count": 0, + "link_to": "Process Loan Security Shortfall", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Process Loan Interest Accrual", + "link_count": 0, + "link_to": "Process Loan Interest Accrual", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Disbursement and Repayment", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Disbursement", + "link_count": 0, + "link_to": "Loan Disbursement", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Repayment", + "link_count": 0, + "link_to": "Loan Repayment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Write Off", + "link_count": 0, + "link_to": "Loan Write Off", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Interest Accrual", + "link_count": 0, + "link_to": "Loan Interest Accrual", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Type", + "link_count": 0, + "link_to": "Loan Security Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Price", + "link_count": 0, + "link_to": "Loan Security Price", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security", + "link_count": 0, + "link_to": "Loan Security", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Pledge", + "link_count": 0, + "link_to": "Loan Security Pledge", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Unpledge", + "link_count": 0, + "link_to": "Loan Security Unpledge", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Shortfall", + "link_count": 0, + "link_to": "Loan Security Shortfall", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 6, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 1, + "label": "Loan Repayment and Closure", + "link_count": 0, + "link_to": "Loan Repayment and Closure", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 1, + "label": "Loan Security Status", + "link_count": 0, + "link_to": "Loan Security Status", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Loan Interest Report", + "link_count": 0, + "link_to": "Loan Interest Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Loan Security Exposure", + "link_count": 0, + "link_to": "Loan Security Exposure", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Applicant-Wise Loan Security Exposure", + "link_count": 0, + "link_to": "Applicant-Wise Loan Security Exposure", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Loan Security Status", + "link_count": 0, + "link_to": "Loan Security Status", + "link_type": "Report", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2023-01-31 19:47:13.114415", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loans", + "owner": "Administrator", + "parent_page": "", + "public": 1, + "quick_lists": [], + "restrict_to_domain": "", + "roles": [], + "sequence_id": 16.0, + "shortcuts": [ + { + "color": "Green", + "format": "{} Open", + "label": "Loan Application", + "link_to": "Loan Application", + "stats_filter": "{ \"status\": \"Open\" }", + "type": "DocType" + }, + { + "label": "Loan", + "link_to": "Loan", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Dashboard", + "link_to": "Loan Dashboard", + "type": "Dashboard" + } + ], + "title": "Loans" +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index c2b331fcfd1..db699b94d8f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -289,7 +289,7 @@ { "fieldname": "scrap_items", "fieldtype": "Table", - "label": "Items", + "label": "Scrap Items", "options": "BOM Scrap Item" }, { @@ -605,7 +605,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-01-10 07:47:08.652616", + "modified": "2023-02-13 17:31:37.504565", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6744d162546..e6be933f258 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -306,7 +306,6 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note execute:frappe.delete_doc("DocType", "Naming Series") erpnext.patches.v13_0.job_card_status_on_hold erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow -erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.crm_ux_cleanup erpnext.patches.v14_0.migrate_existing_lead_notes_as_per_the_new_format erpnext.patches.v14_0.remove_india_localisation # 14-07-2022 @@ -315,7 +314,6 @@ erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022 erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries -erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v13_0.drop_unused_sle_index_parts erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization @@ -325,3 +323,8 @@ erpnext.patches.v14_0.setup_clear_repost_logs erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request erpnext.patches.v14_0.update_entry_type_for_journal_entry erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers +erpnext.patches.v14_0.set_pick_list_status +erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries +# below 2 migration patches should always run last +erpnext.patches.v14_0.migrate_gl_to_payment_ledger +erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger diff --git a/erpnext/patches/v11_0/update_sales_partner_type.py b/erpnext/patches/v11_0/update_sales_partner_type.py index 2d37fd69b19..72fd424b245 100644 --- a/erpnext/patches/v11_0/update_sales_partner_type.py +++ b/erpnext/patches/v11_0/update_sales_partner_type.py @@ -1,16 +1,17 @@ import frappe -from frappe import _ def execute(): - from erpnext.setup.setup_wizard.operations.install_fixtures import default_sales_partner_type + from erpnext.setup.setup_wizard.operations.install_fixtures import read_lines frappe.reload_doc("selling", "doctype", "sales_partner_type") frappe.local.lang = frappe.db.get_default("lang") or "en" + default_sales_partner_type = read_lines("sales_partner_type.txt") + for s in default_sales_partner_type: - insert_sales_partner_type(_(s)) + insert_sales_partner_type(s) # get partner type in existing forms (customized) # and create a document if not created diff --git a/erpnext/patches/v14_0/migrate_cost_center_allocations.py b/erpnext/patches/v14_0/migrate_cost_center_allocations.py index 3bd26933bad..48f4e6d9893 100644 --- a/erpnext/patches/v14_0/migrate_cost_center_allocations.py +++ b/erpnext/patches/v14_0/migrate_cost_center_allocations.py @@ -18,9 +18,11 @@ def create_new_cost_center_allocation_records(cc_allocations): cca = frappe.new_doc("Cost Center Allocation") cca.main_cost_center = main_cc cca.valid_from = today() + cca._skip_from_date_validation = True for child_cc, percentage in allocations.items(): cca.append("allocation_percentages", ({"cost_center": child_cc, "percentage": percentage})) + cca.save() cca.submit() diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py index e15aa4a1f41..853a99a4895 100644 --- a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py @@ -2,7 +2,8 @@ import frappe from frappe import qb from frappe.query_builder import Case, CustomFunction from frappe.query_builder.custom import ConstantColumn -from frappe.query_builder.functions import IfNull +from frappe.query_builder.functions import Count, IfNull +from frappe.utils import flt from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimensions, @@ -17,9 +18,9 @@ def create_accounting_dimension_fields(): make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) -def generate_name_for_payment_ledger_entries(gl_entries): - for index, entry in enumerate(gl_entries, 1): - entry.name = index +def generate_name_for_payment_ledger_entries(gl_entries, start): + for index, entry in enumerate(gl_entries, 0): + entry.name = start + index def get_columns(): @@ -81,6 +82,14 @@ def insert_chunk_into_payment_ledger(insert_query, gl_entries): def execute(): + """ + Description: + Migrate records from `tabGL Entry` to `tabPayment Ledger Entry`. + Patch is non-resumable. if patch failed or is terminatted abnormally, clear 'tabPayment Ledger Entry' table manually before re-running. Re-running is safe only during V13->V14 update. + + Note: Post successful migration to V14, re-running is NOT-SAFE and SHOULD NOT be attempted. + """ + if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): # create accounting dimension fields in Payment Ledger create_accounting_dimension_fields() @@ -89,52 +98,90 @@ def execute(): account = qb.DocType("Account") ifelse = CustomFunction("IF", ["condition", "then", "else"]) - gl_entries = ( - qb.from_(gl) - .inner_join(account) - .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) - .select( - gl.star, - ConstantColumn(1).as_("docstatus"), - account.account_type.as_("account_type"), - IfNull( - ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type - ).as_("against_voucher_type"), - IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_( - "against_voucher_no" - ), - # convert debit/credit to amount - Case() - .when(account.account_type == "Receivable", gl.debit - gl.credit) - .else_(gl.credit - gl.debit) - .as_("amount"), - # convert debit/credit in account currency to amount in account currency - Case() - .when( - account.account_type == "Receivable", - gl.debit_in_account_currency - gl.credit_in_account_currency, - ) - .else_(gl.credit_in_account_currency - gl.debit_in_account_currency) - .as_("amount_in_account_currency"), - ) - .where(gl.is_cancelled == 0) - .orderby(gl.creation) - .run(as_dict=True) + # Get Records Count + accounts = ( + qb.from_(account) + .select(account.name) + .where((account.account_type == "Receivable") | (account.account_type == "Payable")) + .orderby(account.name) ) + un_processed = ( + qb.from_(gl) + .select(Count(gl.name)) + .where((gl.is_cancelled == 0) & (gl.account.isin(accounts))) + .run() + )[0][0] - # primary key(name) for payment ledger records - generate_name_for_payment_ledger_entries(gl_entries) + if un_processed: + print(f"Migrating {un_processed} GL Entries to Payment Ledger") - # split data into chunks - chunk_size = 1000 - try: - for i in range(0, len(gl_entries), chunk_size): - insert_query = build_insert_query() - insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size]) - frappe.db.commit() - except Exception as err: - frappe.db.rollback() - ple = qb.DocType("Payment Ledger Entry") - qb.from_(ple).delete().where(ple.docstatus >= 0).run() - frappe.db.commit() - raise err + processed = 0 + last_update_percent = 0 + batch_size = 5000 + last_name = None + + while True: + if last_name: + where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0) + else: + where_clause = gl.is_cancelled == 0 + + gl_entries = ( + qb.from_(gl) + .inner_join(account) + .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) + .select( + gl.star, + ConstantColumn(1).as_("docstatus"), + account.account_type.as_("account_type"), + IfNull( + ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type + ).as_("against_voucher_type"), + IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_( + "against_voucher_no" + ), + # convert debit/credit to amount + Case() + .when(account.account_type == "Receivable", gl.debit - gl.credit) + .else_(gl.credit - gl.debit) + .as_("amount"), + # convert debit/credit in account currency to amount in account currency + Case() + .when( + account.account_type == "Receivable", + gl.debit_in_account_currency - gl.credit_in_account_currency, + ) + .else_(gl.credit_in_account_currency - gl.debit_in_account_currency) + .as_("amount_in_account_currency"), + ) + .where(where_clause) + .orderby(gl.name) + .limit(batch_size) + .run(as_dict=True) + ) + + if gl_entries: + last_name = gl_entries[-1].name + + # primary key(name) for payment ledger records + generate_name_for_payment_ledger_entries(gl_entries, processed) + + try: + insert_query = build_insert_query() + insert_chunk_into_payment_ledger(insert_query, gl_entries) + frappe.db.commit() + + processed += len(gl_entries) + + # Progress message + percent = flt((processed / un_processed) * 100, 2) + if percent - last_update_percent > 1: + print(f"{percent}% ({processed}) records processed") + last_update_percent = percent + + except Exception as err: + print("Migration Failed. Clear `tabPayment Ledger Entry` table before re-running") + raise err + else: + break + print(f"{processed} records have been sucessfully migrated") diff --git a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py index fd2a2a39cc6..9d216c4028c 100644 --- a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py @@ -1,81 +1,98 @@ import frappe from frappe import qb -from frappe.utils import create_batch - - -def remove_duplicate_entries(pl_entries): - unique_vouchers = set() - for x in pl_entries: - unique_vouchers.add( - (x.company, x.account, x.party_type, x.party, x.voucher_type, x.voucher_no, x.gle_remarks) - ) - - entries = [] - for x in unique_vouchers: - entries.append( - frappe._dict( - company=x[0], - account=x[1], - party_type=x[2], - party=x[3], - voucher_type=x[4], - voucher_no=x[5], - gle_remarks=x[6], - ) - ) - return entries +from frappe.query_builder import CustomFunction +from frappe.query_builder.functions import Count, IfNull +from frappe.utils import flt def execute(): + """ + Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry' + """ + if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): gle = qb.DocType("GL Entry") ple = qb.DocType("Payment Ledger Entry") - # get ple and their remarks from GL Entry - pl_entries = ( - qb.from_(ple) - .left_join(gle) - .on( - (ple.account == gle.account) - & (ple.party_type == gle.party_type) - & (ple.party == gle.party) - & (ple.voucher_type == gle.voucher_type) - & (ple.voucher_no == gle.voucher_no) - & (ple.company == gle.company) - ) - .select( - ple.company, - ple.account, - ple.party_type, - ple.party, - ple.voucher_type, - ple.voucher_no, - gle.remarks.as_("gle_remarks"), - ) - .where((ple.delinked == 0) & (gle.is_cancelled == 0)) - .run(as_dict=True) - ) + # Get empty PLE records + un_processed = ( + qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run() + )[0][0] - pl_entries = remove_duplicate_entries(pl_entries) + if un_processed: + print(f"Remarks for {un_processed} Payment Ledger records will be updated from GL Entry") - if pl_entries: - # split into multiple batches, update and commit for each batch + ifelse = CustomFunction("IF", ["condition", "then", "else"]) + + processed = 0 + last_percent_update = 0 batch_size = 1000 - for batch in create_batch(pl_entries, batch_size): - for entry in batch: - query = ( - qb.update(ple) - .set(ple.remarks, entry.gle_remarks) - .where( - (ple.company == entry.company) - & (ple.account == entry.account) - & (ple.party_type == entry.party_type) - & (ple.party == entry.party) - & (ple.voucher_type == entry.voucher_type) - & (ple.voucher_no == entry.voucher_no) - ) - ) - query.run() + last_name = None - frappe.db.commit() + while True: + if last_name: + where_clause = (ple.name.gt(last_name)) & (ple.remarks.isnull()) & (ple.delinked == 0) + else: + where_clause = (ple.remarks.isnull()) & (ple.delinked == 0) + + # results are deterministic + names = ( + qb.from_(ple).select(ple.name).where(where_clause).orderby(ple.name).limit(batch_size).run() + ) + + if names: + last_name = names[-1][0] + + pl_entries = ( + qb.from_(ple) + .left_join(gle) + .on( + (ple.account == gle.account) + & (ple.party_type == gle.party_type) + & (ple.party == gle.party) + & (ple.voucher_type == gle.voucher_type) + & (ple.voucher_no == gle.voucher_no) + & ( + ple.against_voucher_type + == IfNull( + ifelse(gle.against_voucher_type == "", None, gle.against_voucher_type), gle.voucher_type + ) + ) + & ( + ple.against_voucher_no + == IfNull(ifelse(gle.against_voucher == "", None, gle.against_voucher), gle.voucher_no) + ) + & (ple.company == gle.company) + & ( + ((ple.account_type == "Receivable") & (ple.amount == (gle.debit - gle.credit))) + | (ple.account_type == "Payable") & (ple.amount == (gle.credit - gle.debit)) + ) + & (gle.remarks.notnull()) + & (gle.is_cancelled == 0) + ) + .select(ple.name) + .distinct() + .select( + gle.remarks.as_("gle_remarks"), + ) + .where(ple.name.isin(names)) + .run(as_dict=True) + ) + + if pl_entries: + for entry in pl_entries: + query = qb.update(ple).set(ple.remarks, entry.gle_remarks).where((ple.name == entry.name)) + query.run() + + frappe.db.commit() + + processed += len(pl_entries) + percentage = flt((processed / un_processed) * 100, 2) + if percentage - last_percent_update > 1: + print(f"{percentage}% ({processed}) PLE records updated") + last_percent_update = percentage + + else: + break + print("Remarks succesfully migrated") diff --git a/erpnext/patches/v14_0/set_pick_list_status.py b/erpnext/patches/v14_0/set_pick_list_status.py new file mode 100644 index 00000000000..eea5745c23a --- /dev/null +++ b/erpnext/patches/v14_0/set_pick_list_status.py @@ -0,0 +1,40 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + + +import frappe +from pypika.terms import ExistsCriterion + + +def execute(): + pl = frappe.qb.DocType("Pick List") + se = frappe.qb.DocType("Stock Entry") + dn = frappe.qb.DocType("Delivery Note") + + ( + frappe.qb.update(pl).set( + pl.status, + ( + frappe.qb.terms.Case() + .when(pl.docstatus == 0, "Draft") + .when(pl.docstatus == 2, "Cancelled") + .else_("Completed") + ), + ) + ).run() + + ( + frappe.qb.update(pl) + .set(pl.status, "Open") + .where( + ( + ExistsCriterion( + frappe.qb.from_(se).select(se.name).where((se.docstatus == 1) & (se.pick_list == pl.name)) + ) + | ExistsCriterion( + frappe.qb.from_(dn).select(dn.name).where((dn.docstatus == 1) & (dn.pick_list == pl.name)) + ) + ).negate() + & (pl.docstatus == 1) + ) + ).run() diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 5dc3cdde6f8..371ecbc8c13 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -1,9 +1,5 @@ import frappe -from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - set_draft_asset_depr_schedule_details, -) - def execute(): frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule") @@ -16,7 +12,7 @@ def execute(): for fb_row in finance_book_rows: asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") - set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset, fb_row) + asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row) asset_depr_schedule_doc.insert() diff --git a/erpnext/patches/v15_0/update_asset_value_for_manual_depr_entries.py b/erpnext/patches/v15_0/update_asset_value_for_manual_depr_entries.py new file mode 100644 index 00000000000..5d7b5cf19c1 --- /dev/null +++ b/erpnext/patches/v15_0/update_asset_value_for_manual_depr_entries.py @@ -0,0 +1,38 @@ +import frappe +from frappe.query_builder.functions import IfNull, Sum + + +def execute(): + asset = frappe.qb.DocType("Asset") + gle = frappe.qb.DocType("GL Entry") + aca = frappe.qb.DocType("Asset Category Account") + company = frappe.qb.DocType("Company") + + asset_total_depr_value_map = ( + frappe.qb.from_(gle) + .join(asset) + .on(gle.against_voucher == asset.name) + .join(aca) + .on((aca.parent == asset.asset_category) & (aca.company_name == asset.company)) + .join(company) + .on(company.name == asset.company) + .select(Sum(gle.debit).as_("value"), asset.name.as_("asset_name")) + .where( + gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) + ) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .where(asset.docstatus == 1) + .where(asset.calculate_depreciation == 0) + .groupby(asset.name) + ) + + frappe.qb.update(asset).join(asset_total_depr_value_map).on( + asset_total_depr_value_map.asset_name == asset.name + ).set( + asset.value_after_depreciation, asset.value_after_depreciation - asset_total_depr_value_map.value + ).where( + asset.docstatus == 1 + ).where( + asset.calculate_depreciation == 0 + ).run() diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py index 27c8fe4c95a..3df56e67f60 100644 --- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py +++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py @@ -10,62 +10,6 @@ from frappe.website.serve import get_response class TestHomepageSection(unittest.TestCase): - def test_homepage_section_card(self): - try: - frappe.get_doc( - { - "doctype": "Homepage Section", - "name": "Card Section", - "section_based_on": "Cards", - "section_cards": [ - { - "title": "Card 1", - "subtitle": "Subtitle 1", - "content": "This is test card 1", - "route": "/card-1", - }, - { - "title": "Card 2", - "subtitle": "Subtitle 2", - "content": "This is test card 2", - "image": "test.jpg", - }, - ], - "no_of_columns": 3, - } - ).insert(ignore_if_duplicate=True) - except frappe.DuplicateEntryError: - pass - - set_request(method="GET", path="home") - response = get_response() - - self.assertEqual(response.status_code, 200) - - html = frappe.safe_decode(response.get_data()) - - soup = BeautifulSoup(html, "html.parser") - sections = soup.find("main").find_all("section") - self.assertEqual(len(sections), 3) - - homepage_section = sections[2] - self.assertEqual(homepage_section.h3.text, "Card Section") - - cards = homepage_section.find_all(class_="card") - - self.assertEqual(len(cards), 2) - self.assertEqual(cards[0].h5.text, "Card 1") - self.assertEqual(cards[0].a["href"], "/card-1") - self.assertEqual(cards[1].p.text, "Subtitle 2") - - img = cards[1].find(class_="card-img-top") - - self.assertEqual(img["src"], "test.jpg") - self.assertEqual(img["loading"], "lazy") - - # cleanup - frappe.db.rollback() - def test_homepage_section_custom_html(self): frappe.get_doc( { diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 37d98ad8ea1..ba7aa850825 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -408,7 +408,7 @@ "depends_on": "eval:(doc.frequency == \"Daily\" && doc.collect_progress == true)", "fieldname": "daily_time_to_send", "fieldtype": "Time", - "label": "Time to send" + "label": "Daily Time to send" }, { "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", @@ -421,7 +421,7 @@ "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", "fieldname": "weekly_time_to_send", "fieldtype": "Time", - "label": "Time to send" + "label": "Weekly Time to send" }, { "fieldname": "column_break_45", @@ -451,7 +451,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2022-06-23 16:45:06.108499", + "modified": "2023-02-14 04:54:25.819620", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -497,4 +497,4 @@ "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index e098c3e3c45..828a55e7bc1 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -161,6 +161,37 @@ class TestTimesheet(unittest.TestCase): to_time = timesheet.time_logs[0].to_time self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True)) + def test_per_billed_hours(self): + """If amounts are 0, per_billed should be calculated based on hours.""" + ts = frappe.new_doc("Timesheet") + ts.total_billable_amount = 0 + ts.total_billed_amount = 0 + ts.total_billable_hours = 2 + + ts.total_billed_hours = 0.5 + ts.calculate_percentage_billed() + self.assertEqual(ts.per_billed, 25) + + ts.total_billed_hours = 2 + ts.calculate_percentage_billed() + self.assertEqual(ts.per_billed, 100) + + def test_per_billed_amount(self): + """If amounts are > 0, per_billed should be calculated based on amounts, regardless of hours.""" + ts = frappe.new_doc("Timesheet") + ts.total_billable_hours = 2 + ts.total_billed_hours = 1 + ts.total_billable_amount = 200 + ts.total_billed_amount = 50 + ts.calculate_percentage_billed() + self.assertEqual(ts.per_billed, 25) + + ts.total_billed_hours = 3 + ts.total_billable_amount = 200 + ts.total_billed_amount = 200 + ts.calculate_percentage_billed() + self.assertEqual(ts.per_billed, 100) + def make_timesheet( employee, diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 0cce129034e..468300661a0 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -282,21 +282,21 @@ { "fieldname": "base_total_costing_amount", "fieldtype": "Currency", - "label": "Total Costing Amount", + "label": "Base Total Costing Amount", "print_hide": 1, "read_only": 1 }, { "fieldname": "base_total_billable_amount", "fieldtype": "Currency", - "label": "Total Billable Amount", + "label": "Base Total Billable Amount", "print_hide": 1, "read_only": 1 }, { "fieldname": "base_total_billed_amount", "fieldtype": "Currency", - "label": "Total Billed Amount", + "label": "Base Total Billed Amount", "print_hide": 1, "read_only": 1 }, @@ -311,10 +311,11 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2022-06-15 22:08:53.930200", + "modified": "2023-02-14 04:55:41.735991", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -388,5 +389,6 @@ ], "sort_field": "modified", "sort_order": "ASC", + "states": [], "title_field": "title" } \ No newline at end of file diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index f3bd09a67a4..d482a46053c 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -64,6 +64,8 @@ class Timesheet(Document): self.per_billed = 0 if self.total_billed_amount > 0 and self.total_billable_amount > 0: self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount + elif self.total_billed_hours > 0 and self.total_billable_hours > 0: + self.per_billed = (self.total_billed_hours * 100) / self.total_billable_hours def update_billing_hours(self, args): if args.is_billable: diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2ce0c7eb00d..a87c3ec9514 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -126,7 +126,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { frappe.model.round_floats_in(item); item.net_rate = item.rate; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; - item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item)); + + if (!(me.frm.doc.is_return || me.frm.doc.is_debit_note)) { + item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item)); + } + else { + let qty = item.qty || 1; + qty = me.frm.doc.is_return ? -1 * qty : qty; + item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); + } + item.item_tax_amount = 0.0; item.total_weight = flt(item.weight_per_unit * item.stock_qty); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3a778fa496c..09f2c5d5cb1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1691,6 +1691,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var me = this; var valid = true; + if (frappe.flags.ignore_company_party_validation) { + return valid; + } + $.each(["company", "customer"], function(i, fieldname) { if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) { if (!me.frm.doc[fieldname]) { diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 9288f515cd4..a913844e186 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -13,19 +13,11 @@ frappe.setup.on("before_load", function () { erpnext.setup.slides_settings = [ { - // Brand - name: 'brand', - icon: "fa fa-bookmark", - title: __("The Brand"), - // help: __('Upload your letter head and logo. (you can edit them later).'), + // Organization + name: 'organization', + title: __("Setup your organization"), + icon: "fa fa-building", fields: [ - { - fieldtype: "Attach Image", fieldname: "attach_logo", - label: __("Attach Logo"), - description: __("100px by 100px"), - is_private: 0, - align: 'center' - }, { fieldname: 'company_name', label: __('Company Name'), @@ -35,54 +27,9 @@ erpnext.setup.slides_settings = [ { fieldname: 'company_abbr', label: __('Company Abbreviation'), - fieldtype: 'Data' - } - ], - onload: function(slide) { - this.bind_events(slide); - }, - bind_events: function (slide) { - slide.get_input("company_name").on("change", function () { - var parts = slide.get_input("company_name").val().split(" "); - var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join(""); - slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase()); - }).val(frappe.boot.sysdefaults.company_name || "").trigger("change"); - - slide.get_input("company_abbr").on("change", function () { - if (slide.get_input("company_abbr").val().length > 10) { - frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters")); - slide.get_field("company_abbr").set_value(""); - } - }); - }, - validate: function() { - if ((this.values.company_name || "").toLowerCase() == "company") { - frappe.msgprint(__("Company Name cannot be Company")); - return false; - } - if (!this.values.company_abbr) { - return false; - } - if (this.values.company_abbr.length > 10) { - return false; - } - return true; - } - }, - { - // Organisation - name: 'organisation', - title: __("Your Organization"), - icon: "fa fa-building", - fields: [ - { - fieldname: 'company_tagline', - label: __('What does it do?'), fieldtype: 'Data', - placeholder: __('e.g. "Build tools for builders"'), - reqd: 1 + hidden: 1 }, - { fieldname: 'bank_account', label: __('Bank Name'), fieldtype: 'Data', reqd: 1 }, { fieldname: 'chart_of_accounts', label: __('Chart of Accounts'), options: "", fieldtype: 'Select' @@ -94,40 +41,24 @@ erpnext.setup.slides_settings = [ ], onload: function (slide) { - this.load_chart_of_accounts(slide); this.bind_events(slide); + this.load_chart_of_accounts(slide); this.set_fy_dates(slide); }, - validate: function () { - let me = this; - let exist; - if (!this.validate_fy_dates()) { return false; } - // Validate bank name - if(me.values.bank_account) { - frappe.call({ - async: false, - method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", - args: { - "coa": me.values.chart_of_accounts, - "bank_account": me.values.bank_account - }, - callback: function (r) { - if(r.message){ - exist = r.message; - me.get_field("bank_account").set_value(""); - let message = __('Account {0} already exists. Please enter a different name for your bank account.', - [me.values.bank_account] - ); - frappe.msgprint(message); - } - } - }); - return !exist; // Return False if exist = true + if ((this.values.company_name || "").toLowerCase() == "company") { + frappe.msgprint(__("Company Name cannot be Company")); + return false; + } + if (!this.values.company_abbr) { + return false; + } + if (this.values.company_abbr.length > 10) { + return false; } return true; @@ -151,15 +82,15 @@ erpnext.setup.slides_settings = [ var country = frappe.wizard.values.country; if (country) { - var fy = erpnext.setup.fiscal_years[country]; - var current_year = moment(new Date()).year(); - var next_year = current_year + 1; + let fy = erpnext.setup.fiscal_years[country]; + let current_year = moment(new Date()).year(); + let next_year = current_year + 1; if (!fy) { fy = ["01-01", "12-31"]; next_year = current_year; } - var year_start_date = current_year + "-" + fy[0]; + let year_start_date = current_year + "-" + fy[0]; if (year_start_date > frappe.datetime.get_today()) { next_year = current_year; current_year -= 1; @@ -171,7 +102,7 @@ erpnext.setup.slides_settings = [ load_chart_of_accounts: function (slide) { - var country = frappe.wizard.values.country; + let country = frappe.wizard.values.country; if (country) { frappe.call({ @@ -202,12 +133,25 @@ erpnext.setup.slides_settings = [ me.charts_modal(slide, chart_template); }); + + slide.get_input("company_name").on("change", function () { + let parts = slide.get_input("company_name").val().split(" "); + let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join(""); + slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase()); + }).val(frappe.boot.sysdefaults.company_name || "").trigger("change"); + + slide.get_input("company_abbr").on("change", function () { + if (slide.get_input("company_abbr").val().length > 10) { + frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters")); + slide.get_field("company_abbr").set_value(""); + } + }); }, charts_modal: function(slide, chart_template) { let parent = __('All Accounts'); - var dialog = new frappe.ui.Dialog({ + let dialog = new frappe.ui.Dialog({ title: chart_template, fields: [ {'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button', diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index d37b7bb43b3..51dcd64d9dd 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -491,7 +491,20 @@ erpnext.utils.update_child_items = function(opts) { const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`); const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision; - this.data = []; + this.data = frm.doc[opts.child_docname].map((d) => { + return { + "docname": d.name, + "name": d.name, + "item_code": d.item_code, + "delivery_date": d.delivery_date, + "schedule_date": d.schedule_date, + "conversion_factor": d.conversion_factor, + "qty": d.qty, + "rate": d.rate, + "uom": d.uom + } + }); + const fields = [{ fieldtype:'Data', fieldname:"docname", @@ -588,7 +601,7 @@ erpnext.utils.update_child_items = function(opts) { }) } - const dialog = new frappe.ui.Dialog({ + new frappe.ui.Dialog({ title: __("Update Items"), fields: [ { @@ -624,24 +637,7 @@ erpnext.utils.update_child_items = function(opts) { refresh_field("items"); }, primary_action_label: __('Update') - }); - - frm.doc[opts.child_docname].forEach(d => { - dialog.fields_dict.trans_items.df.data.push({ - "docname": d.name, - "name": d.name, - "item_code": d.item_code, - "delivery_date": d.delivery_date, - "schedule_date": d.schedule_date, - "conversion_factor": d.conversion_factor, - "qty": d.qty, - "rate": d.rate, - "uom": d.uom - }); - this.data = dialog.fields_dict.trans_items.df.data; - dialog.fields_dict.trans_items.grid.refresh(); - }) - dialog.show(); + }).show(); } erpnext.utils.map_current_doc = function(opts) { diff --git a/erpnext/selling/doctype/industry_type/industry_type.json b/erpnext/selling/doctype/industry_type/industry_type.json index 6c49f0f6dda..3c8ab8e47ae 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.json +++ b/erpnext/selling/doctype/industry_type/industry_type.json @@ -1,123 +1,68 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:industry", - "beta": 0, - "creation": "2012-03-27 14:36:09", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:industry", + "creation": "2012-03-27 14:36:09", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "industry" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "industry", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Industry", - "length": 0, - "no_copy": 0, - "oldfieldname": "industry", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "industry", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Industry", + "oldfieldname": "industry", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Selling", - "name": "Industry Type", - "owner": "Administrator", + ], + "icon": "fa fa-flag", + "idx": 1, + "links": [], + "modified": "2023-02-10 03:14:40.735763", + "modified_by": "Administrator", + "module": "Selling", + "name": "Industry Type", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.json b/erpnext/selling/doctype/party_specific_item/party_specific_item.json index 32b5d478bb5..a1f9902aaee 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.json +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_import": 1, "creation": "2021-08-27 19:28:07.559978", "doctype": "DocType", "editable_grid": 1, @@ -51,7 +52,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-14 13:27:58.612334", + "modified": "2023-02-15 13:00:50.379713", "modified_by": "Administrator", "module": "Selling", "name": "Party Specific Item", @@ -72,6 +73,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "party", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 6b42e4daead..b348bd35754 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -85,11 +85,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { - this.frm.add_custom_button( - __("Sales Order"), - this.frm.cscript["Make Sales Order"], - __("Create") - ); + if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation + || (!doc.valid_till) + || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { + this.frm.add_custom_button( + __("Sales Order"), + this.frm.cscript["Make Sales Order"], + __("Create") + ); + } if(doc.status!=="Ordered") { this.frm.add_custom_button(__('Set as Lost'), () => { diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 6836d56647f..063813b2dc7 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -195,6 +195,17 @@ def get_list_context(context=None): @frappe.whitelist() def make_sales_order(source_name: str, target_doc=None): + if not frappe.db.get_singles_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" + ): + quotation = frappe.db.get_value( + "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 + ) + if quotation.valid_till and ( + quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) + ): + frappe.throw(_("Validity period of this quotation has ended.")) + return _make_sales_order(source_name, target_doc) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 5aaba4fa435..cdf5f5d00c5 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -144,11 +144,21 @@ class TestQuotation(FrappeTestCase): def test_so_from_expired_quotation(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order + frappe.db.set_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0 + ) + quotation = frappe.copy_doc(test_records[0]) quotation.valid_till = add_days(nowdate(), -1) quotation.insert() quotation.submit() + self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) + + frappe.db.set_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1 + ) + make_sales_order(quotation.name) def test_shopping_cart_without_website_item(self): diff --git a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json index e7dd0d84a0a..a9b500a625f 100644 --- a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json +++ b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json @@ -1,94 +1,47 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:sales_partner_type", - "beta": 0, - "creation": "2018-06-11 13:15:57.404716", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "field:sales_partner_type", + "creation": "2018-06-11 13:15:57.404716", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_partner_type" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_partner_type", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Sales Partner Type", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "sales_partner_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Sales Partner Type", + "reqd": 1, + "unique": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-06-11 13:45:13.554307", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Partner Type", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2023-02-10 01:00:20.110800", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Partner Type", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 2abb169b8a0..6ea66a02378 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -27,6 +27,7 @@ "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", + "allow_sales_order_creation_for_expired_quotation", "hide_tax_id", "enable_discount_accounting" ], @@ -172,6 +173,12 @@ "fieldname": "enable_discount_accounting", "fieldtype": "Check", "label": "Enable Discount Accounting for Selling" + }, + { + "default": "0", + "fieldname": "allow_sales_order_creation_for_expired_quotation", + "fieldtype": "Check", + "label": "Allow Sales Order Creation For Expired Quotation" } ], "icon": "fa fa-cog", @@ -179,7 +186,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-05-31 19:39:48.398738", + "modified": "2023-02-04 12:37:53.380857", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 999ddc23f0c..158ac1d049a 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -17,45 +17,79 @@ from erpnext.stock.utils import scan_barcode def search_by_term(search_term, warehouse, price_list): result = search_for_serial_or_batch_or_barcode_number(search_term) or {} - item_code = result.get("item_code") or search_term - serial_no = result.get("serial_no") or "" - batch_no = result.get("batch_no") or "" - barcode = result.get("barcode") or "" + item_code = result.get("item_code", search_term) + serial_no = result.get("serial_no", "") + batch_no = result.get("batch_no", "") + barcode = result.get("barcode", "") - if result: - item_info = frappe.db.get_value( - "Item", - item_code, - [ - "name as item_code", - "item_name", - "description", - "stock_uom", - "image as item_image", - "is_stock_item", - ], - as_dict=1, - ) + if not result: + return - item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) - price_list_rate, currency = frappe.db.get_value( - "Item Price", - {"price_list": price_list, "item_code": item_code}, - ["price_list_rate", "currency"], - ) or [None, None] + item_doc = frappe.get_doc("Item", item_code) - item_info.update( + if not item_doc: + return + + item = { + "barcode": barcode, + "batch_no": batch_no, + "description": item_doc.description, + "is_stock_item": item_doc.is_stock_item, + "item_code": item_doc.name, + "item_image": item_doc.image, + "item_name": item_doc.item_name, + "serial_no": serial_no, + "stock_uom": item_doc.stock_uom, + "uom": item_doc.stock_uom, + } + + if barcode: + barcode_info = next(filter(lambda x: x.barcode == barcode, item_doc.get("barcodes", [])), None) + if barcode_info and barcode_info.uom: + uom = next(filter(lambda x: x.uom == barcode_info.uom, item_doc.uoms), {}) + item.update( + { + "uom": barcode_info.uom, + "conversion_factor": uom.get("conversion_factor", 1), + } + ) + + item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) + item_stock_qty = item_stock_qty // item.get("conversion_factor") + item.update({"actual_qty": item_stock_qty}) + + price = frappe.get_list( + doctype="Item Price", + filters={ + "price_list": price_list, + "item_code": item_code, + }, + fields=["uom", "stock_uom", "currency", "price_list_rate"], + ) + + def __sort(p): + p_uom = p.get("uom") + + if p_uom == item.get("uom"): + return 0 + elif p_uom == item.get("stock_uom"): + return 1 + else: + return 2 + + # sort by fallback preference. always pick exact uom match if available + price = sorted(price, key=__sort) + + if len(price) > 0: + p = price.pop(0) + item.update( { - "serial_no": serial_no, - "batch_no": batch_no, - "barcode": barcode, - "price_list_rate": price_list_rate, - "currency": currency, - "actual_qty": item_stock_qty, + "currency": p.get("currency"), + "price_list_rate": p.get("price_list_rate"), } ) - return {"items": [item_info]} + return {"items": [item]} @frappe.whitelist() @@ -121,33 +155,43 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te as_dict=1, ) - if items_data: - items = [d.item_code for d in items_data] - item_prices_data = frappe.get_all( + # return (empty) list if there are no results + if not items_data: + return result + + for item in items_data: + uoms = frappe.get_doc("Item", item.item_code).get("uoms", []) + + item.actual_qty, _ = get_stock_availability(item.item_code, warehouse) + item.uom = item.stock_uom + + item_price = frappe.get_all( "Item Price", - fields=["item_code", "price_list_rate", "currency"], - filters={"price_list": price_list, "item_code": ["in", items]}, + fields=["price_list_rate", "currency", "uom"], + filters={ + "price_list": price_list, + "item_code": item.item_code, + "selling": True, + }, ) - item_prices = {} - for d in item_prices_data: - item_prices[d.item_code] = d + if not item_price: + result.append(item) - for item in items_data: - item_code = item.item_code - item_price = item_prices.get(item_code) or {} - item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) + for price in item_price: + uom = next(filter(lambda x: x.uom == price.uom, uoms), {}) - row = {} - row.update(item) - row.update( + if price.uom != item.stock_uom and uom and uom.conversion_factor: + item.actual_qty = item.actual_qty // uom.conversion_factor + + result.append( { - "price_list_rate": item_price.get("price_list_rate"), - "currency": item_price.get("currency"), - "actual_qty": item_stock_qty, + **item, + "price_list_rate": price.get("price_list_rate"), + "currency": price.get("currency"), + "uom": price.uom or item.uom, } ) - result.append(row) return {"items": result} diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 595b9196e84..c442774d0f7 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -542,12 +542,12 @@ erpnext.PointOfSale.Controller = class { if (!this.frm.doc.customer) return this.raise_customer_selection_alert(); - const { item_code, batch_no, serial_no, rate } = item; + const { item_code, batch_no, serial_no, rate, uom } = item; if (!item_code) return; - const new_item = { item_code, batch_no, rate, [field]: value }; + const new_item = { item_code, batch_no, rate, uom, [field]: value }; if (serial_no) { await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); @@ -649,6 +649,7 @@ erpnext.PointOfSale.Controller = class { const is_stock_item = resp[1]; frappe.dom.unfreeze(); + const bold_uom = item_row.stock_uom.bold(); const bold_item_code = item_row.item_code.bold(); const bold_warehouse = warehouse.bold(); const bold_available_qty = available_qty.toString().bold() @@ -664,7 +665,7 @@ erpnext.PointOfSale.Controller = class { } } else if (is_stock_item && available_qty < qty_needed) { frappe.throw({ - message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), + message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.', [bold_item_code, bold_warehouse, bold_available_qty, bold_uom]), indicator: 'orange' }); frappe.utils.play_sound("error"); diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index e7dd211c0f4..12cc629776c 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -609,7 +609,7 @@ erpnext.PointOfSale.ItemCart = class { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { return `
-
${item_data.qty || 0}
+
${item_data.qty || 0} ${item_data.uom}
${format_currency(item_data.amount, currency)}
${format_currency(item_data.rate, currency)}
@@ -618,7 +618,7 @@ erpnext.PointOfSale.ItemCart = class { } else { return `
-
${item_data.qty || 0}
+
${item_data.qty || 0} ${item_data.uom}
${format_currency(item_data.rate, currency)}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index b5eb0489f9d..ec67bdfd9dd 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -78,7 +78,7 @@ erpnext.PointOfSale.ItemSelector = class { get_item_html(item) { const me = this; // eslint-disable-next-line no-unused-vars - const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item; + const { item_image, serial_no, batch_no, barcode, actual_qty, uom, price_list_rate } = item; const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; let indicator_color; let qty_to_display = actual_qty; @@ -118,7 +118,7 @@ erpnext.PointOfSale.ItemSelector = class { return ( `
@@ -128,7 +128,7 @@ erpnext.PointOfSale.ItemSelector = class {
${frappe.ellipsis(item.item_name, 18)}
-
${format_currency(price_list_rate, item.currency, precision) || 0}
+
${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}
` ); diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 40165c3484f..be75bd64cfd 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -94,7 +94,7 @@ erpnext.PointOfSale.PastOrderSummary = class { get_item_html(doc, item_data) { return `
${item_data.item_name}
-
${item_data.qty || 0}
+
${item_data.qty || 0} ${item_data.uom}
${get_rate_discount_html()}
`; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 0a356b9a6fb..89ce61ab168 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -322,6 +322,11 @@ erpnext.PointOfSale.Payment = class { this.focus_on_default_mop(); } + after_render() { + const frm = this.events.get_frm(); + frm.script_manager.trigger("after_payment_render", frm.doc.doctype, frm.doc.docname); + } + edit_cart() { this.events.toggle_other_sections(false); this.toggle_component(false); @@ -332,6 +337,7 @@ erpnext.PointOfSale.Payment = class { this.toggle_component(true); this.render_payment_section(); + this.after_render(); } toggle_remarks_control() { diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index e10df2acbb5..44c4d5497ba 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -41,8 +41,20 @@ def get_columns(filters): {"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150}, {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150}, {"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100}, - {"label": _("Rate"), "fieldname": "rate", "options": "Currency", "width": 120}, - {"label": _("Amount"), "fieldname": "amount", "options": "Currency", "width": 120}, + { + "label": _("Rate"), + "fieldname": "rate", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, { "label": _("Sales Order"), "fieldtype": "Link", @@ -93,8 +105,9 @@ def get_columns(filters): }, { "label": _("Billed Amount"), - "fieldtype": "currency", + "fieldtype": "Currency", "fieldname": "billed_amount", + "options": "currency", "width": 120, }, { @@ -104,6 +117,13 @@ def get_columns(filters): "options": "Company", "width": 100, }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "hidden": 1, + }, ] @@ -141,31 +161,12 @@ def get_data(filters): "billed_amount": flt(record.get("billed_amt")), "company": record.get("company"), } + row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency") data.append(row) return data -def get_conditions(filters): - conditions = "" - if filters.get("item_group"): - conditions += "AND so_item.item_group = %s" % frappe.db.escape(filters.item_group) - - if filters.get("from_date"): - conditions += "AND so.transaction_date >= '%s'" % filters.from_date - - if filters.get("to_date"): - conditions += "AND so.transaction_date <= '%s'" % filters.to_date - - if filters.get("item_code"): - conditions += "AND so_item.item_code = %s" % frappe.db.escape(filters.item_code) - - if filters.get("customer"): - conditions += "AND so.customer = %s" % frappe.db.escape(filters.customer) - - return conditions - - def get_customer_details(): details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"]) customer_details = {} @@ -187,29 +188,50 @@ def get_item_details(): def get_sales_order_details(company_list, filters): - conditions = get_conditions(filters) + db_so = frappe.qb.DocType("Sales Order") + db_so_item = frappe.qb.DocType("Sales Order Item") - return frappe.db.sql( - """ - SELECT - so_item.item_code, so_item.description, so_item.qty, - so_item.uom, so_item.base_rate, so_item.base_amount, - so.name, so.transaction_date, so.customer,so.territory, - so.project, so_item.delivered_qty, - so_item.billed_amt, so.company - FROM - `tabSales Order` so, `tabSales Order Item` so_item - WHERE - so.name = so_item.parent - AND so.company in ({0}) - AND so.docstatus = 1 {1} - """.format( - ",".join(["%s"] * len(company_list)), conditions - ), - tuple(company_list), - as_dict=1, + query = ( + frappe.qb.from_(db_so) + .inner_join(db_so_item) + .on(db_so_item.parent == db_so.name) + .select( + db_so.name, + db_so.customer, + db_so.transaction_date, + db_so.territory, + db_so.project, + db_so.company, + db_so_item.item_code, + db_so_item.description, + db_so_item.qty, + db_so_item.uom, + db_so_item.base_rate, + db_so_item.base_amount, + db_so_item.delivered_qty, + (db_so_item.billed_amt * db_so.conversion_rate).as_("billed_amt"), + ) + .where(db_so.docstatus == 1) + .where(db_so.company.isin(tuple(company_list))) ) + if filters.get("item_group"): + query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_group)) + + if filters.get("from_date"): + query = query.where(db_so.transaction_date >= filters.from_date) + + if filters.get("to_date"): + query = query.where(db_so.transaction_date <= filters.to_date) + + if filters.get("item_code"): + query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_code)) + + if filters.get("customer"): + query = query.where(db_so.customer == filters.customer) + + return query.run(as_dict=1) + def get_chart_data(data): item_wise_sales_map = {} diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 991ac719cdc..990d736baa4 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -103,6 +103,11 @@ function get_filters() { return options } }, + { + "fieldname":"only_immediate_upcoming_term", + "label": __("Show only the Immediate Upcoming Term"), + "fieldtype": "Check", + }, ] return filters; } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 8bf56865a7d..3682c5fd62e 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -4,6 +4,7 @@ import frappe from frappe import _, qb, query_builder from frappe.query_builder import Criterion, functions +from frappe.utils.dateutils import getdate def get_columns(): @@ -208,6 +209,7 @@ def get_so_with_invoices(filters): ) .where( (so.docstatus == 1) + & (so.status.isin(["To Deliver and Bill", "To Bill"])) & (so.payment_terms_template != "NULL") & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) @@ -291,6 +293,18 @@ def filter_on_calculated_status(filters, sales_orders): return sales_orders +def filter_for_immediate_upcoming_term(filters, sales_orders): + if filters.only_immediate_upcoming_term and sales_orders: + immediate_term_found = set() + filtered_data = [] + for order in sales_orders: + if order.name not in immediate_term_found and order.due_date > getdate(): + filtered_data.append(order) + immediate_term_found.add(order.name) + return filtered_data + return sales_orders + + def execute(filters=None): columns = get_columns() sales_orders, so_invoices = get_so_with_invoices(filters) @@ -298,6 +312,8 @@ def execute(filters=None): sales_orders = filter_on_calculated_status(filters, sales_orders) + sales_orders = filter_for_immediate_upcoming_term(filters, sales_orders) + prepare_chart(sales_orders) data = sales_orders diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 63d339a839d..29691230f22 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -175,7 +175,9 @@ def prepare_data(data, so_elapsed_time, filters): # update existing entry so_row = sales_order_map[so_name] so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"])) - so_row["delay"] = min(so_row["delay"], row["delay"]) + so_row["delay"] = ( + min(so_row["delay"], row["delay"]) if row["delay"] and so_row["delay"] else so_row["delay"] + ) # sum numeric columns fields = [ diff --git a/erpnext/setup/doctype/designation/designation.json b/erpnext/setup/doctype/designation/designation.json index 2cbbb04ed91..a5b2ac9128a 100644 --- a/erpnext/setup/doctype/designation/designation.json +++ b/erpnext/setup/doctype/designation/designation.json @@ -31,7 +31,7 @@ "icon": "fa fa-bookmark", "idx": 1, "links": [], - "modified": "2022-06-28 17:10:26.853753", + "modified": "2023-02-10 01:53:41.319386", "modified_by": "Administrator", "module": "Setup", "name": "Designation", @@ -58,5 +58,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", - "states": [] + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js index 3680906057f..c3605bf0e8b 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js @@ -1,13 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - - -//--------- ONLOAD ------------- -cur_frm.cscript.onload = function(doc, cdt, cdn) { - -} - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} +// frappe.ui.form.on("Terms and Conditions", { +// refresh(frm) {} +// }); diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json index f14b243512f..f884864acfa 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json @@ -33,7 +33,6 @@ "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "in_list_view": 1, "label": "Disabled" }, { @@ -60,12 +59,14 @@ "default": "1", "fieldname": "selling", "fieldtype": "Check", + "in_list_view": 1, "label": "Selling" }, { "default": "1", "fieldname": "buying", "fieldtype": "Check", + "in_list_view": 1, "label": "Buying" }, { @@ -76,10 +77,11 @@ "icon": "icon-legal", "idx": 1, "links": [], - "modified": "2022-06-16 15:07:38.094844", + "modified": "2023-02-01 14:33:39.246532", "modified_by": "Administrator", "module": "Setup", "name": "Terms and Conditions", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -133,5 +135,6 @@ "quick_entry": 1, "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/setup_wizard/data/designation.txt b/erpnext/setup/setup_wizard/data/designation.txt new file mode 100644 index 00000000000..4c6d7bdea8a --- /dev/null +++ b/erpnext/setup/setup_wizard/data/designation.txt @@ -0,0 +1,31 @@ +Accountant +Administrative Assistant +Administrative Officer +Analyst +Associate +Business Analyst +Business Development Manager +Consultant +Chief Executive Officer +Chief Financial Officer +Chief Operating Officer +Chief Technology Officer +Customer Service Representative +Designer +Engineer +Executive Assistant +Finance Manager +HR Manager +Head of Marketing and Sales +Manager +Managing Director +Marketing Manager +Marketing Specialist +President +Product Manager +Project Manager +Researcher +Sales Representative +Secretary +Software Developer +Vice President diff --git a/erpnext/setup/setup_wizard/data/industry_type.py b/erpnext/setup/setup_wizard/data/industry_type.py deleted file mode 100644 index 0bc3f32eb09..00000000000 --- a/erpnext/setup/setup_wizard/data/industry_type.py +++ /dev/null @@ -1,57 +0,0 @@ -from frappe import _ - - -def get_industry_types(): - return [ - _("Accounting"), - _("Advertising"), - _("Aerospace"), - _("Agriculture"), - _("Airline"), - _("Apparel & Accessories"), - _("Automotive"), - _("Banking"), - _("Biotechnology"), - _("Broadcasting"), - _("Brokerage"), - _("Chemical"), - _("Computer"), - _("Consulting"), - _("Consumer Products"), - _("Cosmetics"), - _("Defense"), - _("Department Stores"), - _("Education"), - _("Electronics"), - _("Energy"), - _("Entertainment & Leisure"), - _("Executive Search"), - _("Financial Services"), - _("Food, Beverage & Tobacco"), - _("Grocery"), - _("Health Care"), - _("Internet Publishing"), - _("Investment Banking"), - _("Legal"), - _("Manufacturing"), - _("Motion Picture & Video"), - _("Music"), - _("Newspaper Publishers"), - _("Online Auctions"), - _("Pension Funds"), - _("Pharmaceuticals"), - _("Private Equity"), - _("Publishing"), - _("Real Estate"), - _("Retail & Wholesale"), - _("Securities & Commodity Exchanges"), - _("Service"), - _("Soap & Detergent"), - _("Software"), - _("Sports"), - _("Technology"), - _("Telecommunications"), - _("Television"), - _("Transportation"), - _("Venture Capital"), - ] diff --git a/erpnext/setup/setup_wizard/data/industry_type.txt b/erpnext/setup/setup_wizard/data/industry_type.txt new file mode 100644 index 00000000000..eadc689e312 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/industry_type.txt @@ -0,0 +1,51 @@ +Accounting +Advertising +Aerospace +Agriculture +Airline +Apparel & Accessories +Automotive +Banking +Biotechnology +Broadcasting +Brokerage +Chemical +Computer +Consulting +Consumer Products +Cosmetics +Defense +Department Stores +Education +Electronics +Energy +Entertainment & Leisure +Executive Search +Financial Services +Food, Beverage & Tobacco +Grocery +Health Care +Internet Publishing +Investment Banking +Legal +Manufacturing +Motion Picture & Video +Music +Newspaper Publishers +Online Auctions +Pension Funds +Pharmaceuticals +Private Equity +Publishing +Real Estate +Retail & Wholesale +Securities & Commodity Exchanges +Service +Soap & Detergent +Software +Sports +Technology +Telecommunications +Television +Transportation +Venture Capital diff --git a/erpnext/setup/setup_wizard/data/lead_source.txt b/erpnext/setup/setup_wizard/data/lead_source.txt new file mode 100644 index 00000000000..00ca1808bb5 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/lead_source.txt @@ -0,0 +1,10 @@ +Existing Customer +Reference +Advertisement +Cold Calling +Exhibition +Supplier Reference +Mass Mailing +Customer's Vendor +Campaign +Walk In diff --git a/erpnext/setup/setup_wizard/data/sales_partner_type.txt b/erpnext/setup/setup_wizard/data/sales_partner_type.txt new file mode 100644 index 00000000000..68e9b9ac732 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/sales_partner_type.txt @@ -0,0 +1,7 @@ +Channel Partner +Distributor +Dealer +Agent +Retailer +Implementation Partner +Reseller diff --git a/erpnext/setup/setup_wizard/data/sales_stage.txt b/erpnext/setup/setup_wizard/data/sales_stage.txt new file mode 100644 index 00000000000..2808ce79855 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/sales_stage.txt @@ -0,0 +1,8 @@ +Prospecting +Qualification +Needs Analysis +Value Proposition +Identifying Decision Makers +Perception Analysis +Proposal/Price Quote +Negotiation/Review diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index aadc98989fa..ace5cca0b02 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -4,7 +4,6 @@ import frappe from frappe import _ from frappe.utils import cstr, getdate -from .default_website import website_maker def create_fiscal_year_and_company(args): @@ -48,83 +47,6 @@ def enable_shopping_cart(args): # nosemgrep ).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", - " {1}".format( - fileurl, args.get("company_name") - ), - ) - - -def create_website(args): - website_maker(args) - - def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: diff --git a/erpnext/setup/setup_wizard/operations/default_website.py b/erpnext/setup/setup_wizard/operations/default_website.py deleted file mode 100644 index 40b02b35dfd..00000000000 --- a/erpnext/setup/setup_wizard/operations/default_website.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe -from frappe import _ -from frappe.utils import nowdate - - -class website_maker(object): - def __init__(self, args): - self.args = args - self.company = args.company_name - self.tagline = args.company_tagline - self.user = args.get("email") - self.make_web_page() - self.make_website_settings() - self.make_blog() - - def make_web_page(self): - # home page - homepage = frappe.get_doc("Homepage", "Homepage") - homepage.company = self.company - homepage.tag_line = self.tagline - homepage.setup_items() - homepage.save() - - def make_website_settings(self): - # update in home page in settings - website_settings = frappe.get_doc("Website Settings", "Website Settings") - website_settings.home_page = "home" - website_settings.brand_html = self.company - website_settings.copyright = self.company - website_settings.top_bar_items = [] - website_settings.append( - "top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"} - ) - website_settings.append( - "top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"} - ) - website_settings.append( - "top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"} - ) - website_settings.save() - - def make_blog(self): - blog_category = frappe.get_doc( - {"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")} - ).insert() - - if not self.user: - # Admin setup - return - - blogger = frappe.new_doc("Blogger") - user = frappe.get_doc("User", self.user) - blogger.user = self.user - blogger.full_name = user.first_name + (" " + user.last_name if user.last_name else "") - blogger.short_name = user.first_name.lower() - blogger.avatar = user.user_image - blogger.insert() - - frappe.get_doc( - { - "doctype": "Blog Post", - "title": "Welcome", - "published": 1, - "published_on": nowdate(), - "blogger": blogger.name, - "blog_category": blog_category.name, - "blog_intro": "My First Blog", - "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(), - } - ).insert() - - -def test(): - frappe.delete_doc("Web Page", "test-company") - frappe.delete_doc("Blog Post", "welcome") - frappe.delete_doc("Blogger", "administrator") - frappe.delete_doc("Blog Category", "general") - website_maker( - { - "company": "Test Company", - "company_tagline": "Better Tools for Everyone", - "name": "Administrator", - } - ) - frappe.db.commit() diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 4d9b871e5e7..6bc17718ae0 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -4,6 +4,7 @@ import json import os +from pathlib import Path import frappe from frappe import _ @@ -16,28 +17,10 @@ from frappe.utils import cstr, getdate from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -default_lead_sources = [ - "Existing Customer", - "Reference", - "Advertisement", - "Cold Calling", - "Exhibition", - "Supplier Reference", - "Mass Mailing", - "Customer's Vendor", - "Campaign", - "Walk In", -] -default_sales_partner_type = [ - "Channel Partner", - "Distributor", - "Dealer", - "Agent", - "Retailer", - "Implementation Partner", - "Reseller", -] +def read_lines(filename: str) -> list[str]: + """Return a list of lines from a file in the data directory.""" + return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines() def install(country=None): @@ -85,7 +68,11 @@ def install(country=None): # Stock Entry Type {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, - {"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, + { + "doctype": "Stock Entry Type", + "name": "Material Transfer", + "purpose": "Material Transfer", + }, {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, { @@ -103,22 +90,6 @@ def install(country=None): "name": "Material Consumption for Manufacture", "purpose": "Material Consumption for Manufacture", }, - # Designation - {"doctype": "Designation", "designation_name": _("CEO")}, - {"doctype": "Designation", "designation_name": _("Manager")}, - {"doctype": "Designation", "designation_name": _("Analyst")}, - {"doctype": "Designation", "designation_name": _("Engineer")}, - {"doctype": "Designation", "designation_name": _("Accountant")}, - {"doctype": "Designation", "designation_name": _("Secretary")}, - {"doctype": "Designation", "designation_name": _("Associate")}, - {"doctype": "Designation", "designation_name": _("Administrative Officer")}, - {"doctype": "Designation", "designation_name": _("Business Development Manager")}, - {"doctype": "Designation", "designation_name": _("HR Manager")}, - {"doctype": "Designation", "designation_name": _("Project Manager")}, - {"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")}, - {"doctype": "Designation", "designation_name": _("Software Developer")}, - {"doctype": "Designation", "designation_name": _("Designer")}, - {"doctype": "Designation", "designation_name": _("Researcher")}, # territory: with two default territories, one for home country and one named Rest of the World { "doctype": "Territory", @@ -291,28 +262,18 @@ def install(country=None): {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")}, - # Sales Stages - {"doctype": "Sales Stage", "stage_name": _("Prospecting")}, - {"doctype": "Sales Stage", "stage_name": _("Qualification")}, - {"doctype": "Sales Stage", "stage_name": _("Needs Analysis")}, - {"doctype": "Sales Stage", "stage_name": _("Value Proposition")}, - {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, - {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, - {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, # Warehouse Type {"doctype": "Warehouse Type", "name": "Transit"}, ] - from erpnext.setup.setup_wizard.data.industry_type import get_industry_types - - records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] - # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] - records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] - - records += [ - {"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type - ] + for doctype, title_field, filename in ( + ("Designation", "designation_name", "designation.txt"), + ("Sales Stage", "stage_name", "sales_stage.txt"), + ("Industry Type", "industry", "industry_type.txt"), + ("Lead Source", "source_name", "lead_source.txt"), + ("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"), + ): + records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)] base_path = frappe.get_app_path("erpnext", "stock", "doctype") response = frappe.read_file( @@ -335,16 +296,11 @@ def install(country=None): make_default_records() make_records(records) set_up_address_templates(default_country=country) - set_more_defaults() - update_global_search_doctypes() - - -def set_more_defaults(): - # Do more setup stuff that can be done here with no dependencies update_selling_defaults() update_buying_defaults() add_uom_data() update_item_variant_settings() + update_global_search_doctypes() def update_selling_defaults(): @@ -381,7 +337,7 @@ def add_uom_data(): ) for d in uoms: if not frappe.db.exists("UOM", _(d.get("uom_name"))): - uom_doc = frappe.get_doc( + frappe.get_doc( { "doctype": "UOM", "uom_name": _(d.get("uom_name")), @@ -402,9 +358,10 @@ def add_uom_data(): frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() if not frappe.db.exists( - "UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} + "UOM Conversion Factor", + {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}, ): - uom_conversion = frappe.get_doc( + frappe.get_doc( { "doctype": "UOM Conversion Factor", "category": _(d.get("category")), @@ -412,7 +369,7 @@ def add_uom_data(): "to_uom": _(d.get("to_uom")), "value": d.get("value"), } - ).insert(ignore_permissions=True) + ).db_insert() def add_market_segments(): @@ -468,7 +425,7 @@ def install_company(args): make_records(records) -def install_defaults(args=None): +def install_defaults(args=None): # nosemgrep records = [ # Price Lists { @@ -493,7 +450,7 @@ def install_defaults(args=None): # enable default currency frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) - frappe.db.set_value("Stock Settings", None, "email_footer_address", args.get("company_name")) + frappe.db.set_single_value("Stock Settings", "email_footer_address", args.get("company_name")) set_global_defaults(args) update_stock_settings() @@ -540,7 +497,8 @@ def create_bank_account(args): company_name = args.get("company_name") bank_account_group = frappe.db.get_value( - "Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} + "Account", + {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name}, ) if bank_account_group: bank_account = frappe.get_doc( diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 2f77dd6ae56..49ba78c63a4 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -158,6 +158,7 @@ def make_taxes_and_charges_template(company_name, doctype, template): # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True + doc.flags.ignore_mandatory = True doc.insert(ignore_permissions=True) return doc diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index bd86a5b9693..65b268e385c 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -5,7 +5,6 @@ import frappe from frappe import _ -from .operations import company_setup from .operations import install_fixtures as fixtures @@ -35,7 +34,6 @@ def get_setup_stages(args=None): "fail_msg": "Failed to set defaults", "tasks": [ {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, - {"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")}, ], }, { @@ -60,12 +58,6 @@ def setup_defaults(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): frappe.local.message_log = [] login_as_first_user(args) @@ -81,5 +73,4 @@ def setup_complete(args=None): stage_fixtures(args) setup_company(args) setup_defaults(args) - stage_four(args) fin(args) diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index bb120eaa6b3..62936fcfb89 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -25,6 +25,12 @@ def boot_session(bootinfo): frappe.db.get_single_value("CRM Settings", "default_valid_till") ) + bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint( + frappe.db.get_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" + ) + ) + # if no company, show a dialog box to create a new company bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 1be528f1dd1..b09b715275e 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -42,7 +42,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard { let warehouse = unescape(element.attr('data-warehouse')); let actual_qty = unescape(element.attr('data-actual_qty')); let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry'))); - let entry_type = action === "Move" ? "Material Transfer" : null; + let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt"; if (disable_quick_entry) { open_stock_entry(item, warehouse, entry_type); @@ -63,11 +63,19 @@ erpnext.stock.ItemDashboard = class ItemDashboard { function open_stock_entry(item, warehouse, entry_type) { frappe.model.with_doctype('Stock Entry', function () { var doc = frappe.model.get_new_doc('Stock Entry'); - if (entry_type) doc.stock_entry_type = entry_type; + if (entry_type) { + doc.stock_entry_type = entry_type; + } var row = frappe.model.add_child(doc, 'items'); row.item_code = item; - row.s_warehouse = warehouse; + + if (entry_type === "Material Transfer") { + row.s_warehouse = warehouse; + } + else { + row.t_warehouse = warehouse; + } frappe.set_route('Form', doc.doctype, doc.name); }); diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 9f409d4b96a..72654e6f816 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -159,13 +159,18 @@ def update_qty(bin_name, args): last_sle_qty = ( frappe.qb.from_(sle) .select(sle.qty_after_transaction) - .where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse"))) + .where( + (sle.item_code == args.get("item_code")) + & (sle.warehouse == args.get("warehouse")) + & (sle.is_cancelled == 0) + ) .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc) .orderby(sle.creation, order=Order.desc) .limit(1) .run() ) + actual_qty = 0.0 if last_sle_qty: actual_qty = last_sle_qty[0][0] diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 165a56b7839..0c1f82029e6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -521,6 +521,7 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", + "label": "Delivery Note Item", "oldfieldname": "delivery_note_details", "oldfieldtype": "Table", "options": "Delivery Note Item", @@ -666,6 +667,7 @@ { "fieldname": "taxes", "fieldtype": "Table", + "label": "Sales Taxes and Charges", "oldfieldname": "other_charges", "oldfieldtype": "Table", "options": "Sales Taxes and Charges" @@ -1401,7 +1403,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:38:53.067799", + "modified": "2023-02-14 04:45:44.179670", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index a1df764ea9d..9f9f5cbe2a4 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -228,6 +228,7 @@ class DeliveryNote(SellingController): def on_submit(self): self.validate_packed_qty() + self.update_pick_list_status() # Check for Approving Authority frappe.get_doc("Authorization Control").validate_approving_authority( @@ -313,6 +314,11 @@ class DeliveryNote(SellingController): if has_error: raise frappe.ValidationError + def update_pick_list_status(self): + from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status + + update_pick_list_status(self.pick_list) + def check_next_docstatus(self): submit_rv = frappe.db.sql( """select t1.name diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 9e6f3bc9321..6ff3ed3e8e5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -14,7 +14,7 @@ frappe.listview_settings['Delivery Note'] = { return [__("Completed"), "green", "per_billed,=,100"]; } }, - onload: function (listview) { + onload: function (doclist) { const action = () => { const selected_docs = doclist.get_checked_items(); const docnames = doclist.get_checked_items(true); @@ -56,14 +56,14 @@ frappe.listview_settings['Delivery Note'] = { // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); - listview.page.add_action_item(__('Create Delivery Trip'), action); + doclist.page.add_action_item(__('Create Delivery Trip'), action); - listview.page.add_action_item(__("Sales Invoice"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice"); + doclist.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice"); }); - listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip"); + doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip"); }); } }; diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index ba1023ac691..0310682a2c1 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', { if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger && frm.doc.__onload.has_stock_ledger.length) { let allow_to_edit_fields = ['disabled', 'fetch_from_parent', - 'type_of_transaction', 'condition']; + 'type_of_transaction', 'condition', 'mandatory_depends_on']; frm.fields.forEach((field) => { if (!in_list(allow_to_edit_fields, field.df.fieldname)) { diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 4397e11f540..eb6102a436e 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -24,6 +24,9 @@ "istable", "applicable_condition_example_section", "condition", + "conditional_mandatory_section", + "reqd", + "mandatory_depends_on", "conditional_rule_examples_section", "html_19" ], @@ -153,11 +156,28 @@ "fieldname": "conditional_rule_examples_section", "fieldtype": "Section Break", "label": "Conditional Rule Examples" + }, + { + "description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.", + "fieldname": "mandatory_depends_on", + "fieldtype": "Small Text", + "label": "Mandatory Depends On" + }, + { + "fieldname": "conditional_mandatory_section", + "fieldtype": "Section Break", + "label": "Mandatory Section" + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "label": "Mandatory" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-11-15 15:50:16.767105", + "modified": "2023-01-31 13:44:38.507698", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 009548abf26..db2b5d0a6b6 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -126,6 +126,8 @@ class InventoryDimension(Document): insert_after="inventory_dimension", options=self.reference_document, label=self.dimension_name, + reqd=self.reqd, + mandatory_depends_on=self.mandatory_depends_on, ), ] @@ -142,6 +144,8 @@ class InventoryDimension(Document): "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} ) and not field_exists("Stock Ledger Entry", self.target_fieldname): dimension_field = dimension_fields[1] + dimension_field["mandatory_depends_on"] = "" + dimension_field["reqd"] = 0 dimension_field["fieldname"] = self.target_fieldname custom_fields["Stock Ledger Entry"] = dimension_field diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index edff3fd556c..28b1ed96f0d 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -85,6 +85,9 @@ class TestInventoryDimension(FrappeTestCase): condition="parent.purpose == 'Material Issue'", ) + inv_dim1.reqd = 0 + inv_dim1.save() + create_inventory_dimension( reference_document="Shelf", type_of_transaction="Inward", @@ -205,6 +208,48 @@ 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(): if not frappe.db.exists("DocType", "Shelf"): @@ -251,6 +296,22 @@ def prepare_test_data(): create_warehouse("Rack Warehouse") + if not frappe.db.exists("DocType", "Pallet"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Pallet", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:pallet_name", + "fields": [{"label": "Pallet Name", "fieldname": "pallet_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index e61f0f514e3..5bcb05aa988 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -894,6 +894,12 @@ function open_form(frm, doctype, child_doctype, parentfield) { new_child_doc.uom = frm.doc.stock_uom; new_child_doc.description = frm.doc.description; - frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + frappe.run_serially([ + () => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc), + () => { + frappe.flags.ignore_company_party_validation = true; + frappe.model.trigger("item_code", frm.doc.name, new_child_doc); + } + ]) }); } diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 629e50efeb9..34adbebc07c 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -706,7 +706,7 @@ "depends_on": "enable_deferred_expense", "fieldname": "no_of_months_exp", "fieldtype": "Int", - "label": "No of Months" + "label": "No of Months (Expense)" }, { "collapsible": 1, @@ -911,7 +911,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-01-07 22:45:00.341745", + "modified": "2023-02-14 04:48:26.343620", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item_price/test_records.json b/erpnext/stock/doctype/item_price/test_records.json index 0a3d7e81985..afe5ad65b75 100644 --- a/erpnext/stock/doctype/item_price/test_records.json +++ b/erpnext/stock/doctype/item_price/test_records.json @@ -38,5 +38,19 @@ "price_list_rate": 1000, "valid_from": "2017-04-10", "valid_upto": "2017-04-17" + }, + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Buying Price List", + "price_list_rate": 100, + "supplier": "_Test Supplier" + }, + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Selling Price List", + "price_list_rate": 200, + "customer": "_Test Customer" } ] diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 5f05de6991b..c1f1b0d1352 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -110,8 +110,11 @@ frappe.ui.form.on('Material Request', { if (frm.doc.material_request_type === "Material Transfer") { add_create_pick_list_button(); - frm.add_custom_button(__("Transfer Material"), + frm.add_custom_button(__("Material Transfer"), () => frm.events.make_stock_entry(frm), __('Create')); + + frm.add_custom_button(__("Material Transfer (In Transit)"), + () => frm.events.make_in_transit_stock_entry(frm), __('Create')); } if (frm.doc.material_request_type === "Material Issue") { @@ -333,6 +336,46 @@ frappe.ui.form.on('Material Request', { }); }, + make_in_transit_stock_entry(frm) { + frappe.prompt( + [ + { + label: __('In Transit Warehouse'), + fieldname: 'in_transit_warehouse', + fieldtype: 'Link', + options: 'Warehouse', + reqd: 1, + get_query: () => { + return{ + filters: { + 'company': frm.doc.company, + 'is_group': 0, + 'warehouse_type': 'Transit' + } + } + } + } + ], + (values) => { + frappe.call({ + method: "erpnext.stock.doctype.material_request.material_request.make_in_transit_stock_entry", + args: { + source_name: frm.doc.name, + in_transit_warehouse: values.in_transit_warehouse + }, + callback: function(r) { + if (r.message) { + let doc = frappe.model.sync(r.message); + frappe.set_route('Form', doc[0].doctype, doc[0].name); + } + } + }) + }, + __('In Transit Transfer'), + __("Create Stock Entry") + ) + }, + create_pick_list: (frm) => { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.material_request.material_request.create_pick_list", @@ -366,10 +409,11 @@ frappe.ui.form.on('Material Request', { frappe.ui.form.on("Material Request Item", { qty: function (frm, doctype, name) { - var d = locals[doctype][name]; - if (flt(d.qty) < flt(d.min_order_qty)) { + const item = locals[doctype][name]; + if (flt(item.qty) < flt(item.min_order_qty)) { frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum Order Qty")); } + frm.events.get_item_data(frm, item, false); }, from_warehouse: function(frm, doctype, name) { diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 94f63a599b5..6426fe8015a 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -716,3 +716,14 @@ def create_pick_list(source_name, target_doc=None): doc.set_item_locations() return doc + + +@frappe.whitelist() +def make_in_transit_stock_entry(source_name, in_transit_warehouse): + ste_doc = make_stock_entry(source_name) + ste_doc.add_to_transit = 1 + + for row in ste_doc.items: + row.t_warehouse = in_transit_warehouse + + return ste_doc diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index f0a94997fe8..a707c74c7db 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -11,6 +11,7 @@ from frappe.utils import flt, today from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.material_request.material_request import ( + make_in_transit_stock_entry, make_purchase_order, make_stock_entry, make_supplier_quotation, @@ -56,6 +57,22 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(len(se.get("items")), len(mr.get("items"))) + def test_in_transit_make_stock_entry(self): + mr = frappe.copy_doc(test_records[0]).insert() + + self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name) + + mr = frappe.get_doc("Material Request", mr.name) + mr.material_request_type = "Material Transfer" + mr.submit() + + in_transit_warehouse = get_in_transit_warehouse(mr.company) + se = make_in_transit_stock_entry(mr.name, in_transit_warehouse) + + self.assertEqual(se.doctype, "Stock Entry") + for row in se.get("items"): + self.assertEqual(row.t_warehouse, in_transit_warehouse) + def _insert_stock_entry(self, qty1, qty2, warehouse=None): se = frappe.get_doc( { @@ -742,6 +759,36 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(existing_requested_qty, current_requested_qty) +def get_in_transit_warehouse(company): + if not frappe.db.exists("Warehouse Type", "Transit"): + frappe.get_doc( + { + "doctype": "Warehouse Type", + "name": "Transit", + } + ).insert() + + in_transit_warehouse = frappe.db.exists( + "Warehouse", {"warehouse_type": "Transit", "company": company} + ) + + if not in_transit_warehouse: + in_transit_warehouse = ( + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Transit", + "warehouse_type": "Transit", + "company": company, + } + ) + .insert() + .name + ) + + return in_transit_warehouse + + def make_material_request(**args): args = frappe._dict(args) mr = frappe.new_doc("Material Request") diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index e1c3f0f5061..7259dc00a81 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -26,7 +26,8 @@ "locations", "amended_from", "print_settings_section", - "group_same_items" + "group_same_items", + "status" ], "fields": [ { @@ -168,11 +169,26 @@ "fieldtype": "Data", "label": "Customer Name", "read_only": 1 + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Draft\nOpen\nCompleted\nCancelled", + "print_hide": 1, + "read_only": 1, + "report_hide": 1, + "reqd": 1, + "search_index": 1 } ], "is_submittable": 1, "links": [], - "modified": "2022-07-19 11:03:04.442174", + "modified": "2023-01-24 10:33:43.244476", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", @@ -244,4 +260,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 808f19e2740..bf3b5ddc54a 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -11,7 +11,8 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.query_builder import Case -from frappe.query_builder.functions import IfNull, Locate, Sum +from frappe.query_builder.custom import GROUP_CONCAT +from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum from frappe.utils import cint, floor, flt, today from frappe.utils.nestedset import get_descendants_of @@ -77,15 +78,32 @@ class PickList(Document): ) def on_submit(self): + self.update_status() self.update_bundle_picked_qty() self.update_reference_qty() self.update_sales_order_picking_status() def on_cancel(self): + self.update_status() self.update_bundle_picked_qty() self.update_reference_qty() self.update_sales_order_picking_status() + def update_status(self, status=None, update_modified=True): + if not status: + if self.docstatus == 0: + status = "Draft" + elif self.docstatus == 1: + if self.status == "Draft": + status = "Open" + elif target_document_exists(self.name, self.purpose): + status = "Completed" + elif self.docstatus == 2: + status = "Cancelled" + + if status: + frappe.db.set_value("Pick List", self.name, "status", status, update_modified=update_modified) + def update_reference_qty(self): packed_items = [] so_items = [] @@ -162,6 +180,7 @@ class PickList(Document): def set_item_locations(self, save=False): self.validate_for_qty() items = self.aggregate_item_qty() + picked_items_details = self.get_picked_items_details(items) self.item_location_map = frappe._dict() from_warehouses = None @@ -180,7 +199,11 @@ class PickList(Document): self.item_location_map.setdefault( item_code, get_available_item_locations( - item_code, from_warehouses, self.item_count_map.get(item_code), self.company + item_code, + from_warehouses, + self.item_count_map.get(item_code), + self.company, + picked_item_details=picked_items_details.get(item_code), ), ) @@ -309,6 +332,56 @@ class PickList(Document): already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)), ) + def get_picked_items_details(self, items): + picked_items = frappe._dict() + + if items: + pi = frappe.qb.DocType("Pick List") + pi_item = frappe.qb.DocType("Pick List Item") + query = ( + frappe.qb.from_(pi) + .inner_join(pi_item) + .on(pi.name == pi_item.parent) + .select( + pi_item.item_code, + pi_item.warehouse, + pi_item.batch_no, + Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_( + "picked_qty" + ), + Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"), + ) + .where( + (pi_item.item_code.isin([x.item_code for x in items])) + & ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0)) + & (pi.status != "Completed") + & (pi_item.docstatus != 2) + ) + .groupby( + pi_item.item_code, + pi_item.warehouse, + pi_item.batch_no, + ) + ) + + if self.name: + query = query.where(pi_item.parent != self.name) + + items_data = query.run(as_dict=True) + + for item_data in items_data: + key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse + serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None + data = {"picked_qty": item_data.picked_qty} + if serial_no: + data["serial_no"] = serial_no + if item_data.item_code not in picked_items: + picked_items[item_data.item_code] = {key: data} + else: + picked_items[item_data.item_code][key] = data + + return picked_items + def _get_product_bundles(self) -> Dict[str, str]: # Dict[so_item_row: item_code] product_bundles = {} @@ -346,29 +419,30 @@ class PickList(Document): return int(flt(min(possible_bundles), precision or 6)) +def update_pick_list_status(pick_list): + if pick_list: + doc = frappe.get_doc("Pick List", pick_list) + doc.run_method("update_status") + + def get_picked_items_qty(items) -> List[Dict]: - return frappe.db.sql( - f""" - SELECT - sales_order_item, - item_code, - sales_order, - SUM(stock_qty) AS stock_qty, - SUM(picked_qty) AS picked_qty - FROM - `tabPick List Item` - WHERE - sales_order_item IN ( - {", ".join(frappe.db.escape(d) for d in items)} - ) - AND docstatus = 1 - GROUP BY - sales_order_item, - sales_order - FOR UPDATE - """, - as_dict=1, - ) + pi_item = frappe.qb.DocType("Pick List Item") + return ( + frappe.qb.from_(pi_item) + .select( + pi_item.sales_order_item, + pi_item.item_code, + pi_item.sales_order, + Sum(pi_item.stock_qty).as_("stock_qty"), + Sum(pi_item.picked_qty).as_("picked_qty"), + ) + .where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items))) + .groupby( + pi_item.sales_order_item, + pi_item.sales_order, + ) + .for_update() + ).run(as_dict=True) def validate_item_locations(pick_list): @@ -434,31 +508,38 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) def get_available_item_locations( - item_code, from_warehouses, required_qty, company, ignore_validation=False + item_code, + from_warehouses, + required_qty, + company, + ignore_validation=False, + picked_item_details=None, ): locations = [] + total_picked_qty = ( + sum([v.get("picked_qty") for k, v in picked_item_details.items()]) if picked_item_details else 0 + ) has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") if has_batch_no and has_serial_no: locations = get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) elif has_serial_no: locations = get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) elif has_batch_no: locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) else: locations = get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) total_qty_available = sum(location.get("qty") for location in locations) - remaining_qty = required_qty - total_qty_available if remaining_qty > 0 and not ignore_validation: @@ -469,25 +550,60 @@ def get_available_item_locations( title=_("Insufficient Stock"), ) + if picked_item_details: + for location in list(locations): + key = ( + (location["warehouse"], location["batch_no"]) + if location.get("batch_no") + else location["warehouse"] + ) + + if key in picked_item_details: + picked_detail = picked_item_details[key] + + if picked_detail.get("serial_no") and location.get("serial_no"): + location["serial_no"] = list( + set(location["serial_no"]).difference(set(picked_detail["serial_no"])) + ) + location["qty"] = len(location["serial_no"]) + else: + location["qty"] -= picked_detail.get("picked_qty") + + if location["qty"] < 1: + locations.remove(location) + + total_qty_available = sum(location.get("qty") for location in locations) + remaining_qty = required_qty - total_qty_available + + if remaining_qty > 0 and not ignore_validation: + frappe.msgprint( + _("{0} units of Item {1} is picked in another Pick List.").format( + remaining_qty, frappe.get_desk_link("Item", item_code) + ), + title=_("Already Picked"), + ) + return locations def get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): - filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]}) + sn = frappe.qb.DocType("Serial No") + query = ( + frappe.qb.from_(sn) + .select(sn.name, sn.warehouse) + .where((sn.item_code == item_code) & (sn.company == company)) + .orderby(sn.purchase_date) + .limit(cint(required_qty + total_picked_qty)) + ) if from_warehouses: - filters.warehouse = ["in", from_warehouses] + query = query.where(sn.warehouse.isin(from_warehouses)) + else: + query = query.where(Coalesce(sn.warehouse, "") != "") - serial_nos = frappe.get_all( - "Serial No", - fields=["name", "warehouse"], - filters=filters, - limit=required_qty, - order_by="purchase_date", - as_list=1, - ) + serial_nos = query.run(as_list=True) warehouse_serial_nos_map = frappe._dict() for serial_no, warehouse in serial_nos: @@ -501,7 +617,7 @@ def get_available_item_locations_for_serialized_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") 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) .having(Sum(sle.actual_qty) > 0) .orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse) + .limit(cint(required_qty + total_picked_qty)) ) if from_warehouses: @@ -530,53 +647,58 @@ def get_available_item_locations_for_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 locations = get_available_item_locations_for_batched_item( item_code, from_warehouses, required_qty, company ) - filters = frappe._dict( - {"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""} - ) + if locations: + sn = frappe.qb.DocType("Serial No") + conditions = (sn.item_code == item_code) & (sn.company == company) - # Get Serial Nos by FIFO for Batch No - for location in locations: - filters.batch_no = location.batch_no - filters.warehouse = location.warehouse - location.qty = ( - required_qty if location.qty > required_qty else location.qty - ) # if extra qty in batch + for location in locations: + location.qty = ( + required_qty if location.qty > required_qty else location.qty + ) # if extra qty in batch - serial_nos = frappe.get_list( - "Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date" - ) + serial_nos = ( + frappe.qb.from_(sn) + .select(sn.name) + .where( + (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) + ) + .orderby(sn.purchase_date) + .limit(cint(location.qty + total_picked_qty)) + ).run(as_dict=True) - serial_nos = [sn.name for sn in serial_nos] - location.serial_no = serial_nos + serial_nos = [sn.name for sn in serial_nos] + location.serial_no = serial_nos + location.qty = len(serial_nos) return locations -def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): - # gets all items available in different warehouses - warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")] - - filters = frappe._dict( - {"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]} +def get_available_item_locations_for_other_item( + item_code, from_warehouses, required_qty, company, total_picked_qty=0 +): + bin = frappe.qb.DocType("Bin") + query = ( + frappe.qb.from_(bin) + .select(bin.warehouse, bin.actual_qty.as_("qty")) + .where((bin.item_code == item_code) & (bin.actual_qty > 0)) + .orderby(bin.creation) + .limit(cint(required_qty + total_picked_qty)) ) if from_warehouses: - filters.warehouse = ["in", from_warehouses] + query = query.where(bin.warehouse.isin(from_warehouses)) + else: + wh = frappe.qb.DocType("Warehouse") + query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company)) - item_locations = frappe.get_all( - "Bin", - fields=["warehouse", "actual_qty as qty"], - filters=filters, - limit=required_qty, - order_by="creation", - ) + item_locations = query.run(as_dict=True) return item_locations diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js new file mode 100644 index 00000000000..ad88b0a682f --- /dev/null +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -0,0 +1,14 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.listview_settings['Pick List'] = { + get_indicator: function (doc) { + const status_colors = { + "Draft": "grey", + "Open": "orange", + "Completed": "green", + "Cancelled": "red", + }; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; + }, +}; \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 43acdf08360..1254fe3927f 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -414,6 +414,7 @@ class TestPickList(FrappeTestCase): pick_list.submit() delivery_note = create_delivery_note(pick_list.name) + pick_list.load_from_db() self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) @@ -663,3 +664,147 @@ class TestPickList(FrappeTestCase): self.assertEqual(dn.items[0].rate, 42) so.reload() self.assertEqual(so.per_delivered, 100) + + def test_pick_list_status(self): + warehouse = "_Test Warehouse - _TC" + item = make_item(properties={"maintain_stock": 1}).name + make_stock_entry(item=item, to_warehouse=warehouse, qty=10) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl = create_pick_list(so.name) + pl.save() + pl.reload() + self.assertEqual(pl.status, "Draft") + + pl.submit() + pl.reload() + self.assertEqual(pl.status, "Open") + + dn = create_delivery_note(pl.name) + dn.save() + pl.reload() + self.assertEqual(pl.status, "Open") + + dn.submit() + pl.reload() + self.assertEqual(pl.status, "Completed") + + dn.cancel() + pl.reload() + self.assertEqual(pl.status, "Completed") + + pl.cancel() + pl.reload() + self.assertEqual(pl.status, "Cancelled") + + def test_consider_existing_pick_list(self): + def create_items(items_properties): + items = [] + + for properties in items_properties: + properties.update({"maintain_stock": 1}) + item_code = make_item(properties=properties).name + properties.update({"item_code": item_code}) + items.append(properties) + + return items + + def create_stock_entries(items): + warehouses = ["Stores - _TC", "Finished Goods - _TC"] + + for item in items: + for warehouse in warehouses: + se = make_stock_entry( + item=item.get("item_code"), + to_warehouse=warehouse, + qty=5, + ) + + def get_item_list(items, qty, warehouse="All Warehouses - _TC"): + return [ + { + "item_code": item.get("item_code"), + "qty": qty, + "warehouse": warehouse, + } + for item in items + ] + + def get_picked_items_details(pick_list_doc): + items_data = {} + + for location in pick_list_doc.locations: + key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse + serial_no = [x for x in location.serial_no.split("\n") if x] if location.serial_no else None + data = {"picked_qty": location.picked_qty} + if serial_no: + data["serial_no"] = serial_no + if location.item_code not in items_data: + items_data[location.item_code] = {key: data} + else: + items_data[location.item_code][key] = data + + return items_data + + # Step - 1: Setup - Create Items and Stock Entries + items_properties = [ + { + "valuation_rate": 100, + }, + { + "valuation_rate": 200, + "has_batch_no": 1, + "create_new_batch": 1, + }, + { + "valuation_rate": 300, + "has_serial_no": 1, + "serial_no_series": "SNO.###", + }, + { + "valuation_rate": 400, + "has_batch_no": 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "SNO.###", + }, + ] + + items = create_items(items_properties) + create_stock_entries(items) + + # Step - 2: Create Sales Order [1] + so1 = make_sales_order(item_list=get_item_list(items, qty=6)) + + # Step - 3: Create and Submit Pick List [1] for Sales Order [1] + pl1 = create_pick_list(so1.name) + pl1.submit() + + # Step - 4: Create Sales Order [2] with same Item(s) as Sales Order [1] + so2 = make_sales_order(item_list=get_item_list(items, qty=4)) + + # Step - 5: Create Pick List [2] for Sales Order [2] + pl2 = create_pick_list(so2.name) + pl2.save() + + # Step - 6: Assert + picked_items_details = get_picked_items_details(pl1) + + for location in pl2.locations: + key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse + item_data = picked_items_details.get(location.item_code, {}).get(key, {}) + picked_qty = item_data.get("picked_qty", 0) + picked_serial_no = picked_items_details.get("serial_no", []) + bin_actual_qty = frappe.db.get_value( + "Bin", {"item_code": location.item_code, "warehouse": location.warehouse}, "actual_qty" + ) + + # Available Qty to pick should be equal to [Actual Qty - Picked Qty] + self.assertEqual(location.stock_qty, bin_actual_qty - picked_qty) + + # Serial No should not be in the Picked Serial No list + if location.serial_no: + a = set(picked_serial_no) + b = set([x for x in location.serial_no.split("\n") if x]) + self.assertSetEqual(b, b.difference(a)) diff --git a/erpnext/stock/doctype/price_list/test_records.json b/erpnext/stock/doctype/price_list/test_records.json index 7ca949c4026..e02a7adbd8b 100644 --- a/erpnext/stock/doctype/price_list/test_records.json +++ b/erpnext/stock/doctype/price_list/test_records.json @@ -31,5 +31,21 @@ "enabled": 1, "price_list_name": "_Test Price List Rest of the World", "selling": 1 + }, + { + "buying": 0, + "currency": "USD", + "doctype": "Price List", + "enabled": 1, + "price_list_name": "_Test Selling Price List", + "selling": 1 + }, + { + "buying": 1, + "currency": "USD", + "doctype": "Price List", + "enabled": 1, + "price_list_name": "_Test Buying Price List", + "selling": 0 } ] diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 9321c2c166b..2a9f091bd09 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -221,7 +221,7 @@ class QualityInspection(Document): def item_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond - from_doctype = cstr(filters.get("doctype")) + from_doctype = cstr(filters.get("from")) if not from_doctype or not frappe.db.exists("DocType", from_doctype): return [] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1755f28a1d4..7f69397fce9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -158,6 +158,7 @@ class StockEntry(StockController): self.validate_subcontract_order() self.update_subcontract_order_supplied_items() self.update_subcontracting_order_status() + self.update_pick_list_status() self.make_gl_entries() @@ -2276,6 +2277,11 @@ class StockEntry(StockController): update_subcontracting_order_status(self.subcontracting_order) + def update_pick_list_status(self): + from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status + + update_pick_list_status(self.pick_list) + def set_missing_values(self): "Updates rate and availability of all the items of mapped doc." self.set_transfer_qty() @@ -2488,7 +2494,7 @@ def get_uom_details(item_code, uom, qty): if not conversion_factor: frappe.msgprint( - _("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) + _("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) ) ret = {"uom": ""} else: diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 38bf0a5f9e5..cc06bd709ad 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1662,6 +1662,48 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(BatchExpiredError, se.save) + def test_negative_stock_reco(self): + from erpnext.controllers.stock_controller import BatchExpiredError + from erpnext.stock.doctype.batch.test_batch import make_new_batch + + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0) + + item_code = "Test Negative Item - 001" + item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) + + make_stock_entry( + item_code=item_code, + posting_date=add_days(today(), -3), + posting_time="00:00:00", + purpose="Material Receipt", + qty=10, + to_warehouse="_Test Warehouse - _TC", + do_not_save=True, + ) + + make_stock_entry( + item_code=item_code, + posting_date=today(), + posting_time="00:00:00", + purpose="Material Receipt", + qty=8, + from_warehouse="_Test Warehouse - _TC", + do_not_save=True, + ) + + sr_doc = create_stock_reconciliation( + purpose="Stock Reconciliation", + posting_date=add_days(today(), -3), + posting_time="00:00:00", + item_code=item_code, + warehouse="_Test Warehouse - _TC", + valuation_rate=10, + qty=7, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, sr_doc.submit) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 5af144110f0..b53f429edf2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -88,8 +88,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru update_party_blanket_order(args, out) + # Never try to find a customer price if customer is set in these Doctype + current_customer = args.customer + if args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: + args.customer = None + out.update(get_price_list_rate(args, item)) + args.customer = current_customer + if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index b5c6764224b..abbb33b2f16 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -62,7 +62,7 @@ def execute(filters=None): continue total_stock_value = sum(item_value[(item, item_group)]) - row = [item, item_group, total_stock_value] + row = [item, item_map[item]["item_name"], item_group, total_stock_value] fifo_queue = item_ageing[item]["fifo_queue"] average_age = 0.00 @@ -89,10 +89,11 @@ def get_columns(filters): """return columns""" columns = [ - _("Item") + ":Link/Item:180", - _("Item Group") + "::100", + _("Item") + ":Link/Item:150", + _("Item Name") + ":Link/Item:150", + _("Item Group") + "::120", _("Value") + ":Currency:120", - _("Age") + ":Float:80", + _("Age") + ":Float:120", ] return columns @@ -123,7 +124,7 @@ def get_warehouse_list(filters): def add_warehouse_column(columns, warehouse_list): if len(warehouse_list) > 1: - columns += [_("Total Qty") + ":Int:90"] + columns += [_("Total Qty") + ":Int:120"] for wh in warehouse_list: - columns += [_(wh.name) + ":Int:120"] + columns += [_(wh.name) + ":Int:100"] diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 14cedd2e8a9..439ed7a8e09 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -121,7 +121,7 @@ def get_reserved_qty(item_code, warehouse): and parenttype='Sales Order' and item_code != parent_item and exists (select * from `tabSales Order` so - where name = dnpi_in.parent and docstatus = 1 and status != 'Closed') + where name = dnpi_in.parent and docstatus = 1 and status not in ('On Hold', 'Closed')) ) dnpi) union (select stock_qty as dnpi_qty, qty as so_item_qty, @@ -131,7 +131,7 @@ def get_reserved_qty(item_code, warehouse): and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) and exists(select * from `tabSales Order` so where so.name = so_item.parent and so.docstatus = 1 - and so.status != 'Closed')) + and so.status not in ('On Hold', 'Closed'))) ) tab where so_item_qty >= so_item_delivered_qty diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5d75bfd05a3..08fc6fbd42f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1050,7 +1050,7 @@ class update_entries_after(object): frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) -def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): +def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" args["time_format"] = "%H:%i:%s" @@ -1076,13 +1076,13 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): posting_date < %(posting_date)s or ( posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) < time_format(%(posting_time)s, %(time_format)s) + time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s) ) ) order by timestamp(posting_date, posting_time) desc, creation desc limit 1 for update""".format( - voucher_condition=voucher_condition + operator=operator, voucher_condition=voucher_condition ), args, as_dict=1, @@ -1179,7 +1179,7 @@ def get_stock_ledger_entries( def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): return frappe.db.get_value( "Stock Ledger Entry", - {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]}, + {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0}, [ "item_code", "warehouse", @@ -1375,7 +1375,7 @@ def get_stock_reco_qty_shift(args): stock_reco_qty_shift = flt(args.actual_qty) else: # reco is being submitted - last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get( + last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get( "qty_after_transaction" ) diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py new file mode 100644 index 00000000000..b53e29e9e8e --- /dev/null +++ b/erpnext/stock/tests/test_get_item_details.py @@ -0,0 +1,40 @@ +import json + +import frappe +from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase + +from erpnext.stock.get_item_details import get_item_details + +test_ignore = ["BOM"] +test_dependencies = ["Customer", "Supplier", "Item", "Price List", "Item Price"] + + +class TestGetItemDetail(FrappeTestCase): + def setUp(self): + make_test_records("Price List") + super().setUp() + + def test_get_item_detail_purchase_order(self): + + args = frappe._dict( + { + "item_code": "_Test Item", + "company": "_Test Company", + "customer": "_Test Customer", + "conversion_rate": 1.0, + "price_list_currency": "USD", + "plc_conversion_rate": 1.0, + "doctype": "Purchase Order", + "name": None, + "supplier": "_Test Supplier", + "transaction_date": None, + "conversion_rate": 1.0, + "price_list": "_Test Buying Price List", + "is_subcontracted": 0, + "ignore_pricing_rule": 1, + "qty": 1, + } + ) + details = get_item_details(args) + self.assertEqual(details.get("price_list_rate"), 100) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index d054ce0f9d4..6a2983faaaf 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -2,6 +2,7 @@ # See license.txt import copy +from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase @@ -186,6 +187,40 @@ class TestSubcontractingOrder(FrappeTestCase): ) self.assertEqual(len(ste.items), len(rm_items)) + def test_make_rm_stock_entry_for_batch_items_with_less_transfer(self): + set_backflush_based_on("BOM") + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 5, + } + ] + + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + itemwise_transfer_qty = defaultdict(int) + for item in rm_items: + item["qty"] -= 1 + itemwise_transfer_qty[item["item_code"]] += item["qty"] + + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + + for row in scr.supplied_items: + self.assertEqual(row.consumed_qty, itemwise_transfer_qty.get(row.rm_item_code) + 1) + def test_update_reserved_qty_for_subcontracting(self): # Create RM Material Receipt make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index b6bef8c4a02..3a2c53f4e44 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -51,13 +51,31 @@ frappe.ui.form.on('Subcontracting Receipt', { } })); - frm.set_query("expense_account", "items", function () { + frm.set_query('expense_account', 'items', function () { return { - query: "erpnext.controllers.queries.get_expense_account", + query: 'erpnext.controllers.queries.get_expense_account', filters: { 'company': frm.doc.company } }; }); + frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { + var row = locals[cdt][cdn]; + return { + filters: { + item: row.item_code + } + } + }); + + let batch_no_field = frm.get_docfield("items", "batch_no"); + if (batch_no_field) { + batch_no_field.get_route_options_for_new_doc = function(row) { + return { + "item": row.doc.item_code + } + }; + } + frappe.db.get_single_value('Buying Settings', 'backflush_raw_materials_of_subcontract_based_on').then(val => { if (val == 'Material Transferred for Subcontract') { frm.fields_dict['supplied_items'].grid.grid_rows.forEach((grid_row) => { @@ -73,7 +91,7 @@ frappe.ui.form.on('Subcontracting Receipt', { refresh: (frm) => { if (frm.doc.docstatus > 0) { - frm.add_custom_button(__("Stock Ledger"), function () { + frm.add_custom_button(__('Stock Ledger'), function () { frappe.route_options = { voucher_no: frm.doc.name, from_date: frm.doc.posting_date, @@ -81,8 +99,8 @@ frappe.ui.form.on('Subcontracting Receipt', { company: frm.doc.company, show_cancelled_entries: frm.doc.docstatus === 2 }; - frappe.set_route("query-report", "Stock Ledger"); - }, __("View")); + frappe.set_route('query-report', 'Stock Ledger'); + }, __('View')); frm.add_custom_button(__('Accounting Ledger'), function () { frappe.route_options = { @@ -90,11 +108,11 @@ frappe.ui.form.on('Subcontracting Receipt', { from_date: frm.doc.posting_date, to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), company: frm.doc.company, - group_by: "Group by Voucher (Consolidated)", + group_by: 'Group by Voucher (Consolidated)', show_cancelled_entries: frm.doc.docstatus === 2 }; - frappe.set_route("query-report", "General Ledger"); - }, __("View")); + frappe.set_route('query-report', 'General Ledger'); + }, __('View')); } if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) { @@ -111,25 +129,25 @@ frappe.ui.form.on('Subcontracting Receipt', { frm.add_custom_button(__('Subcontracting Order'), function () { if (!frm.doc.supplier) { frappe.throw({ - title: __("Mandatory"), - message: __("Please Select a Supplier") + title: __('Mandatory'), + message: __('Please Select a Supplier') }); } erpnext.utils.map_current_doc({ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', - source_doctype: "Subcontracting Order", + source_doctype: 'Subcontracting Order', target: frm, setters: { supplier: frm.doc.supplier, }, get_query_filters: { docstatus: 1, - per_received: ["<", 100], + per_received: ['<', 100], company: frm.doc.company } }); - }, __("Get Items From")); + }, __('Get Items From')); } }, diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv index 173320d1fde..2337bcb4da9 100644 --- a/erpnext/translations/zh.csv +++ b/erpnext/translations/zh.csv @@ -3537,7 +3537,6 @@ Quality Feedback Template,质量反馈模板, Rules for applying different promotional schemes.,适用不同促销计划的规则。, Shift,转移, Show {0},显示{0}, -"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",命名系列中不允许使用除"-", "#", "।", "/", "{{" 和 "}}"之外的特殊字符 {0}, Target Details,目标细节, {0} already has a Parent Procedure {1}.,{0}已有父程序{1}。, API,应用程序界面, diff --git a/erpnext/translations/zh_tw.csv b/erpnext/translations/zh_tw.csv index 313908fed2c..1b7e18637d3 100644 --- a/erpnext/translations/zh_tw.csv +++ b/erpnext/translations/zh_tw.csv @@ -3311,7 +3311,6 @@ Quality Feedback Template,質量反饋模板, Rules for applying different promotional schemes.,適用不同促銷計劃的規則。, Shift,轉移, Show {0},顯示{0}, -"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",命名系列中不允許使用除 "-", "#", "।", "/", "{{" 和 "}}"之外的特殊字符 {0}, Target Details,目標細節, API,API, Annual,年刊, diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py index 8a92418d25e..219747c9f8a 100644 --- a/erpnext/www/shop-by-category/index.py +++ b/erpnext/www/shop-by-category/index.py @@ -51,21 +51,31 @@ def get_tabs(categories): return tab_values -def get_category_records(categories): +def get_category_records(categories: list): categorical_data = {} - for category in categories: - if category == "item_group": + + for c in categories: + if c == "item_group": categorical_data["item_group"] = frappe.db.get_all( "Item Group", filters={"parent_item_group": "All Item Groups", "show_in_website": 1}, fields=["name", "parent_item_group", "is_group", "image", "route"], ) - else: - doctype = frappe.unscrub(category) - fields = ["name"] - if frappe.get_meta(doctype, cached=True).get_field("image"): + + continue + + doctype = frappe.unscrub(c) + fields = ["name"] + + try: + meta = frappe.get_meta(doctype, cached=True) + if meta.get_field("image"): fields += ["image"] - categorical_data[category] = frappe.db.get_all(doctype, fields=fields) + data = frappe.db.get_all(doctype, fields=fields) + categorical_data[c] = data + except BaseException: + frappe.throw(_("DocType {} not found").format(doctype)) + continue return categorical_data