mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-15 04:45:09 +00:00
Merge pull request #34051 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -234,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"
|
||||
)
|
||||
@@ -253,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")
|
||||
@@ -407,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 not hasattr(ref_doc, "order_type") or 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)
|
||||
|
||||
@@ -550,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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -716,6 +716,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
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
315
erpnext/loan_management/workspace/loans/loans.json
Normal file
315
erpnext/loan_management/workspace/loans/loans.json
Normal file
@@ -0,0 +1,315 @@
|
||||
{
|
||||
"charts": [],
|
||||
"content": "[{\"id\":\"_38WStznya\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -326,3 +326,4 @@ 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.update_asset_value_for_manual_depr_entries
|
||||
erpnext.patches.v14_0.set_pick_list_status
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
40
erpnext/patches/v14_0/set_pick_list_status.py
Normal file
40
erpnext/patches/v14_0/set_pick_list_status.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
||||
import frappe
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
def execute():
|
||||
pl = frappe.qb.DocType("Pick List")
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
dn = frappe.qb.DocType("Delivery Note")
|
||||
|
||||
(
|
||||
frappe.qb.update(pl).set(
|
||||
pl.status,
|
||||
(
|
||||
frappe.qb.terms.Case()
|
||||
.when(pl.docstatus == 0, "Draft")
|
||||
.when(pl.docstatus == 2, "Cancelled")
|
||||
.else_("Completed")
|
||||
),
|
||||
)
|
||||
).run()
|
||||
|
||||
(
|
||||
frappe.qb.update(pl)
|
||||
.set(pl.status, "Open")
|
||||
.where(
|
||||
(
|
||||
ExistsCriterion(
|
||||
frappe.qb.from_(se).select(se.name).where((se.docstatus == 1) & (se.pick_list == pl.name))
|
||||
)
|
||||
| ExistsCriterion(
|
||||
frappe.qb.from_(dn).select(dn.name).where((dn.docstatus == 1) & (dn.pick_list == pl.name))
|
||||
)
|
||||
).negate()
|
||||
& (pl.docstatus == 1)
|
||||
)
|
||||
).run()
|
||||
@@ -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,
|
||||
|
||||
@@ -58,6 +58,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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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'), () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
// });
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -335,16 +335,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 +376,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")),
|
||||
@@ -404,7 +399,7 @@ def add_uom_data():
|
||||
if not frappe.db.exists(
|
||||
"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 +407,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 +463,7 @@ def install_company(args):
|
||||
make_records(records)
|
||||
|
||||
|
||||
def install_defaults(args=None):
|
||||
def install_defaults(args=None): # nosemgrep
|
||||
records = [
|
||||
# Price Lists
|
||||
{
|
||||
@@ -493,7 +488,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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -366,10 +366,11 @@ frappe.ui.form.on('Material Request', {
|
||||
|
||||
frappe.ui.form.on("Material Request Item", {
|
||||
qty: function (frm, doctype, name) {
|
||||
var d = locals[doctype][name];
|
||||
if (flt(d.qty) < flt(d.min_order_qty)) {
|
||||
const item = locals[doctype][name];
|
||||
if (flt(item.qty) < flt(item.min_order_qty)) {
|
||||
frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum Order Qty"));
|
||||
}
|
||||
frm.events.get_item_data(frm, item, false);
|
||||
},
|
||||
|
||||
from_warehouse: function(frm, doctype, name) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
14
erpnext/stock/doctype/pick_list/pick_list_list.js
Normal file
14
erpnext/stock/doctype/pick_list/pick_list_list.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.listview_settings['Pick List'] = {
|
||||
get_indicator: function (doc) {
|
||||
const status_colors = {
|
||||
"Draft": "grey",
|
||||
"Open": "orange",
|
||||
"Completed": "green",
|
||||
"Cancelled": "red",
|
||||
};
|
||||
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
|
||||
},
|
||||
};
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -161,6 +161,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()
|
||||
|
||||
@@ -2279,6 +2280,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()
|
||||
@@ -2491,7 +2497,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
40
erpnext/stock/tests/test_get_item_details.py
Normal file
40
erpnext/stock/tests/test_get_item_details.py
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user