Merge pull request #38952 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
commit
e2bf03bdb2
@ -5,7 +5,7 @@ fail_fast: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "erpnext.*"
|
||||
@ -15,6 +15,10 @@ repos:
|
||||
args: ['--branch', 'develop']
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.44.0
|
||||
|
@ -26,7 +26,7 @@
|
||||
"0360 Bauliche Investitionen in fremden (gepachteten) Betriebs- und Geschäftsgebäuden": {"account_type": "Fixed Asset"},
|
||||
"0370 Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgebäuden": {"account_type": "Fixed Asset"},
|
||||
"0390 Kumulierte Abschreibungen zu Grundstücken ": {"account_type": "Fixed Asset"},
|
||||
"0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"},
|
||||
"0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"},
|
||||
"0500 Maschinenwerkzeuge ": {"account_type": "Fixed Asset"},
|
||||
"0510 Allgemeine Werkzeuge und Handwerkzeuge ": {"account_type": "Fixed Asset"},
|
||||
"0520 Prototypen, Formen, Modelle ": {"account_type": "Fixed Asset"},
|
||||
@ -65,42 +65,41 @@
|
||||
"0980 Geleistete Anzahlungen auf Finanzanlagen ": {"account_type": "Fixed Asset"},
|
||||
"0990 Kumulierte Abschreibungen zu Finanzanlagen ": {"account_type": "Fixed Asset"},
|
||||
"root_type": "Asset"
|
||||
},
|
||||
},
|
||||
"Klasse 1 Aktiva: Vorr\u00e4te": {
|
||||
"1000 Bezugsverrechnung": {"account_type": "Stock"},
|
||||
"1100 Rohstoffe": {"account_type": "Stock"},
|
||||
"1200 Bezogene Teile": {"account_type": "Stock"},
|
||||
"1300 Hilfsstoffe": {"account_type": "Stock"},
|
||||
"1350 Betriebsstoffe": {"account_type": "Stock"},
|
||||
"1360 Vorrat Energietraeger": {"account_type": "Stock"},
|
||||
"1360 Vorrat Energietraeger": {"account_type": "Stock"},
|
||||
"1400 Unfertige Erzeugnisse": {"account_type": "Stock"},
|
||||
"1500 Fertige Erzeugnisse": {"account_type": "Stock"},
|
||||
"1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"},
|
||||
"1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"},
|
||||
"1900 Wertberichtigungen": {"account_type": "Stock"},
|
||||
"1800 Geleistete Anzahlungen": {"account_type": "Stock"},
|
||||
"1900 Wertberichtigungen": {"account_type": "Stock"},
|
||||
"root_type": "Asset"
|
||||
},
|
||||
},
|
||||
"Klasse 3 Passiva: Verbindlichkeiten": {
|
||||
"3000 Allgemeine Verbindlichkeiten (Schuld)": {"account_type": "Payable"},
|
||||
"3010 R\u00fcckstellungen f\u00fcr Pensionen": {"account_type": "Payable"},
|
||||
"3020 Steuerr\u00fcckstellungen": {"account_type": "Tax"},
|
||||
"3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"},
|
||||
"3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"},
|
||||
"3110 Verbindlichkeiten gegen\u00fcber Bank": {"account_type": "Payable"},
|
||||
"3150 Verbindlichkeiten Darlehen": {"account_type": "Payable"},
|
||||
"3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"},
|
||||
"3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"},
|
||||
"3380 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"3400 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {},
|
||||
"3460 Verbindlichkeiten gegenueber Gesellschaftern": {"account_type": "Payable"},
|
||||
"3470 Einlagen stiller Gesellschafter": {"account_type": "Payable"},
|
||||
"3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"},
|
||||
"3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"},
|
||||
"3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"},
|
||||
"3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"},
|
||||
"3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"},
|
||||
"3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"},
|
||||
"3600 Verbindlichkeiten Sozialversicherung": {"account_type": "Payable"},
|
||||
"3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"},
|
||||
"3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"},
|
||||
"3700 Sonstige Verbindlichkeiten": {"account_type": "Payable"},
|
||||
"3900 Passive Rechnungsabgrenzungsposten": {"account_type": "Payable"},
|
||||
"3100 Anleihen (einschlie\u00dflich konvertibler)": {"account_type": "Payable"},
|
||||
@ -119,13 +118,13 @@
|
||||
},
|
||||
"3515 Umsatzsteuer Inland 10%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
},
|
||||
"3520 Umsatzsteuer aus i.g. Erwerb 20%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"3525 Umsatzsteuer aus i.g. Erwerb 10%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
},
|
||||
"3560 Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {},
|
||||
"3360 Verbindlichkeiten aus Lieferungen u. Leistungen EU": {
|
||||
"account_type": "Payable"
|
||||
@ -141,7 +140,7 @@
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"root_type": "Liability"
|
||||
},
|
||||
},
|
||||
"Klasse 2 Aktiva: Umlaufverm\u00f6gen, Rechnungsabgrenzungen": {
|
||||
"2030 Forderungen aus Lieferungen und Leistungen Inland (0% USt, umsatzsteuerfrei)": {
|
||||
"account_type": "Receivable"
|
||||
@ -154,7 +153,7 @@
|
||||
},
|
||||
"2040 Forderungen aus Lieferungen und Leistungen Inland (sonstiger USt-Satz)": {
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
},
|
||||
"2100 Forderungen aus Lieferungen und Leistungen EU": {
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
@ -192,7 +191,7 @@
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"2570 Einfuhrumsatzsteuer (bezahlt)": {"account_type": "Tax"},
|
||||
|
||||
|
||||
"2460 Eingeforderte aber noch nicht eingezahlte Einlagen": {
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
@ -243,10 +242,10 @@
|
||||
},
|
||||
"2800 Guthaben bei Bank": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
},
|
||||
"2801 Guthaben bei Bank - Sparkonto": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
},
|
||||
"2810 Guthaben bei Paypal": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
@ -264,19 +263,19 @@
|
||||
},
|
||||
"2895 Schwebende Geldbewegugen": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
},
|
||||
"2513 Vorsteuer Inland 5%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"2515 Vorsteuer Inland 20%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
},
|
||||
"2520 Vorsteuer aus innergemeinschaftlichem Erwerb 10%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"2525 Vorsteuer aus innergemeinschaftlichem Erwerb 20%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
},
|
||||
"2530 Vorsteuer \u00a719/Art 19 ( reverse charge ) ": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
@ -286,16 +285,16 @@
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Klasse 4: Betriebliche Erträge": {
|
||||
"4000 Erlöse 20 %": {"account_type": "Income Account"},
|
||||
"4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"},
|
||||
"4000 Erlöse 20 %": {"account_type": "Income Account"},
|
||||
"4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"},
|
||||
"4010 Erl\u00f6se 10 %": {"account_type": "Income Account"},
|
||||
"4030 Erl\u00f6se 13 %": {"account_type": "Income Account"},
|
||||
"4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"},
|
||||
"4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"},
|
||||
"4030 Erl\u00f6se 13 %": {"account_type": "Income Account"},
|
||||
"4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"},
|
||||
"4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"},
|
||||
"4410 Erl\u00f6sreduktion 10 %": {"account_type": "Expense Account"},
|
||||
"4420 Erl\u00f6sreduktion 20 %": {"account_type": "Expense Account"},
|
||||
"4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"},
|
||||
"4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"},
|
||||
"4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"},
|
||||
"4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"},
|
||||
"4500 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {"account_type": "Income Account"},
|
||||
"4580 Aktivierte Eigenleistungen": {"account_type": "Income Account"},
|
||||
"4600 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
|
||||
@ -304,15 +303,15 @@
|
||||
"4700 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {"account_type": "Income Account"},
|
||||
"4800 \u00dcbrige betriebliche Ertr\u00e4ge": {"account_type": "Income Account"},
|
||||
"root_type": "Income"
|
||||
},
|
||||
},
|
||||
"Klasse 5: Aufwand f\u00fcr Material und Leistungen": {
|
||||
"5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"},
|
||||
"5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"},
|
||||
"5100 Verbrauch an Rohstoffen": {"account_type": "Cost of Goods Sold"},
|
||||
"5200 Verbrauch von bezogenen Fertig- und Einzelteilen": {"account_type": "Cost of Goods Sold"},
|
||||
"5300 Verbrauch von Hilfsstoffen": {"account_type": "Cost of Goods Sold"},
|
||||
"5340 Verbrauch Verpackungsmaterial": {"account_type": "Cost of Goods Sold"},
|
||||
"5470 Verbrauch von Kleinmaterial": {"account_type": "Cost of Goods Sold"},
|
||||
"5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"},
|
||||
"5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"},
|
||||
"5400 Verbrauch von Betriebsstoffen": {"account_type": "Cost of Goods Sold"},
|
||||
"5500 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {"account_type": "Cost of Goods Sold"},
|
||||
"5600 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {"account_type": "Cost of Goods Sold"},
|
||||
@ -340,7 +339,7 @@
|
||||
"6700 Sonstige Sozialaufwendungen": {"account_type": "Payable"},
|
||||
"6900 Aufwandsstellenrechnung Personal": {"account_type": "Payable"},
|
||||
"root_type": "Expense"
|
||||
},
|
||||
},
|
||||
"Klasse 7: Abschreibungen und sonstige betriebliche Aufwendungen": {
|
||||
"7010 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {"account_type": "Depreciation"},
|
||||
"7100 Sonstige Steuern und Geb\u00fchren": {"account_type": "Tax"},
|
||||
@ -349,7 +348,7 @@
|
||||
"7310 Fahrrad - Aufwand": {"account_type": "Expense Account"},
|
||||
"7320 Kfz - Aufwand": {"account_type": "Expense Account"},
|
||||
"7330 LKW - Aufwand": {"account_type": "Expense Account"},
|
||||
"7340 Lastenrad - Aufwand": {"account_type": "Expense Account"},
|
||||
"7340 Lastenrad - Aufwand": {"account_type": "Expense Account"},
|
||||
"7350 Reise- und Fahraufwand": {"account_type": "Expense Account"},
|
||||
"7360 Tag- und N\u00e4chtigungsgelder": {"account_type": "Expense Account"},
|
||||
"7380 Nachrichtenaufwand": {"account_type": "Expense Account"},
|
||||
@ -409,7 +408,7 @@
|
||||
"8990 Gewinnabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {"account_type": "Expense Account"},
|
||||
"8350 nicht ausgenutzte Lieferantenskonti": {"account_type": "Expense Account"},
|
||||
"root_type": "Income"
|
||||
},
|
||||
},
|
||||
"Klasse 9 Passiva: Eigenkapital, R\u00fccklagen, stille Einlagen, Abschlusskonten": {
|
||||
"9000 Gezeichnetes bzw. gewidmetes Kapital": {
|
||||
"account_type": "Equity"
|
||||
@ -435,5 +434,5 @@
|
||||
},
|
||||
"root_type": "Equity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,8 +142,7 @@
|
||||
"label": "Against Voucher Type",
|
||||
"oldfieldname": "against_voucher_type",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher",
|
||||
@ -162,8 +161,7 @@
|
||||
"label": "Voucher Type",
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
@ -321,4 +319,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
@ -1,97 +1,94 @@
|
||||
[
|
||||
{
|
||||
"cheque_date": "2013-03-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "_Test Payable - _TC",
|
||||
"party_type": "Supplier",
|
||||
"party": "_Test Supplier",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
}
|
||||
{
|
||||
"cheque_date": "2013-03-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "_Test Payable - _TC",
|
||||
"party_type": "Supplier",
|
||||
"party": "_Test Supplier",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "Sales - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
}
|
||||
]
|
||||
|
@ -163,6 +163,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
})
|
||||
}, __("Get Items From"));
|
||||
|
||||
if (!this.frm.doc.is_return) {
|
||||
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
|
||||
if (value) {
|
||||
this.frm.doc.items.forEach((item) => {
|
||||
this.frm.fields_dict.items.grid.update_docfield_property(
|
||||
"rate", "read_only", (item.purchase_receipt && item.pr_detail)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
|
||||
|
||||
|
@ -1816,10 +1816,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
|
||||
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
|
@ -288,7 +288,6 @@
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -919,7 +918,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-30 16:26:05.629780",
|
||||
"modified": "2023-12-25 22:00:28.043555",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -2549,10 +2549,6 @@ def get_loyalty_programs(customer):
|
||||
return lp_details
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name, target_doc=None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
|
@ -153,8 +153,12 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "in_party_currency",
|
||||
"label": __("In Party Currency"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
data = execute(filters)
|
||||
|
@ -185,9 +185,12 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "in_party_currency",
|
||||
"label": __("In Party Currency"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
22
erpnext/accounts/report/accounts_receivable/accounts_receivable.py
Executable file → Normal file
22
erpnext/accounts/report/accounts_receivable/accounts_receivable.py
Executable file → Normal file
@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision, get_party_types_from_
|
||||
# 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters
|
||||
# 7. For overpayment against an invoice with payment terms, there will be an additional row
|
||||
# 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated
|
||||
# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party
|
||||
# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable"
|
||||
# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency
|
||||
# 10. This report is based on Payment Ledger Entries
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@ -82,6 +82,9 @@ class ReceivablePayableReport(object):
|
||||
self.total_row_map = {}
|
||||
self.skip_total_row = 1
|
||||
|
||||
if self.filters.get("in_party_currency"):
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
@ -143,7 +146,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||
self.init_subtotal_row("Total")
|
||||
|
||||
def get_invoices(self, ple):
|
||||
@ -222,8 +225,7 @@ class ReceivablePayableReport(object):
|
||||
if not row:
|
||||
return
|
||||
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
@ -258,8 +260,10 @@ class ReceivablePayableReport(object):
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
for field in self.get_currency_fields():
|
||||
total_row[field] += row.get(field, 0.0)
|
||||
if total_row:
|
||||
for field in self.get_currency_fields():
|
||||
total_row[field] += row.get(field, 0.0)
|
||||
total_row["currency"] = row.get("currency", "")
|
||||
|
||||
def append_subtotal_row(self, party):
|
||||
sub_total_row = self.total_row_map.get(party)
|
||||
@ -320,7 +324,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.get("group_by_party"):
|
||||
self.append_subtotal_row(self.previous_party)
|
||||
if self.data:
|
||||
self.data.append(self.total_row_map.get("Total"))
|
||||
self.data.append(self.total_row_map.get("Total", {}))
|
||||
|
||||
def append_row(self, row):
|
||||
self.allocate_future_payments(row)
|
||||
@ -451,7 +455,7 @@ class ReceivablePayableReport(object):
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
|
||||
row.currency = row.account_currency
|
||||
else:
|
||||
row.currency = self.company_currency
|
||||
|
@ -579,7 +579,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters.update({"party_account": self.debtors_usd})
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
|
||||
expected_data = [100.0, 100.0, self.debtors_usd, si2.currency]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||
@ -616,6 +616,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
|
@ -35,6 +35,8 @@
|
||||
"purchase_receipt",
|
||||
"purchase_invoice",
|
||||
"available_for_use_date",
|
||||
"total_asset_cost",
|
||||
"additional_asset_cost",
|
||||
"column_break_23",
|
||||
"gross_purchase_amount",
|
||||
"asset_quantity",
|
||||
@ -529,6 +531,22 @@
|
||||
"label": "Capitalized In",
|
||||
"options": "Asset Capitalization",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus > 0",
|
||||
"fieldname": "total_asset_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Asset Cost",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus > 0",
|
||||
"fieldname": "additional_asset_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional Asset Cost",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@ -572,7 +590,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-20 20:57:37.010467",
|
||||
"modified": "2023-12-21 16:46:20.732869",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
@ -50,6 +50,7 @@ class Asset(AccountsController):
|
||||
|
||||
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
|
||||
|
||||
additional_asset_cost: DF.Currency
|
||||
amended_from: DF.Link | None
|
||||
asset_category: DF.Link | None
|
||||
asset_name: DF.Data
|
||||
@ -111,6 +112,7 @@ class Asset(AccountsController):
|
||||
"Decapitalized",
|
||||
]
|
||||
supplier: DF.Link | None
|
||||
total_asset_cost: DF.Currency
|
||||
total_number_of_depreciations: DF.Int
|
||||
value_after_depreciation: DF.Currency
|
||||
# end: auto-generated types
|
||||
@ -144,6 +146,7 @@ class Asset(AccountsController):
|
||||
).format(asset_depr_schedules_links)
|
||||
)
|
||||
|
||||
self.total_asset_cost = self.gross_purchase_amount
|
||||
self.status = self.get_status()
|
||||
|
||||
def on_submit(self):
|
||||
|
@ -93,6 +93,10 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.increase_asset_value()
|
||||
|
||||
if self.capitalize_repair_cost:
|
||||
self.asset_doc.total_asset_cost += self.repair_cost
|
||||
self.asset_doc.additional_asset_cost += self.repair_cost
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
self.check_for_stock_items_and_warehouse()
|
||||
self.decrease_stock_quantity()
|
||||
@ -128,6 +132,10 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.decrease_asset_value()
|
||||
|
||||
if self.capitalize_repair_cost:
|
||||
self.asset_doc.total_asset_cost -= self.repair_cost
|
||||
self.asset_doc.additional_asset_cost -= self.repair_cost
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
self.increase_stock_quantity()
|
||||
if self.get("capitalize_repair_cost"):
|
||||
|
@ -123,8 +123,7 @@
|
||||
"oldfieldname": "item_code",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_part_no",
|
||||
|
@ -891,3 +891,31 @@ def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len,
|
||||
as_list=1,
|
||||
)
|
||||
return terms
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters) -> list:
|
||||
table = frappe.qb.DocType(doctype)
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(
|
||||
table.name,
|
||||
Concat("#", table.idx, ", ", table.item_code),
|
||||
)
|
||||
.orderby(table.idx)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
if filters:
|
||||
for field, value in filters.items():
|
||||
query = query.where(table[field] == value)
|
||||
|
||||
if txt:
|
||||
txt += "%"
|
||||
query = query.where(
|
||||
((table.idx.like(txt.replace("#", ""))) | (table.item_code.like(txt))) | (table.name.like(txt))
|
||||
)
|
||||
|
||||
return query.run(as_dict=False)
|
||||
|
@ -438,7 +438,9 @@ class SellingController(StockController):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
|
||||
|
||||
if not d.incoming_rate:
|
||||
if not d.incoming_rate or (
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
):
|
||||
d.incoming_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
|
@ -218,6 +218,7 @@
|
||||
"options": "\nWork Order\nJob Card"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversion_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Conversion Rate",
|
||||
@ -636,7 +637,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-07 11:38:08.152294",
|
||||
"modified": "2023-12-26 19:34:08.159312",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
@ -744,6 +744,9 @@ class BOM(WebsiteGenerator):
|
||||
base_total_rm_cost = 0
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate":
|
||||
continue
|
||||
|
||||
old_rate = d.rate
|
||||
if self.rm_cost_as_per != "Manual":
|
||||
d.rate = self.get_rm_rate(
|
||||
@ -1017,6 +1020,8 @@ def get_bom_item_rate(args, bom_doc):
|
||||
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
|
||||
price_list_data = get_price_list_rate(bom_args, item_doc)
|
||||
rate = price_list_data.price_list_rate
|
||||
elif bom_doc.rm_cost_as_per == "Manual":
|
||||
return
|
||||
|
||||
return flt(rate)
|
||||
|
||||
|
@ -698,6 +698,35 @@ class TestBOM(FrappeTestCase):
|
||||
bom.update_cost()
|
||||
self.assertFalse(bom.flags.cost_updated)
|
||||
|
||||
def test_bom_with_service_item_cost(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 1000.0}).name
|
||||
|
||||
service_item = make_item(properties={"is_stock_item": 0}).name
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item, service_item], do_not_save=True)
|
||||
bom.rm_cost_as_per = "Valuation Rate"
|
||||
|
||||
for row in bom.items:
|
||||
if row.item_code == service_item:
|
||||
row.rate = 566.00
|
||||
else:
|
||||
row.rate = 800.00
|
||||
|
||||
bom.save()
|
||||
|
||||
for row in bom.items:
|
||||
if row.item_code == service_item:
|
||||
self.assertEqual(row.is_stock_item, 0)
|
||||
self.assertEqual(row.rate, 566.00)
|
||||
else:
|
||||
self.assertEqual(row.is_stock_item, 1)
|
||||
|
||||
def test_do_not_include_manufacturing_and_fixed_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import item_query
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
"bom_no",
|
||||
"source_warehouse",
|
||||
"allow_alternative_item",
|
||||
"is_stock_item",
|
||||
"section_break_5",
|
||||
"description",
|
||||
"col_break1",
|
||||
@ -185,7 +186,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"options": "currency",
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval:doc.is_stock_item == 1",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -284,13 +285,21 @@
|
||||
"fieldname": "do_not_explode",
|
||||
"fieldtype": "Check",
|
||||
"label": "Do Not Explode"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.is_stock_item",
|
||||
"fieldname": "is_stock_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Stock Item",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:35:51.378513",
|
||||
"modified": "2023-12-20 16:21:55.477883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Item",
|
||||
|
@ -305,6 +305,8 @@ frappe.ui.form.on('Production Plan', {
|
||||
frappe.throw(__("Select the Warehouse"));
|
||||
}
|
||||
|
||||
frm.set_value("consider_minimum_order_qty", 0);
|
||||
|
||||
if (frm.doc.ignore_existing_ordered_qty) {
|
||||
frm.events.get_items_for_material_requests(frm);
|
||||
} else {
|
||||
|
@ -48,6 +48,7 @@
|
||||
"material_request_planning",
|
||||
"include_non_stock_items",
|
||||
"include_subcontracted_items",
|
||||
"consider_minimum_order_qty",
|
||||
"include_safety_stock",
|
||||
"ignore_existing_ordered_qty",
|
||||
"column_break_25",
|
||||
@ -423,13 +424,19 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Sub Assembly Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "consider_minimum_order_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Minimum Order Qty"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-03 14:08:11.928027",
|
||||
"modified": "2023-12-26 16:31:13.740777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
@ -67,6 +67,7 @@ class ProductionPlan(Document):
|
||||
combine_items: DF.Check
|
||||
combine_sub_items: DF.Check
|
||||
company: DF.Link
|
||||
consider_minimum_order_qty: DF.Check
|
||||
customer: DF.Link | None
|
||||
for_warehouse: DF.Link | None
|
||||
from_date: DF.Date | None
|
||||
@ -1211,7 +1212,14 @@ def get_subitems(
|
||||
|
||||
|
||||
def get_material_request_items(
|
||||
row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict
|
||||
doc,
|
||||
row,
|
||||
sales_order,
|
||||
company,
|
||||
ignore_existing_ordered_qty,
|
||||
include_safety_stock,
|
||||
warehouse,
|
||||
bin_dict,
|
||||
):
|
||||
total_qty = row["qty"]
|
||||
|
||||
@ -1220,8 +1228,14 @@ def get_material_request_items(
|
||||
required_qty = total_qty
|
||||
elif total_qty > bin_dict.get("projected_qty", 0):
|
||||
required_qty = total_qty - bin_dict.get("projected_qty", 0)
|
||||
if required_qty > 0 and required_qty < row["min_order_qty"]:
|
||||
|
||||
if (
|
||||
doc.get("consider_minimum_order_qty")
|
||||
and required_qty > 0
|
||||
and required_qty < row["min_order_qty"]
|
||||
):
|
||||
required_qty = row["min_order_qty"]
|
||||
|
||||
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
||||
|
||||
if not row["purchase_uom"]:
|
||||
@ -1559,6 +1573,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
|
||||
if details.qty > 0:
|
||||
items = get_material_request_items(
|
||||
doc,
|
||||
details,
|
||||
sales_order,
|
||||
company,
|
||||
|
@ -1499,6 +1499,29 @@ class TestProductionPlan(FrappeTestCase):
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
self.assertAlmostEqual(after_qty, before_qty)
|
||||
|
||||
def test_min_order_qty_in_pp(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "min_order_qty": 1000}).name
|
||||
|
||||
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
|
||||
|
||||
pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1)
|
||||
|
||||
pln.for_warehouse = rm_warehouse
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
self.assertEqual(d.get("quantity"), 10.0)
|
||||
|
||||
pln.consider_minimum_order_qty = 1
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
self.assertEqual(d.get("quantity"), 1000.0)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
@ -350,6 +350,8 @@ erpnext.patches.v15_0.set_reserved_stock_in_bin
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
|
||||
erpnext.patches.v14_0.update_zero_asset_quantity_field
|
||||
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
|
||||
erpnext.patches.v14_0.update_total_asset_cost_field
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
|
19
erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py
Normal file
19
erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py
Normal file
@ -0,0 +1,19 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if not frappe.db.exists("BOM", {"docstatus": 1}):
|
||||
return
|
||||
|
||||
# Added is_stock_item to handle Read Only based on condition for the rate field
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE
|
||||
`tabBOM Item` boi,
|
||||
`tabItem` i
|
||||
SET
|
||||
boi.is_stock_item = i.is_stock_item
|
||||
WHERE
|
||||
boi.item_code = i.name
|
||||
"""
|
||||
)
|
17
erpnext/patches/v14_0/update_total_asset_cost_field.py
Normal file
17
erpnext/patches/v14_0/update_total_asset_cost_field.py
Normal file
@ -0,0 +1,17 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
frappe.qb.update(asset).set(asset.total_asset_cost, asset.gross_purchase_amount).run()
|
||||
|
||||
asset_repair_list = frappe.db.get_all(
|
||||
"Asset Repair",
|
||||
filters={"docstatus": 1, "repair_status": "Completed", "capitalize_repair_cost": 1},
|
||||
fields=["asset", "repair_cost"],
|
||||
)
|
||||
|
||||
for asset_repair in asset_repair_list:
|
||||
frappe.qb.update(asset).set(
|
||||
asset.total_asset_cost, asset.total_asset_cost + asset_repair.repair_cost
|
||||
).where(asset.name == asset_repair.asset).run()
|
@ -184,6 +184,12 @@ erpnext.sales_common = {
|
||||
refresh_field("incentives",row.name,row.parentfield);
|
||||
}
|
||||
|
||||
warehouse(doc, cdt, cdn) {
|
||||
if (doc.docstatus === 0 && doc.is_return && !doc.return_against) {
|
||||
frappe.model.set_value(cdt, cdn, "incoming_rate", 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
toggle_editable_price_list_rate() {
|
||||
var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name);
|
||||
var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
|
||||
|
@ -502,6 +502,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
|
||||
set_data(data) {
|
||||
data.forEach(d => {
|
||||
d.qty = Math.abs(d.qty);
|
||||
this.dialog.fields_dict.entries.df.data.push(d);
|
||||
});
|
||||
|
||||
|
@ -182,7 +182,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
create_stock_reservation_entries(frm) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Stock Reservation"),
|
||||
size: "large",
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "set_warehouse",
|
||||
@ -207,6 +207,50 @@ frappe.ui.form.on("Sales Order", {
|
||||
},
|
||||
},
|
||||
{fieldtype: "Column Break"},
|
||||
{
|
||||
fieldname: "add_item",
|
||||
fieldtype: "Link",
|
||||
label: __("Add Item"),
|
||||
options: "Sales Order Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_filtered_child_rows",
|
||||
filters: {
|
||||
"parenttype": frm.doc.doctype,
|
||||
"parent": frm.doc.name,
|
||||
"reserve_stock": 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
onchange: () => {
|
||||
let sales_order_item = dialog.get_value("add_item");
|
||||
|
||||
if (sales_order_item) {
|
||||
frm.doc.items.forEach(item => {
|
||||
if (item.name === sales_order_item) {
|
||||
let unreserved_qty = (flt(item.stock_qty) - (item.stock_reserved_qty ? flt(item.stock_reserved_qty) : (flt(item.delivered_qty) * flt(item.conversion_factor)))) / flt(item.conversion_factor);
|
||||
|
||||
if (unreserved_qty > 0) {
|
||||
dialog.fields_dict.items.df.data.forEach((row) => {
|
||||
if (row.sales_order_item === sales_order_item) {
|
||||
unreserved_qty -= row.qty_to_reserve;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
'sales_order_item': item.name,
|
||||
'item_code': item.item_code,
|
||||
'warehouse': dialog.get_value("set_warehouse") || item.warehouse,
|
||||
'qty_to_reserve': Math.max(unreserved_qty, 0)
|
||||
});
|
||||
dialog.fields_dict.items.grid.refresh();
|
||||
dialog.set_value("add_item", undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{fieldtype: "Section Break"},
|
||||
{
|
||||
fieldname: "items",
|
||||
@ -218,10 +262,34 @@ frappe.ui.form.on("Sales Order", {
|
||||
fields: [
|
||||
{
|
||||
fieldname: "sales_order_item",
|
||||
fieldtype: "Data",
|
||||
fieldtype: "Link",
|
||||
label: __("Sales Order Item"),
|
||||
options: "Sales Order Item",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_filtered_child_rows",
|
||||
filters: {
|
||||
"parenttype": frm.doc.doctype,
|
||||
"parent": frm.doc.name,
|
||||
"reserve_stock": 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
onchange: (event) => {
|
||||
if (event) {
|
||||
let name = $(event.currentTarget).closest(".grid-row").attr("data-name");
|
||||
let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc;
|
||||
|
||||
frm.doc.items.forEach(item => {
|
||||
if (item.name === item_row.sales_order_item) {
|
||||
item_row.item_code = item.item_code;
|
||||
}
|
||||
});
|
||||
dialog.fields_dict.items.grid.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
@ -284,14 +352,14 @@ frappe.ui.form.on("Sales Order", {
|
||||
|
||||
frm.doc.items.forEach(item => {
|
||||
if (item.reserve_stock) {
|
||||
let unreserved_qty = (flt(item.stock_qty) - (item.stock_reserved_qty ? flt(item.stock_reserved_qty) : (flt(item.delivered_qty) * flt(item.conversion_factor))))
|
||||
let unreserved_qty = (flt(item.stock_qty) - (item.stock_reserved_qty ? flt(item.stock_reserved_qty) : (flt(item.delivered_qty) * flt(item.conversion_factor)))) / flt(item.conversion_factor);
|
||||
|
||||
if (unreserved_qty > 0) {
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
'sales_order_item': item.name,
|
||||
'item_code': item.item_code,
|
||||
'warehouse': item.warehouse,
|
||||
'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor))
|
||||
'qty_to_reserve': unreserved_qty
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -520,7 +520,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
|
||||
render_taxes(taxes) {
|
||||
if (taxes.length) {
|
||||
if (taxes && taxes.length) {
|
||||
const currency = this.events.get_frm().doc.currency;
|
||||
const taxes_html = taxes.map(t => {
|
||||
if (t.tax_amount_after_discount_amount == 0.0) return;
|
||||
|
@ -3,11 +3,11 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_price_list_rate_for
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@ -50,6 +50,42 @@ def get_columns(filters=None):
|
||||
]
|
||||
|
||||
|
||||
def fetch_item_prices(
|
||||
customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None
|
||||
):
|
||||
price_list_map = frappe._dict()
|
||||
ip = qb.DocType("Item Price")
|
||||
and_conditions = []
|
||||
or_conditions = []
|
||||
if items:
|
||||
and_conditions.append(ip.item_code.isin([x.item_code for x in items]))
|
||||
and_conditions.append(ip.selling == True)
|
||||
|
||||
or_conditions.append(ip.customer == None)
|
||||
or_conditions.append(ip.price_list == None)
|
||||
|
||||
if customer:
|
||||
or_conditions.append(ip.customer == customer)
|
||||
|
||||
if price_list:
|
||||
or_conditions.append(ip.price_list == price_list)
|
||||
|
||||
if selling_price_list:
|
||||
or_conditions.append(ip.price_list == selling_price_list)
|
||||
|
||||
res = (
|
||||
qb.from_(ip)
|
||||
.select(ip.item_code, ip.price_list, ip.price_list_rate)
|
||||
.where(Criterion.all(and_conditions))
|
||||
.where(Criterion.any(or_conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for x in res:
|
||||
price_list_map.update({(x.item_code, x.price_list): x.price_list_rate})
|
||||
|
||||
return price_list_map
|
||||
|
||||
|
||||
def get_data(filters=None):
|
||||
data = []
|
||||
customer_details = get_customer_details(filters)
|
||||
@ -59,9 +95,17 @@ def get_data(filters=None):
|
||||
"Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code"
|
||||
)
|
||||
item_stock_map = {item.item_code: item.available for item in item_stock_map}
|
||||
price_list_map = fetch_item_prices(
|
||||
customer_details.customer,
|
||||
customer_details.price_list,
|
||||
customer_details.selling_price_list,
|
||||
items,
|
||||
)
|
||||
|
||||
for item in items:
|
||||
price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0
|
||||
price_list_rate = price_list_map.get(
|
||||
(item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0
|
||||
)
|
||||
available_stock = item_stock_map.get(item.item_code)
|
||||
|
||||
data.append(
|
||||
|
@ -4,22 +4,22 @@
|
||||
"cheque_no": "33",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"party_type": "Customer",
|
||||
"party": "ABC Enterprises",
|
||||
"credit_in_account_currency": 40000.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
},
|
||||
{
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 40000.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
}
|
||||
{
|
||||
"party_type": "Customer",
|
||||
"party": "ABC Enterprises",
|
||||
"credit_in_account_currency": 40000.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts"
|
||||
},
|
||||
{
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 40000.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts"
|
||||
}
|
||||
],
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -1,21 +1,6 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
cur_frm.cscript.refresh = function(doc, cdt, cdn) {
|
||||
cur_frm.cscript.set_root_readonly(doc);
|
||||
}
|
||||
|
||||
cur_frm.cscript.set_root_readonly = function(doc) {
|
||||
// read-only for root customer group
|
||||
if(!doc.parent_customer_group && !doc.__islocal) {
|
||||
cur_frm.set_read_only();
|
||||
cur_frm.set_intro(__("This is a root customer group and cannot be edited."));
|
||||
} else {
|
||||
cur_frm.set_intro(null);
|
||||
}
|
||||
}
|
||||
|
||||
frappe.ui.form.on("Customer Group", {
|
||||
setup: function(frm){
|
||||
frm.set_query('parent_customer_group', function (doc) {
|
||||
@ -48,5 +33,17 @@ frappe.ui.form.on("Customer Group", {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frm.trigger("set_root_readonly");
|
||||
},
|
||||
set_root_readonly: function(frm) {
|
||||
// read-only for root customer group
|
||||
if(!frm.doc.parent_customer_group && !frm.doc.__islocal) {
|
||||
frm.set_read_only();
|
||||
frm.set_intro(__("This is a root customer group and cannot be edited."));
|
||||
} else {
|
||||
frm.set_intro(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ frappe.ui.form.on('Sales Person', {
|
||||
frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}',
|
||||
[format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue');
|
||||
}
|
||||
frm.trigger("set_root_readonly");
|
||||
},
|
||||
|
||||
setup: function(frm) {
|
||||
@ -27,22 +28,18 @@ frappe.ui.form.on('Sales Person', {
|
||||
'Sales Order': () => frappe.new_doc("Sales Order")
|
||||
.then(() => frm.add_child("sales_team", {"sales_person": frm.doc.name}))
|
||||
}
|
||||
},
|
||||
set_root_readonly: function(frm) {
|
||||
// read-only for root
|
||||
if(!frm.doc.parent_sales_person && !frm.doc.__islocal) {
|
||||
frm.set_read_only();
|
||||
frm.set_intro(__("This is a root sales person and cannot be edited."));
|
||||
} else {
|
||||
frm.set_intro(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.cscript.refresh = function(doc, cdt, cdn) {
|
||||
cur_frm.cscript.set_root_readonly(doc);
|
||||
}
|
||||
|
||||
cur_frm.cscript.set_root_readonly = function(doc) {
|
||||
// read-only for root
|
||||
if(!doc.parent_sales_person && !doc.__islocal) {
|
||||
cur_frm.set_read_only();
|
||||
cur_frm.set_intro(__("This is a root sales person and cannot be edited."));
|
||||
} else {
|
||||
cur_frm.set_intro(null);
|
||||
}
|
||||
}
|
||||
|
||||
//get query select sales person
|
||||
cur_frm.fields_dict['parent_sales_person'].get_query = function(doc, cdt, cdn) {
|
||||
|
@ -1,21 +1,6 @@
|
||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
cur_frm.cscript.refresh = function(doc) {
|
||||
cur_frm.set_intro(doc.__islocal ? "" : __("There is nothing to edit."));
|
||||
cur_frm.cscript.set_root_readonly(doc);
|
||||
};
|
||||
|
||||
cur_frm.cscript.set_root_readonly = function(doc) {
|
||||
// read-only for root customer group
|
||||
if(!doc.parent_supplier_group && !doc.__islocal) {
|
||||
cur_frm.set_read_only();
|
||||
cur_frm.set_intro(__("This is a root supplier group and cannot be edited."));
|
||||
} else {
|
||||
cur_frm.set_intro(null);
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.on("Supplier Group", {
|
||||
setup: function(frm){
|
||||
frm.set_query('parent_supplier_group', function (doc) {
|
||||
@ -48,5 +33,17 @@ frappe.ui.form.on("Supplier Group", {
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frm.set_intro(frm.doc.__islocal ? "" : __("There is nothing to edit."));
|
||||
frm.trigger("set_root_readonly");
|
||||
},
|
||||
set_root_readonly: function(frm) {
|
||||
if(!frm.doc.parent_supplier_group && !frm.doc.__islocal) {
|
||||
frm.trigger("set_read_only");
|
||||
frm.set_intro(__("This is a root supplier group and cannot be edited."));
|
||||
} else {
|
||||
frm.set_intro(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -11,23 +11,22 @@ frappe.ui.form.on("Territory", {
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frm.trigger("set_root_readonly");
|
||||
},
|
||||
set_root_readonly: function(frm) {
|
||||
// read-only for root territory
|
||||
if(!frm.doc.parent_territory && !frm.doc.__islocal) {
|
||||
frm.set_read_only();
|
||||
frm.set_intro(__("This is a root territory and cannot be edited."));
|
||||
} else {
|
||||
frm.set_intro(null);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
cur_frm.cscript.refresh = function(doc, cdt, cdn) {
|
||||
cur_frm.cscript.set_root_readonly(doc);
|
||||
}
|
||||
|
||||
cur_frm.cscript.set_root_readonly = function(doc) {
|
||||
// read-only for root territory
|
||||
if(!doc.parent_territory && !doc.__islocal) {
|
||||
cur_frm.set_read_only();
|
||||
cur_frm.set_intro(__("This is a root territory and cannot be edited."));
|
||||
} else {
|
||||
cur_frm.set_intro(null);
|
||||
}
|
||||
}
|
||||
|
||||
//get query select territory
|
||||
cur_frm.fields_dict['parent_territory'].get_query = function(doc,cdt,cdn) {
|
||||
return{
|
||||
|
@ -52,8 +52,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0.00",
|
||||
|
@ -311,11 +311,13 @@ class DeliveryNote(SellingController):
|
||||
)
|
||||
|
||||
def set_serial_and_batch_bundle_from_pick_list(self):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if not self.pick_list:
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
if item.pick_list_item:
|
||||
if item.pick_list_item and not item.serial_and_batch_bundle:
|
||||
filters = {
|
||||
"item_code": item.item_code,
|
||||
"voucher_type": "Pick List",
|
||||
@ -326,7 +328,17 @@ class DeliveryNote(SellingController):
|
||||
bundle_id = frappe.db.get_value("Serial and Batch Bundle", filters, "name")
|
||||
|
||||
if bundle_id:
|
||||
item.serial_and_batch_bundle = bundle_id
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": "Outward",
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"item_code": item.get("item_code"),
|
||||
}
|
||||
)
|
||||
|
||||
cls_obj.duplicate_package()
|
||||
|
||||
item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
|
||||
|
||||
def validate_proj_cust(self):
|
||||
"""check for does customer belong to same project as entered.."""
|
||||
@ -408,6 +420,7 @@ class DeliveryNote(SellingController):
|
||||
self.update_stock_ledger()
|
||||
|
||||
self.cancel_packing_slips()
|
||||
self.update_pick_list_status()
|
||||
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.repost_future_sle_and_gle()
|
||||
|
@ -1,15 +1,27 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
UNUSED_INDEXES = [
|
||||
("Delivery Note", ["customer", "is_return", "return_against"]),
|
||||
("Sales Invoice", ["customer", "is_return", "return_against"]),
|
||||
("Purchase Invoice", ["supplier", "is_return", "return_against"]),
|
||||
("Purchase Receipt", ["supplier", "is_return", "return_against"]),
|
||||
]
|
||||
|
||||
|
||||
def execute():
|
||||
"""Drop unused return_against index"""
|
||||
for doctype, index_fields in UNUSED_INDEXES:
|
||||
table = f"tab{doctype}"
|
||||
index_name = frappe.db.get_index_name(index_fields)
|
||||
drop_index_if_exists(table, index_name)
|
||||
|
||||
|
||||
def drop_index_if_exists(table: str, index: str):
|
||||
if not frappe.db.has_index(table, index):
|
||||
return
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(
|
||||
"ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`"
|
||||
)
|
||||
frappe.db.sql_ddl(
|
||||
"ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`"
|
||||
)
|
||||
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
|
||||
click.echo(f"✓ dropped {index} index from {table}")
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop unused index")
|
||||
frappe.log_error("Failed to drop index")
|
||||
|
@ -1425,6 +1425,59 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0)
|
||||
|
||||
def test_sales_return_valuation_for_moving_average_case2(self):
|
||||
# Make DN return
|
||||
# Make Bakcdated Purchase Receipt and check DN return valuation rate
|
||||
# The rate should be recalculate based on the backdated purchase receipt
|
||||
frappe.flags.print_debug_messages = False
|
||||
item_code = make_item(
|
||||
"_Test Item Sales Return with MA Case2",
|
||||
{"is_stock_item": 1, "valuation_method": "Moving Average", "stock_uom": "Nos"},
|
||||
).name
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=100.0,
|
||||
posting_date=add_days(nowdate(), -5),
|
||||
)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_code,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
rate=500,
|
||||
posting_date=add_days(nowdate(), -4),
|
||||
)
|
||||
|
||||
returned_dn = create_delivery_note(
|
||||
is_return=1,
|
||||
item_code=item_code,
|
||||
return_against=dn.name,
|
||||
qty=-5,
|
||||
rate=500,
|
||||
company=dn.company,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 100.0)
|
||||
|
||||
# Make backdated purchase receipt
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=200.0,
|
||||
posting_date=add_days(nowdate(), -3),
|
||||
)
|
||||
|
||||
returned_dn.reload()
|
||||
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
@ -283,6 +283,7 @@ frappe.ui.form.on('Pick List Item', {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
uom: (frm, cdt, cdn) => {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (row.uom) {
|
||||
@ -291,13 +292,50 @@ frappe.ui.form.on('Pick List Item', {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
qty: (frm, cdt, cdn) => {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor);
|
||||
},
|
||||
|
||||
conversion_factor: (frm, cdt, cdn) => {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor);
|
||||
},
|
||||
|
||||
pick_serial_and_batch(frm, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
|
||||
.then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
item.type_of_transaction = item.qty > 0 ? "Outward":"Inward";
|
||||
|
||||
item.title = item.has_serial_no ?
|
||||
__("Select Serial No") : __("Select Batch No");
|
||||
|
||||
if (item.has_serial_no && item.has_batch_no) {
|
||||
item.title = __("Select Serial and Batch");
|
||||
}
|
||||
|
||||
frappe.require(path, function() {
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"qty": qty
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -21,6 +21,7 @@ from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
)
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
get_picked_serial_nos,
|
||||
)
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
@ -167,6 +168,9 @@ class PickList(Document):
|
||||
"Serial and Batch Bundle", row.serial_and_batch_bundle
|
||||
).set_serial_and_batch_values(self, row)
|
||||
|
||||
def on_trash(self):
|
||||
self.remove_serial_and_batch_bundle()
|
||||
|
||||
def remove_serial_and_batch_bundle(self):
|
||||
for row in self.locations:
|
||||
if row.serial_and_batch_bundle:
|
||||
@ -723,13 +727,14 @@ def get_available_item_locations(
|
||||
def get_available_item_locations_for_serialized_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
||||
):
|
||||
picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
|
||||
|
||||
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.creation)
|
||||
.limit(cint(required_qty + total_picked_qty))
|
||||
)
|
||||
|
||||
if from_warehouses:
|
||||
@ -742,6 +747,9 @@ def get_available_item_locations_for_serialized_item(
|
||||
warehouse_serial_nos_map = frappe._dict()
|
||||
picked_qty = required_qty
|
||||
for serial_no, warehouse in serial_nos:
|
||||
if serial_no in picked_serial_nos:
|
||||
continue
|
||||
|
||||
if picked_qty <= 0:
|
||||
break
|
||||
|
||||
@ -786,7 +794,8 @@ def get_available_item_locations_for_batched_item(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": from_warehouses,
|
||||
"qty": required_qty + total_picked_qty,
|
||||
"qty": required_qty,
|
||||
"is_pick_list": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -1050,7 +1059,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
|
||||
@frappe.whitelist()
|
||||
def target_document_exists(pick_list_name, purpose):
|
||||
if purpose == "Delivery":
|
||||
return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name})
|
||||
return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name, "docstatus": 1})
|
||||
|
||||
return stock_entry_exists(pick_list_name)
|
||||
|
||||
|
@ -644,6 +644,122 @@ class TestPickList(FrappeTestCase):
|
||||
so.reload()
|
||||
self.assertEqual(so.per_picked, 50)
|
||||
|
||||
def test_picklist_for_batch_item(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
properties={"is_stock_item": 1, "has_batch_no": 1, "batch_no_series": "PICKLT-.######"}
|
||||
).name
|
||||
|
||||
# create batch
|
||||
for batch_id in ["PICKLT-000001", "PICKLT-000002"]:
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_id,
|
||||
"item": item,
|
||||
}
|
||||
).insert()
|
||||
|
||||
make_stock_entry(
|
||||
item=item,
|
||||
to_warehouse=warehouse,
|
||||
qty=50,
|
||||
basic_rate=100,
|
||||
batches=frappe._dict({"PICKLT-000001": 30, "PICKLT-000002": 20}),
|
||||
)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=25.0, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.qty, 25.0)
|
||||
self.assertTrue(loc.serial_and_batch_bundle)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
fields=["qty", "batch_no"],
|
||||
filters={"parent": loc.serial_and_batch_bundle},
|
||||
)
|
||||
|
||||
for d in data:
|
||||
self.assertEqual(d.batch_no, "PICKLT-000001")
|
||||
self.assertEqual(d.qty, 25.0 * -1)
|
||||
|
||||
pl.save()
|
||||
pl.submit()
|
||||
|
||||
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
|
||||
pl = create_pick_list(so1.name)
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.qty, 10.0)
|
||||
self.assertTrue(loc.serial_and_batch_bundle)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
fields=["qty", "batch_no"],
|
||||
filters={"parent": loc.serial_and_batch_bundle},
|
||||
)
|
||||
|
||||
for d in data:
|
||||
self.assertTrue(d.batch_no in ["PICKLT-000001", "PICKLT-000002"])
|
||||
if d.batch_no == "PICKLT-000001":
|
||||
self.assertEqual(d.qty, 5.0 * -1)
|
||||
elif d.batch_no == "PICKLT-000002":
|
||||
self.assertEqual(d.qty, 5.0 * -1)
|
||||
|
||||
pl.save()
|
||||
pl.submit()
|
||||
pl.cancel()
|
||||
|
||||
def test_picklist_for_serial_item(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-PICKLT-.######"}
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=50, basic_rate=100)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=25.0, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
picked_serial_nos = []
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.qty, 25.0)
|
||||
self.assertTrue(loc.serial_and_batch_bundle)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["serial_no"], filters={"parent": loc.serial_and_batch_bundle}
|
||||
)
|
||||
|
||||
picked_serial_nos = [d.serial_no for d in data]
|
||||
self.assertEqual(len(picked_serial_nos), 25)
|
||||
|
||||
pl.save()
|
||||
pl.submit()
|
||||
|
||||
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
|
||||
pl = create_pick_list(so1.name)
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.qty, 10.0)
|
||||
self.assertTrue(loc.serial_and_batch_bundle)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
fields=["qty", "batch_no"],
|
||||
filters={"parent": loc.serial_and_batch_bundle},
|
||||
)
|
||||
|
||||
self.assertEqual(len(data), 10)
|
||||
for d in data:
|
||||
self.assertTrue(d.serial_no not in picked_serial_nos)
|
||||
|
||||
pl.save()
|
||||
pl.submit()
|
||||
pl.cancel()
|
||||
|
||||
def test_picklist_with_bundles(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
@ -732,7 +848,7 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
dn.cancel()
|
||||
pl.reload()
|
||||
self.assertEqual(pl.status, "Completed")
|
||||
self.assertEqual(pl.status, "Open")
|
||||
|
||||
pl.cancel()
|
||||
pl.reload()
|
||||
|
@ -88,6 +88,20 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
if (!frm.doc.is_return) {
|
||||
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
|
||||
if (value) {
|
||||
frm.doc.items.forEach((item) => {
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"rate", "read_only", (item.purchase_order && item.purchase_order_item)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
frm.events.add_custom_buttons(frm);
|
||||
},
|
||||
|
||||
|
@ -359,7 +359,6 @@
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"print_width": "100px",
|
||||
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@ -1104,7 +1103,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-30 16:12:02.364608",
|
||||
"modified": "2023-12-25 22:32:09.801965",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
@ -85,6 +85,7 @@ class SerialandBatchBundle(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.set_batch_no()
|
||||
self.validate_serial_and_batch_no()
|
||||
self.validate_duplicate_serial_and_batch_no()
|
||||
self.validate_voucher_no()
|
||||
@ -99,6 +100,26 @@ class SerialandBatchBundle(Document):
|
||||
self.set_incoming_rate()
|
||||
self.calculate_qty_and_amount()
|
||||
|
||||
def set_batch_no(self):
|
||||
if self.has_serial_no and self.has_batch_no:
|
||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
has_no_batch = any(not d.batch_no for d in self.entries)
|
||||
if not has_no_batch:
|
||||
return
|
||||
|
||||
serial_no_batch = frappe._dict(
|
||||
frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"name": ("in", serial_nos)},
|
||||
fields=["name", "batch_no"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
for row in self.entries:
|
||||
if not row.batch_no:
|
||||
row.batch_no = serial_no_batch.get(row.serial_no)
|
||||
|
||||
def validate_serial_nos_inventory(self):
|
||||
if not (self.has_serial_no and self.type_of_transaction == "Outward"):
|
||||
return
|
||||
@ -915,7 +936,7 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
if index == 0:
|
||||
has_serial_no = row[0] == "Serial No"
|
||||
has_batch_no = row[0] == "Batch No"
|
||||
if not has_batch_no:
|
||||
if not has_batch_no and len(row) > 1:
|
||||
has_batch_no = row[1] == "Batch No"
|
||||
|
||||
continue
|
||||
@ -1164,7 +1185,7 @@ def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
|
||||
doc.append(
|
||||
"entries",
|
||||
{
|
||||
"qty": (row.qty or 1.0) * (1 if type_of_transaction == "Inward" else -1),
|
||||
"qty": (flt(row.qty) or 1.0) * (1 if type_of_transaction == "Inward" else -1),
|
||||
"warehouse": warehouse,
|
||||
"batch_no": row.batch_no,
|
||||
"serial_no": row.serial_no,
|
||||
@ -1192,7 +1213,7 @@ def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
|
||||
doc.append(
|
||||
"entries",
|
||||
{
|
||||
"qty": d.get("qty") * (1 if doc.type_of_transaction == "Inward" else -1),
|
||||
"qty": (flt(d.get("qty")) or 1.0) * (1 if doc.type_of_transaction == "Inward" else -1),
|
||||
"warehouse": warehouse or d.get("warehouse"),
|
||||
"batch_no": d.get("batch_no"),
|
||||
"serial_no": d.get("serial_no"),
|
||||
@ -1590,10 +1611,17 @@ def get_auto_batch_nos(kwargs):
|
||||
stock_ledgers_batches = get_stock_ledgers_batches(kwargs)
|
||||
pos_invoice_batches = get_reserved_batches_for_pos(kwargs)
|
||||
sre_reserved_batches = get_reserved_batches_for_sre(kwargs)
|
||||
picked_batches = frappe._dict()
|
||||
if kwargs.get("is_pick_list"):
|
||||
picked_batches = get_picked_batches(kwargs)
|
||||
|
||||
if stock_ledgers_batches or pos_invoice_batches or sre_reserved_batches:
|
||||
if stock_ledgers_batches or pos_invoice_batches or sre_reserved_batches or picked_batches:
|
||||
update_available_batches(
|
||||
available_batches, stock_ledgers_batches, pos_invoice_batches, sre_reserved_batches
|
||||
available_batches,
|
||||
stock_ledgers_batches,
|
||||
pos_invoice_batches,
|
||||
sre_reserved_batches,
|
||||
picked_batches,
|
||||
)
|
||||
|
||||
if not kwargs.consider_negative_batches:
|
||||
@ -1750,6 +1778,102 @@ def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]:
|
||||
return group_by_voucher
|
||||
|
||||
|
||||
def get_picked_batches(kwargs) -> dict[str, dict]:
|
||||
picked_batches = frappe._dict()
|
||||
|
||||
table = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
child_table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
pick_list_table = frappe.qb.DocType("Pick List")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child_table)
|
||||
.on(table.name == child_table.parent)
|
||||
.inner_join(pick_list_table)
|
||||
.on(table.voucher_no == pick_list_table.name)
|
||||
.select(
|
||||
child_table.batch_no,
|
||||
child_table.warehouse,
|
||||
Sum(child_table.qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(table.docstatus != 2)
|
||||
& (pick_list_table.status != "Completed")
|
||||
& (table.type_of_transaction == "Outward")
|
||||
& (table.is_cancelled == 0)
|
||||
& (table.voucher_type == "Pick List")
|
||||
& (table.voucher_no.isnotnull())
|
||||
)
|
||||
)
|
||||
|
||||
if kwargs.get("item_code"):
|
||||
query = query.where(table.item_code == kwargs.get("item_code"))
|
||||
|
||||
if kwargs.get("warehouse"):
|
||||
if isinstance(kwargs.warehouse, list):
|
||||
query = query.where(table.warehouse.isin(kwargs.warehouse))
|
||||
else:
|
||||
query = query.where(table.warehouse == kwargs.get("warehouse"))
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
for row in data:
|
||||
if not row.qty:
|
||||
continue
|
||||
|
||||
key = (row.batch_no, row.warehouse)
|
||||
if key not in picked_batches:
|
||||
picked_batches[key] = frappe._dict(
|
||||
{
|
||||
"qty": row.qty,
|
||||
"warehouse": row.warehouse,
|
||||
}
|
||||
)
|
||||
else:
|
||||
picked_batches[key].qty += row.qty
|
||||
|
||||
return picked_batches
|
||||
|
||||
|
||||
def get_picked_serial_nos(item_code, warehouse=None) -> list[str]:
|
||||
table = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
child_table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
pick_list_table = frappe.qb.DocType("Pick List")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child_table)
|
||||
.on(table.name == child_table.parent)
|
||||
.inner_join(pick_list_table)
|
||||
.on(table.voucher_no == pick_list_table.name)
|
||||
.select(
|
||||
child_table.serial_no,
|
||||
)
|
||||
.where(
|
||||
(table.docstatus != 2)
|
||||
& (pick_list_table.status != "Completed")
|
||||
& (table.type_of_transaction == "Outward")
|
||||
& (table.is_cancelled == 0)
|
||||
& (table.voucher_type == "Pick List")
|
||||
& (table.voucher_no.isnotnull())
|
||||
)
|
||||
)
|
||||
|
||||
if item_code:
|
||||
query = query.where(table.item_code == item_code)
|
||||
|
||||
if warehouse:
|
||||
if isinstance(warehouse, list):
|
||||
query = query.where(table.warehouse.isin(warehouse))
|
||||
else:
|
||||
query = query.where(table.warehouse == warehouse)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
if not data:
|
||||
return []
|
||||
|
||||
return [row.serial_no for row in data if row.serial_no]
|
||||
|
||||
|
||||
def get_ledgers_from_serial_batch_bundle(**kwargs) -> List[frappe._dict]:
|
||||
bundle_table = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
serial_batch_table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
@ -512,7 +512,12 @@ frappe.ui.form.on('Stock Entry', {
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
["actual_qty", "basic_rate"].forEach((field) => {
|
||||
let fields = ["actual_qty", "basic_rate"];
|
||||
if (frm.doc.purpose == "Material Receipt") {
|
||||
fields = ["actual_qty"];
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
|
||||
});
|
||||
frm.events.calculate_basic_amount(frm, child);
|
||||
|
@ -171,7 +171,7 @@ class StockReconciliation(StockController):
|
||||
},
|
||||
)
|
||||
|
||||
if item_details.has_batch_no:
|
||||
elif item_details.has_batch_no:
|
||||
batch_nos_details = get_available_batches(
|
||||
frappe._dict(
|
||||
{
|
||||
@ -228,6 +228,9 @@ class StockReconciliation(StockController):
|
||||
|
||||
def set_new_serial_and_batch_bundle(self):
|
||||
for item in self.items:
|
||||
if not item.qty:
|
||||
continue
|
||||
|
||||
if item.current_serial_and_batch_bundle and not item.serial_and_batch_bundle:
|
||||
current_doc = frappe.get_doc("Serial and Batch Bundle", item.current_serial_and_batch_bundle)
|
||||
|
||||
|
@ -865,6 +865,66 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
sr1.load_from_db()
|
||||
self.assertEqual(sr1.difference_amount, 10000)
|
||||
|
||||
def test_make_stock_zero_for_serial_batch_item(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
serial_item = self.make_item(
|
||||
properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "DJJ.####"}
|
||||
).name
|
||||
batch_item = self.make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BDJJ.####",
|
||||
"create_new_batch": 1,
|
||||
}
|
||||
).name
|
||||
|
||||
serial_batch_item = self.make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "ADJJ.####",
|
||||
"create_new_batch": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-ADJJ.####",
|
||||
}
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
for item_code in [serial_item, batch_item, serial_batch_item]:
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target=warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
_reco = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=0.0,
|
||||
)
|
||||
|
||||
serial_batch_bundle = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
{"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0, "voucher_no": _reco.name},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
self.assertEqual(len(serial_batch_bundle), 1)
|
||||
|
||||
_reco.cancel()
|
||||
|
||||
serial_batch_bundle = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
{"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0, "voucher_no": _reco.name},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
self.assertEqual(len(serial_batch_bundle), 0)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
@ -9,7 +9,7 @@ from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
from erpnext.stock.utils import get_or_make_bin, get_stock_balance
|
||||
|
||||
|
||||
class StockReservationEntry(Document):
|
||||
@ -151,7 +151,7 @@ class StockReservationEntry(Document):
|
||||
"""Validates `Reserved Qty` when `Reservation Based On` is `Qty`."""
|
||||
|
||||
if self.reservation_based_on == "Qty":
|
||||
self.validate_with_max_reserved_qty(self.reserved_qty)
|
||||
self.validate_with_allowed_qty(self.reserved_qty)
|
||||
|
||||
def auto_reserve_serial_and_batch(self, based_on: str = None) -> None:
|
||||
"""Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`."""
|
||||
@ -324,7 +324,7 @@ class StockReservationEntry(Document):
|
||||
frappe.throw(msg)
|
||||
|
||||
# Should be called after validating Serial and Batch Nos.
|
||||
self.validate_with_max_reserved_qty(qty_to_be_reserved)
|
||||
self.validate_with_allowed_qty(qty_to_be_reserved)
|
||||
self.db_set("reserved_qty", qty_to_be_reserved)
|
||||
|
||||
def update_reserved_qty_in_voucher(
|
||||
@ -429,7 +429,7 @@ class StockReservationEntry(Document):
|
||||
msg = _("Stock Reservation Entry cannot be updated as it has been delivered.")
|
||||
frappe.throw(msg)
|
||||
|
||||
def validate_with_max_reserved_qty(self, qty_to_be_reserved: float) -> None:
|
||||
def validate_with_allowed_qty(self, qty_to_be_reserved: float) -> None:
|
||||
"""Validates `Reserved Qty` with `Max Reserved Qty`."""
|
||||
|
||||
self.db_set(
|
||||
@ -448,12 +448,12 @@ class StockReservationEntry(Document):
|
||||
)
|
||||
voucher_delivered_qty = flt(delivered_qty) * flt(conversion_factor)
|
||||
|
||||
max_reserved_qty = min(
|
||||
allowed_qty = min(
|
||||
self.available_qty, (self.voucher_qty - voucher_delivered_qty - total_reserved_qty)
|
||||
)
|
||||
|
||||
if max_reserved_qty <= 0 and self.voucher_type == "Sales Order":
|
||||
msg = _("Item {0} is already delivered for Sales Order {1}.").format(
|
||||
if self.get("_action") != "submit" and self.voucher_type == "Sales Order" and allowed_qty <= 0:
|
||||
msg = _("Item {0} is already reserved/delivered against Sales Order {1}.").format(
|
||||
frappe.bold(self.item_code), frappe.bold(self.voucher_no)
|
||||
)
|
||||
|
||||
@ -463,19 +463,33 @@ class StockReservationEntry(Document):
|
||||
else:
|
||||
frappe.throw(msg)
|
||||
|
||||
if qty_to_be_reserved > max_reserved_qty:
|
||||
if qty_to_be_reserved > allowed_qty:
|
||||
actual_qty = get_stock_balance(self.item_code, self.warehouse)
|
||||
msg = """
|
||||
Cannot reserve more than Max Reserved Qty {0} {1}.<br /><br />
|
||||
The <b>Max Reserved Qty</b> is calculated as follows:<br />
|
||||
Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.<br /><br />
|
||||
The <b>Allowed Qty</b> is calculated as follows:<br />
|
||||
<ul>
|
||||
<li><b>Available Qty To Reserve</b> = (Actual Stock Qty - Reserved Stock Qty)</li>
|
||||
<li><b>Voucher Qty</b> = Voucher Item Qty</li>
|
||||
<li><b>Delivered Qty</b> = Qty delivered against the Voucher Item</li>
|
||||
<li><b>Total Reserved Qty</b> = Qty reserved against the Voucher Item</li>
|
||||
<li><b>Max Reserved Qty</b> = Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))</li>
|
||||
<li>Actual Qty [Available Qty at Warehouse] = {5}</li>
|
||||
<li>Reserved Stock [Ignore current SRE] = {6}</li>
|
||||
<li>Available Qty To Reserve [Actual Qty - Reserved Stock] = {7}</li>
|
||||
<li>Voucher Qty [Voucher Item Qty] = {8}</li>
|
||||
<li>Delivered Qty [Qty delivered against the Voucher Item] = {9}</li>
|
||||
<li>Total Reserved Qty [Qty reserved against the Voucher Item] = {10}</li>
|
||||
<li>Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {11}</li>
|
||||
</ul>
|
||||
""".format(
|
||||
frappe.bold(max_reserved_qty), self.stock_uom
|
||||
frappe.bold(allowed_qty),
|
||||
self.stock_uom,
|
||||
frappe.bold(self.item_code),
|
||||
self.voucher_type,
|
||||
frappe.bold(self.voucher_no),
|
||||
actual_qty,
|
||||
actual_qty - self.available_qty,
|
||||
self.available_qty,
|
||||
self.voucher_qty,
|
||||
voucher_delivered_qty,
|
||||
total_reserved_qty,
|
||||
allowed_qty,
|
||||
)
|
||||
frappe.throw(msg)
|
||||
|
||||
@ -509,7 +523,6 @@ def get_available_qty_to_reserve(
|
||||
"""Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item, Warehouse and Batch combination."""
|
||||
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
from erpnext.stock.utils import get_stock_balance
|
||||
|
||||
if batch_no:
|
||||
return get_batch_qty(
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils import cint, flt, get_table_name, getdate
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from pypika import functions as fn
|
||||
|
||||
@ -13,11 +13,22 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
||||
SLE_COUNT_LIMIT = 10_000
|
||||
|
||||
|
||||
def _estimate_table_row_count(doctype: str):
|
||||
table = get_table_name(doctype)
|
||||
return cint(
|
||||
frappe.db.sql(
|
||||
f"""select table_rows
|
||||
from information_schema.tables
|
||||
where table_name = '{table}' ;"""
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
sle_count = frappe.db.count("Stock Ledger Entry")
|
||||
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
||||
|
||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
||||
|
@ -149,34 +149,36 @@ frappe.query_reports["Reserved Stock"] = {
|
||||
formatter: (value, row, column, data, default_formatter) => {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.fieldname == "status") {
|
||||
switch (data.status) {
|
||||
case "Partially Reserved":
|
||||
value = "<span style='color:orange'>" + value + "</span>";
|
||||
break;
|
||||
case "Reserved":
|
||||
value = "<span style='color:blue'>" + value + "</span>";
|
||||
break;
|
||||
case "Partially Delivered":
|
||||
value = "<span style='color:purple'>" + value + "</span>";
|
||||
break;
|
||||
case "Delivered":
|
||||
value = "<span style='color:green'>" + value + "</span>";
|
||||
break;
|
||||
if (data) {
|
||||
if (column.fieldname == "status") {
|
||||
switch (data.status) {
|
||||
case "Partially Reserved":
|
||||
value = "<span style='color:orange'>" + value + "</span>";
|
||||
break;
|
||||
case "Reserved":
|
||||
value = "<span style='color:blue'>" + value + "</span>";
|
||||
break;
|
||||
case "Partially Delivered":
|
||||
value = "<span style='color:purple'>" + value + "</span>";
|
||||
break;
|
||||
case "Delivered":
|
||||
value = "<span style='color:green'>" + value + "</span>";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (column.fieldname == "delivered_qty") {
|
||||
if (data.delivered_qty > 0) {
|
||||
if (data.reserved_qty > data.delivered_qty) {
|
||||
value = "<span style='color:blue'>" + value + "</span>";
|
||||
else if (column.fieldname == "delivered_qty") {
|
||||
if (data.delivered_qty > 0) {
|
||||
if (data.reserved_qty > data.delivered_qty) {
|
||||
value = "<span style='color:blue'>" + value + "</span>";
|
||||
}
|
||||
else {
|
||||
value = "<span style='color:green'>" + value + "</span>";
|
||||
}
|
||||
}
|
||||
else {
|
||||
value = "<span style='color:green'>" + value + "</span>";
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
}
|
||||
}
|
||||
else {
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
|
@ -25,6 +25,7 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor
|
||||
)
|
||||
from erpnext.stock.utils import (
|
||||
get_incoming_outgoing_rate_for_cancel,
|
||||
get_incoming_rate,
|
||||
get_or_make_bin,
|
||||
get_stock_balance,
|
||||
get_valuation_method,
|
||||
@ -841,14 +842,33 @@ class update_entries_after(object):
|
||||
get_rate_for_return, # don't move this import to top
|
||||
)
|
||||
|
||||
rate = get_rate_for_return(
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
sle.item_code,
|
||||
voucher_detail_no=sle.voucher_detail_no,
|
||||
sle=sle,
|
||||
)
|
||||
if self.valuation_method == "Moving Average":
|
||||
rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": sle.item_code,
|
||||
"warehouse": sle.warehouse,
|
||||
"posting_date": sle.posting_date,
|
||||
"posting_time": sle.posting_time,
|
||||
"qty": sle.actual_qty,
|
||||
"serial_no": sle.get("serial_no"),
|
||||
"batch_no": sle.get("batch_no"),
|
||||
"serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
|
||||
"company": sle.company,
|
||||
"voucher_type": sle.voucher_type,
|
||||
"voucher_no": sle.voucher_no,
|
||||
"allow_zero_valuation": self.allow_zero_rate,
|
||||
"sle": sle.name,
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
rate = get_rate_for_return(
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
sle.item_code,
|
||||
voucher_detail_no=sle.voucher_detail_no,
|
||||
sle=sle,
|
||||
)
|
||||
elif (
|
||||
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
|
||||
and sle.voucher_detail_no
|
||||
|
24
erpnext/tests/test_perf.py
Normal file
24
erpnext/tests/test_perf.py
Normal file
@ -0,0 +1,24 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
INDEXED_FIELDS = {
|
||||
"Bin": ["item_code"],
|
||||
"GL Entry": ["voucher_type", "against_voucher_type"],
|
||||
"Purchase Order Item": ["item_code"],
|
||||
"Stock Ledger Entry": ["warehouse"],
|
||||
}
|
||||
|
||||
|
||||
class TestPerformance(FrappeTestCase):
|
||||
def test_ensure_indexes(self):
|
||||
# These fields are not explicitly indexed BUT they are prefix in some
|
||||
# other composite index. If those are removed this test should be
|
||||
# updated accordingly.
|
||||
for doctype, fields in INDEXED_FIELDS.items():
|
||||
for field in fields:
|
||||
self.assertTrue(
|
||||
frappe.db.sql(
|
||||
f"""SHOW INDEX FROM `tab{doctype}`
|
||||
WHERE Column_name = "{field}" AND Seq_in_index = 1"""
|
||||
)
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user