diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 30be903ae8..6ea121f298 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -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
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 367b017969..6282e9a560 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -358,9 +358,11 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice":
+ against_type = "Customer"
against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account
else:
+ against_type = "Supplier"
against, project = doc.supplier, item.project
credit_account, debit_account = item.deferred_expense_account, item.expense_account
@@ -413,6 +415,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
doc,
credit_account,
debit_account,
+ against_type,
against,
amount,
base_amount,
@@ -494,6 +497,7 @@ def make_gl_entries(
doc,
credit_account,
debit_account,
+ against_type,
against,
amount,
base_amount,
@@ -515,7 +519,9 @@ def make_gl_entries(
doc.get_gl_dict(
{
"account": credit_account,
+ "against_type": against_type,
"against": against,
+ "against_link": against,
"credit": base_amount,
"credit_in_account_currency": amount,
"cost_center": cost_center,
@@ -534,7 +540,9 @@ def make_gl_entries(
doc.get_gl_dict(
{
"account": debit_account,
+ "against_type": against_type,
"against": against,
+ "against_link": against,
"debit": base_amount,
"debit_in_account_currency": amount,
"cost_center": cost_center,
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json b/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json
index 58d67beb67..bd7228ec41 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json
@@ -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"
}
- }
+ }
}
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ca_plan_comptable_pour_les_provinces_francophones.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ca_plan_comptable_pour_les_provinces_francophones.json
index 2811fc5fb6..2a30cbcbc9 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ca_plan_comptable_pour_les_provinces_francophones.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ca_plan_comptable_pour_les_provinces_francophones.json
@@ -33,7 +33,9 @@
},
"Stocks": {
"Mati\u00e8res premi\u00e8res": {},
- "Stock de produits fini": {},
+ "Stock de produits fini": {
+ "account_type": "Stock"
+ },
"Stock exp\u00e9di\u00e9 non-factur\u00e9": {},
"Travaux en cours": {},
"account_type": "Stock"
@@ -395,9 +397,11 @@
},
"Produits": {
"Revenus de ventes": {
- " Escomptes de volume sur ventes": {},
+ "Escomptes de volume sur ventes": {},
"Autres produits d'exploitation": {},
- "Ventes": {},
+ "Ventes": {
+ "account_type": "Income Account"
+ },
"Ventes avec des provinces harmonis\u00e9es": {},
"Ventes avec des provinces non-harmonis\u00e9es": {},
"Ventes \u00e0 l'\u00e9tranger": {}
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03.json
index 741d4283e2..daf2e21d78 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03.json
@@ -53,8 +53,13 @@
},
"II. Forderungen und sonstige Vermögensgegenstände": {
"is_group": 1,
- "Ford. a. Lieferungen und Leistungen": {
+ "Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1400",
+ "account_type": "Receivable",
+ "is_group": 1
+ },
+ "Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
+ "account_number": "1410",
"account_type": "Receivable"
},
"Durchlaufende Posten": {
@@ -180,8 +185,13 @@
},
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
"is_group": 1,
- "Verbindlichkeiten aus Lieferungen u. Leistungen": {
+ "Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1600",
+ "account_type": "Payable",
+ "is_group": 1
+ },
+ "Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
+ "account_number": "1610",
"account_type": "Payable"
}
},
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json
index 5063ec6076..09912e9896 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.json
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json
@@ -17,10 +17,13 @@
"account_currency",
"debit_in_account_currency",
"credit_in_account_currency",
+ "against_type",
"against",
+ "against_link",
"against_voucher_type",
"against_voucher",
"voucher_type",
+ "voucher_subtype",
"voucher_no",
"voucher_detail_no",
"project",
@@ -128,6 +131,13 @@
"label": "Credit Amount in Account Currency",
"options": "account_currency"
},
+ {
+ "fieldname": "against_type",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "label": "Against Type",
+ "options": "DocType"
+ },
{
"fieldname": "against",
"fieldtype": "Text",
@@ -136,14 +146,20 @@
"oldfieldname": "against",
"oldfieldtype": "Text"
},
+ {
+ "fieldname": "against_link",
+ "fieldtype": "Dynamic Link",
+ "in_filter": 1,
+ "label": "Against",
+ "options": "against_type"
+ },
{
"fieldname": "against_voucher_type",
"fieldtype": "Link",
"label": "Against Voucher Type",
"oldfieldname": "against_voucher_type",
"oldfieldtype": "Data",
- "options": "DocType",
- "search_index": 1
+ "options": "DocType"
},
{
"fieldname": "against_voucher",
@@ -162,8 +178,7 @@
"label": "Voucher Type",
"oldfieldname": "voucher_type",
"oldfieldtype": "Select",
- "options": "DocType",
- "search_index": 1
+ "options": "DocType"
},
{
"fieldname": "voucher_no",
@@ -280,13 +295,18 @@
"fieldtype": "Currency",
"label": "Credit Amount in Transaction Currency",
"options": "transaction_currency"
+ },
+ {
+ "fieldname": "voucher_subtype",
+ "fieldtype": "Small Text",
+ "label": "Voucher Subtype"
}
],
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2023-08-16 21:38:44.072267",
+ "modified": "2023-12-18 15:38:14.006208",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
@@ -321,4 +341,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index f7dd29ab1c..139f52696b 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -39,6 +39,8 @@ class GLEntry(Document):
account: DF.Link | None
account_currency: DF.Link | None
against: DF.Text | None
+ against_link: DF.DynamicLink | None
+ against_type: DF.Link | None
against_voucher: DF.DynamicLink | None
against_voucher_type: DF.Link | None
company: DF.Link | None
@@ -66,6 +68,7 @@ class GLEntry(Document):
transaction_exchange_rate: DF.Float
voucher_detail_no: DF.Data | None
voucher_no: DF.DynamicLink | None
+ voucher_subtype: DF.SmallText | None
voucher_type: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
index 76f4dadf87..69b0860a30 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
@@ -153,7 +153,9 @@ class InvoiceDiscounting(AccountsController):
"account": inv.debit_to,
"party_type": "Customer",
"party": d.customer,
+ "against_type": "Account",
"against": self.accounts_receivable_credit,
+ "against_link": self.accounts_receivable_credit,
"credit": outstanding_in_company_currency,
"credit_in_account_currency": outstanding_in_company_currency
if inv.party_account_currency == company_currency
@@ -173,7 +175,9 @@ class InvoiceDiscounting(AccountsController):
"account": self.accounts_receivable_credit,
"party_type": "Customer",
"party": d.customer,
+ "against_type": "Account",
"against": inv.debit_to,
+ "against_link": inv.debit_to,
"debit": outstanding_in_company_currency,
"debit_in_account_currency": outstanding_in_company_currency
if ar_credit_account_currency == company_currency
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 266154d87f..c97a8dc18d 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -220,6 +220,16 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
return erpnext.journal_entry.account_query(me.frm);
});
+ me.frm.set_query("against_account_link", "accounts", function(doc, cdt, cdn) {
+ return erpnext.journal_entry.against_account_query(me.frm);
+ });
+
+ me.frm.set_query("against_type", "accounts", function(){
+ return {
+ query: "erpnext.accounts.doctype.journal_entry.journal_entry.get_against_type",
+ }
+ })
+
me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
@@ -591,6 +601,21 @@ $.extend(erpnext.journal_entry, {
return { filters: filters };
},
+ against_account_query: function(frm) {
+ if (frm.doc.against_type != "Account"){
+ return { filters: {} };
+ }
+ else {
+ let filters = { company: frm.doc.company, is_group: 0 };
+ if(!frm.doc.multi_currency) {
+ $.extend(filters, {
+ account_currency: ['in', [frappe.get_doc(":Company", frm.doc.company).default_currency, null]]
+ });
+ }
+ return { filters: filters };
+ }
+ },
+
reverse_journal_entry: function() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_reverse_journal_entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 40d552bc88..79f1ab063d 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -304,6 +304,7 @@ class JournalEntry(AccountsController):
"account": tax_withholding_details.get("account_head"),
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
"against_account": parties[0],
+ "against_account_link": parties[0],
},
)
@@ -750,27 +751,90 @@ class JournalEntry(AccountsController):
)
def set_against_account(self):
- accounts_debited, accounts_credited = [], []
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
for d in self.get("accounts"):
if d.reference_type == "Sales Invoice":
- field = "customer"
+ against_type = "Customer"
else:
- field = "supplier"
+ against_type = "Supplier"
- d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
+ against_account = frappe.db.get_value(d.reference_type, d.reference_name, against_type.lower())
+ d.against_type = against_type
+ d.against_account_link = against_account
else:
- for d in self.get("accounts"):
- if flt(d.debit) > 0:
- accounts_debited.append(d.party or d.account)
- if flt(d.credit) > 0:
- accounts_credited.append(d.party or d.account)
+ self.get_debited_credited_accounts()
+ if len(self.accounts_credited) > 1 and len(self.accounts_debited) > 1:
+ self.auto_set_against_accounts()
+ return
+ self.get_against_accounts()
- for d in self.get("accounts"):
- if flt(d.debit) > 0:
- d.against_account = ", ".join(list(set(accounts_credited)))
- if flt(d.credit) > 0:
- d.against_account = ", ".join(list(set(accounts_debited)))
+ def auto_set_against_accounts(self):
+ for i in range(0, len(self.accounts), 2):
+ acc = self.accounts[i]
+ against_acc = self.accounts[i + 1]
+ if acc.debit_in_account_currency > 0:
+ current_val = acc.debit_in_account_currency * flt(acc.exchange_rate)
+ against_val = against_acc.credit_in_account_currency * flt(against_acc.exchange_rate)
+ else:
+ current_val = acc.credit_in_account_currency * flt(acc.exchange_rate)
+ against_val = against_acc.debit_in_account_currency * flt(against_acc.exchange_rate)
+
+ if current_val == against_val:
+ acc.against_type = against_acc.party_type or "Account"
+ against_acc.against_type = acc.party_type or "Account"
+
+ acc.against_account_link = against_acc.party or against_acc.account
+ against_acc.against_account_link = acc.party or acc.account
+ else:
+ frappe.msgprint(
+ _(
+ "Unable to automatically determine {0} accounts. Set them up in the {1} table if needed."
+ ).format(frappe.bold("against"), frappe.bold("Accounting Entries")),
+ alert=True,
+ )
+ break
+
+ def get_against_accounts(self):
+ self.against_accounts = []
+ self.split_account = {}
+ self.get_debited_credited_accounts()
+
+ if self.separate_against_account_entries:
+ no_of_credited_acc, no_of_debited_acc = len(self.accounts_credited), len(self.accounts_debited)
+ if no_of_credited_acc <= 1 and no_of_debited_acc <= 1:
+ self.set_against_accounts_for_single_dr_cr()
+ self.separate_against_account_entries = 0
+ elif no_of_credited_acc == 1:
+ self.against_accounts = self.accounts_debited
+ self.split_account = self.accounts_credited[0]
+ elif no_of_debited_acc == 1:
+ self.against_accounts = self.accounts_credited
+ self.split_account = self.accounts_debited[0]
+
+ def get_debited_credited_accounts(self):
+ self.accounts_debited, self.accounts_credited = [], []
+ self.separate_against_account_entries = 1
+ for d in self.get("accounts"):
+ if flt(d.debit) > 0:
+ self.accounts_debited.append(d)
+ elif flt(d.credit) > 0:
+ self.accounts_credited.append(d)
+
+ if d.against_account_link:
+ self.separate_against_account_entries = 0
+ break
+
+ def set_against_accounts_for_single_dr_cr(self):
+ against_account = None
+ for d in self.accounts:
+ if flt(d.debit) > 0:
+ against_account = self.accounts_credited[0]
+ elif flt(d.credit) > 0:
+ against_account = self.accounts_debited[0]
+ if against_account:
+ d.against_type = against_account.party_type or "Account"
+ d.against_account = against_account.party or against_account.account
+ d.against_account_link = against_account.party or against_account.account
def validate_debit_credit_amount(self):
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
@@ -967,40 +1031,82 @@ class JournalEntry(AccountsController):
def build_gl_map(self):
gl_map = []
+ self.get_against_accounts()
for d in self.get("accounts"):
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark]
r = [x for x in r if x]
remarks = "\n".join(r)
- gl_map.append(
- self.get_gl_dict(
- {
- "account": d.account,
- "party_type": d.party_type,
- "due_date": self.due_date,
- "party": d.party,
- "against": d.against_account,
- "debit": flt(d.debit, d.precision("debit")),
- "credit": flt(d.credit, d.precision("credit")),
- "account_currency": d.account_currency,
- "debit_in_account_currency": flt(
- d.debit_in_account_currency, d.precision("debit_in_account_currency")
- ),
- "credit_in_account_currency": flt(
- d.credit_in_account_currency, d.precision("credit_in_account_currency")
- ),
- "against_voucher_type": d.reference_type,
- "against_voucher": d.reference_name,
- "remarks": remarks,
- "voucher_detail_no": d.reference_detail_no,
- "cost_center": d.cost_center,
- "project": d.project,
- "finance_book": self.finance_book,
- },
- item=d,
- )
+ gl_dict = self.get_gl_dict(
+ {
+ "account": d.account,
+ "party_type": d.party_type,
+ "due_date": self.due_date,
+ "party": d.party,
+ "debit": flt(d.debit, d.precision("debit")),
+ "credit": flt(d.credit, d.precision("credit")),
+ "account_currency": d.account_currency,
+ "debit_in_account_currency": flt(
+ d.debit_in_account_currency, d.precision("debit_in_account_currency")
+ ),
+ "credit_in_account_currency": flt(
+ d.credit_in_account_currency, d.precision("credit_in_account_currency")
+ ),
+ "against_voucher_type": d.reference_type,
+ "against_voucher": d.reference_name,
+ "remarks": remarks,
+ "voucher_detail_no": d.reference_detail_no,
+ "cost_center": d.cost_center,
+ "project": d.project,
+ "finance_book": self.finance_book,
+ },
+ item=d,
)
+
+ if not self.separate_against_account_entries:
+ gl_dict.update(
+ {
+ "against_type": d.against_type,
+ "against_link": d.against_account_link,
+ }
+ )
+ gl_map.append(gl_dict)
+
+ elif d in self.against_accounts:
+ gl_dict.update(
+ {
+ "against_type": self.split_account.get("party_type") or "Account",
+ "against": self.split_account.get("party") or self.split_account.get("account"),
+ "against_link": self.split_account.get("party") or self.split_account.get("account"),
+ }
+ )
+ gl_map.append(gl_dict)
+
+ else:
+ for against_account in self.against_accounts:
+ against_account = against_account.as_dict()
+ debit = against_account.credit or against_account.credit_in_account_currency
+ credit = against_account.debit or against_account.debit_in_account_currency
+ gl_dict = gl_dict.copy()
+ gl_dict.update(
+ {
+ "against_type": against_account.party_type or "Account",
+ "against": against_account.party or against_account.account,
+ "against_link": against_account.party or against_account.account,
+ "debit": flt(debit, d.precision("debit")),
+ "credit": flt(credit, d.precision("credit")),
+ "account_currency": d.account_currency,
+ "debit_in_account_currency": flt(
+ debit / d.exchange_rate, d.precision("debit_in_account_currency")
+ ),
+ "credit_in_account_currency": flt(
+ credit / d.exchange_rate, d.precision("credit_in_account_currency")
+ ),
+ }
+ )
+ gl_map.append(gl_dict)
+
return gl_map
def make_gl_entries(self, cancel=0, adv_adj=0):
@@ -1625,3 +1731,10 @@ def make_reverse_journal_entry(source_name, target_doc=None):
)
return doclist
+
+
+@frappe.whitelist()
+def get_against_type(doctype, txt, searchfield, start, page_len, filters):
+ against_types = frappe.db.get_list("Party Type", pluck="name") + ["Account"]
+ doctype = frappe.qb.DocType("DocType")
+ return frappe.qb.from_(doctype).select(doctype.name).where(doctype.name.isin(against_types)).run()
diff --git a/erpnext/accounts/doctype/journal_entry/test_records.json b/erpnext/accounts/doctype/journal_entry/test_records.json
index dafcf56abd..717c579c7a 100644
--- a/erpnext/accounts/doctype/journal_entry/test_records.json
+++ b/erpnext/accounts/doctype/journal_entry/test_records.json
@@ -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"
+ }
]
diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
index 3132fe9b12..01006bd3db 100644
--- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
+++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
@@ -37,7 +37,9 @@
"col_break3",
"is_advance",
"user_remark",
- "against_account"
+ "against_type",
+ "against_account",
+ "against_account_link"
],
"fields": [
{
@@ -250,14 +252,21 @@
"print_hide": 1
},
{
- "fieldname": "against_account",
- "fieldtype": "Text",
- "hidden": 1,
+ "fieldname": "against_account",
+ "fieldtype": "Text",
+ "hidden": 1,
+ "label": "Against Account",
+ "no_copy": 1,
+ "oldfieldname": "against_account",
+ "oldfieldtype": "Text",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "against_account_link",
+ "fieldtype": "Dynamic Link",
"label": "Against Account",
"no_copy": 1,
- "oldfieldname": "against_account",
- "oldfieldtype": "Text",
- "print_hide": 1
+ "options": "against_type"
},
{
"collapsible": 1,
@@ -280,14 +289,19 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Detail No",
- "no_copy": 1,
- "search_index": 1
+ "no_copy": 1
+ },
+ {
+ "fieldname": "against_type",
+ "fieldtype": "Link",
+ "label": "Against Type",
+ "options": "DocType"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-23 11:44:25.841187",
+ "modified": "2023-12-02 23:21:22.205409",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index a6ddce5223..11c7c179b6 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1061,7 +1061,9 @@ class PaymentEntry(AccountsController):
"account": self.party_account,
"party_type": self.party_type,
"party": self.party,
+ "against_type": "Account",
"against": against_account,
+ "against_link": against_account,
"account_currency": self.party_account_currency,
"cost_center": self.cost_center,
},
@@ -1226,7 +1228,9 @@ class PaymentEntry(AccountsController):
{
"account": self.paid_from,
"account_currency": self.paid_from_account_currency,
+ "against_type": self.party_type if self.payment_type == "Pay" else "Account",
"against": self.party if self.payment_type == "Pay" else self.paid_to,
+ "against_link": self.party if self.payment_type == "Pay" else self.paid_to,
"credit_in_account_currency": self.paid_amount,
"credit": self.base_paid_amount,
"cost_center": self.cost_center,
@@ -1241,7 +1245,9 @@ class PaymentEntry(AccountsController):
{
"account": self.paid_to,
"account_currency": self.paid_to_account_currency,
+ "against_type": self.party_type if self.payment_type == "Receive" else "Account",
"against": self.party if self.payment_type == "Receive" else self.paid_from,
+ "against_link": self.party if self.payment_type == "Receive" else self.paid_from,
"debit_in_account_currency": self.received_amount,
"debit": self.base_received_amount,
"cost_center": self.cost_center,
@@ -1265,6 +1271,7 @@ class PaymentEntry(AccountsController):
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_to
+ against_type = self.party_type or "Account"
payment_account = self.get_party_account_for_taxes()
tax_amount = d.tax_amount
base_tax_amount = d.base_tax_amount
@@ -1273,7 +1280,9 @@ class PaymentEntry(AccountsController):
self.get_gl_dict(
{
"account": d.account_head,
+ "against_type": against_type,
"against": against,
+ "against_link": against,
dr_or_cr: tax_amount,
dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency == self.company_currency
@@ -1298,7 +1307,9 @@ class PaymentEntry(AccountsController):
self.get_gl_dict(
{
"account": payment_account,
+ "against_type": against_type,
"against": against,
+ "against_link": against,
rev_dr_or_cr: tax_amount,
rev_dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency == self.company_currency
@@ -1323,7 +1334,9 @@ class PaymentEntry(AccountsController):
{
"account": d.account,
"account_currency": account_currency,
+ "against_type": self.party_type or "Account",
"against": self.party or self.paid_from,
+ "against_link": self.party or self.paid_from,
"debit_in_account_currency": d.amount,
"debit": d.amount,
"cost_center": d.cost_center,
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
index a3a953f910..ae6059c005 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -15,6 +15,7 @@
"group_by",
"cost_center",
"territory",
+ "ignore_exchange_rate_revaluation_journals",
"column_break_14",
"to_date",
"finance_book",
@@ -376,10 +377,16 @@
"fieldname": "pdf_name",
"fieldtype": "Data",
"label": "PDF Name"
+ },
+ {
+ "default": "0",
+ "fieldname": "ignore_exchange_rate_revaluation_journals",
+ "fieldtype": "Check",
+ "label": "Ignore Exchange Rate Revaluation Journals"
}
],
"links": [],
- "modified": "2023-08-28 12:59:53.071334",
+ "modified": "2023-12-18 12:20:08.965120",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 9ad25483e3..c03b18a871 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -56,6 +56,7 @@ class ProcessStatementOfAccounts(Document):
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
from_date: DF.Date | None
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
+ ignore_exchange_rate_revaluation_journals: DF.Check
include_ageing: DF.Check
include_break: DF.Check
letter_head: DF.Link | None
@@ -119,6 +120,18 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
+ err_journals = None
+ if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
+ err_journals = frappe.db.get_all(
+ "Journal Entry",
+ filters={
+ "company": doc.company,
+ "docstatus": 1,
+ "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
+ },
+ as_list=True,
+ )
+
for entry in doc.customers:
if doc.include_ageing:
ageing = set_ageing(doc, entry)
@@ -131,6 +144,8 @@ def get_statement_dict(doc, get_statement_dict=False):
)
filters = get_common_filters(doc)
+ if err_journals:
+ filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index cebd61a6f5..215d8ec215 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -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);
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index ae377ebafd..f40824dff4 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -815,7 +815,9 @@ class PurchaseInvoice(BuyingController):
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
+ "against_type": "Account",
"against": self.against_expense_account,
+ "against_link": self.against_expense_account,
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
@@ -888,7 +890,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": warehouse_account[item.warehouse]["account"],
+ "against_type": "Account",
"against": warehouse_account[item.from_warehouse]["account"],
+ "against_link": warehouse_account[item.from_warehouse]["account"],
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
@@ -908,7 +912,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": warehouse_account[item.from_warehouse]["account"],
+ "against_type": "Account",
"against": warehouse_account[item.warehouse]["account"],
+ "against_link": warehouse_account[item.warehouse]["account"],
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
@@ -925,7 +931,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": item.expense_account,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
@@ -942,7 +950,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": item.expense_account,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"debit": warehouse_debit_amount,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
@@ -961,7 +971,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": account,
+ "against_type": "Account",
"against": item.expense_account,
+ "against_link": item.expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]),
@@ -981,7 +993,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": supplier_warehouse_account,
+ "against_type": "Account",
"against": item.expense_account,
+ "against_link": item.expense_account,
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
@@ -1036,7 +1050,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": expense_account,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"debit": amount,
"cost_center": item.cost_center,
"project": item.project or self.project,
@@ -1062,7 +1078,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": expense_account,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"debit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center,
"project": item.project or self.project,
@@ -1075,7 +1093,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": self.get_company_default("exchange_gain_loss_account"),
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"credit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center,
"project": item.project or self.project,
@@ -1120,7 +1140,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": stock_rbnb,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"remarks": self.remarks or _("Accounting Entry for Stock"),
"cost_center": self.cost_center,
@@ -1168,7 +1190,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": cost_of_goods_sold_account,
+ "against_type": "Account",
"against": item.expense_account,
+ "against_link": item.expense_account,
"debit": stock_adjustment_amt,
"remarks": self.get("remarks") or _("Stock Adjustment"),
"cost_center": item.cost_center,
@@ -1198,7 +1222,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": tax.account_head,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
dr_or_cr: base_amount,
dr_or_cr + "_in_account_currency": base_amount
if account_currency == self.company_currency
@@ -1246,8 +1272,10 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": tax.account_head,
+ "against_type": "Supplier",
"cost_center": tax.cost_center,
"against": self.supplier,
+ "against_link": self.supplier,
"credit": applicable_amount,
"remarks": self.remarks or _("Accounting Entry for Stock"),
},
@@ -1265,7 +1293,9 @@ class PurchaseInvoice(BuyingController):
{
"account": tax.account_head,
"cost_center": tax.cost_center,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"credit": valuation_tax[tax.name],
"remarks": self.remarks or _("Accounting Entry for Stock"),
},
@@ -1280,7 +1310,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": self.unrealized_profit_loss_account,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"credit": flt(self.total_taxes_and_charges),
"credit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center,
@@ -1301,7 +1333,9 @@ class PurchaseInvoice(BuyingController):
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
+ "against_type": "Account",
"against": self.cash_bank_account,
+ "against_link": self.cash_bank_account,
"debit": self.base_paid_amount,
"debit_in_account_currency": self.base_paid_amount
if self.party_account_currency == self.company_currency
@@ -1322,7 +1356,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": self.cash_bank_account,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"credit": self.base_paid_amount,
"credit_in_account_currency": self.base_paid_amount
if bank_account_currency == self.company_currency
@@ -1346,7 +1382,9 @@ class PurchaseInvoice(BuyingController):
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
+ "against_type": "Account",
"against": self.write_off_account,
+ "against_link": self.write_off_account,
"debit": self.base_write_off_amount,
"debit_in_account_currency": self.base_write_off_amount
if self.party_account_currency == self.company_currency
@@ -1366,7 +1404,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": self.write_off_account,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"credit": flt(self.base_write_off_amount),
"credit_in_account_currency": self.base_write_off_amount
if write_off_account_currency == self.company_currency
@@ -1393,7 +1433,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict(
{
"account": round_off_account,
+ "against_type": "Supplier",
"against": self.supplier,
+ "against_link": self.supplier,
"debit_in_account_currency": self.rounding_adjustment,
"debit": self.base_rounding_adjustment,
"cost_center": round_off_cost_center
@@ -1816,10 +1858,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):
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 7cad3ae1c0..9cf4e4fd7c 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -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",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f2f4dda817..2cddb863c6 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -7,7 +7,7 @@ from frappe import _, msgprint, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
-from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
+from frappe.utils import add_days, cint, flt, formatdate, get_link_to_form, getdate, nowdate
import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date
@@ -1225,7 +1225,9 @@ class SalesInvoice(SellingController):
"party_type": "Customer",
"party": self.customer,
"due_date": self.due_date,
+ "against_type": "Account",
"against": self.against_income_account,
+ "against_link": self.against_income_account,
"debit": base_grand_total,
"debit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
@@ -1254,7 +1256,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict(
{
"account": tax.account_head,
+ "against_type": "Customer",
"against": self.customer,
+ "against_link": self.customer,
"credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")),
"credit_in_account_currency": (
flt(base_amount, tax.precision("base_tax_amount_after_discount_amount"))
@@ -1275,7 +1279,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict(
{
"account": self.unrealized_profit_loss_account,
+ "against_type": "Customer",
"against": self.customer,
+ "against_link": self.customer,
"debit": flt(self.total_taxes_and_charges),
"debit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center,
@@ -1343,7 +1349,9 @@ class SalesInvoice(SellingController):
add_asset_activity(asset.name, _("Asset sold"))
for gle in fixed_asset_gl_entries:
+ gle["against_type"] = "Customer"
gle["against"] = self.customer
+ gle["against_link"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
self.set_asset_status(asset)
@@ -1364,7 +1372,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict(
{
"account": income_account,
+ "against_type": "Customer",
"against": self.customer,
+ "against_link": self.customer,
"credit": flt(base_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (
flt(base_amount, item.precision("base_net_amount"))
@@ -1418,9 +1428,9 @@ class SalesInvoice(SellingController):
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
- "against": "Expense account - "
- + cstr(self.loyalty_redemption_account)
- + " for the Loyalty Program",
+ "against_type": "Account",
+ "against": self.loyalty_redemption_account,
+ "against_link": self.loyalty_redemption_account,
"credit": self.loyalty_amount,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype,
@@ -1434,7 +1444,9 @@ class SalesInvoice(SellingController):
{
"account": self.loyalty_redemption_account,
"cost_center": self.cost_center or self.loyalty_redemption_cost_center,
+ "against_type": "Customer",
"against": self.customer,
+ "against_link": self.customer,
"debit": self.loyalty_amount,
"remark": "Loyalty Points redeemed by the customer",
},
@@ -1461,7 +1473,9 @@ class SalesInvoice(SellingController):
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
+ "against_type": "Account",
"against": payment_mode.account,
+ "against_link": payment_mode.account,
"credit": payment_mode.base_amount,
"credit_in_account_currency": payment_mode.base_amount
if self.party_account_currency == self.company_currency
@@ -1482,7 +1496,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict(
{
"account": payment_mode.account,
+ "against_type": "Customer",
"against": self.customer,
+ "against_link": self.customer,
"debit": payment_mode.base_amount,
"debit_in_account_currency": payment_mode.base_amount
if payment_mode_account_currency == self.company_currency
@@ -1506,7 +1522,9 @@ class SalesInvoice(SellingController):
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
+ "against_type": "Account",
"against": self.account_for_change_amount,
+ "against_link": self.account_for_change_amount,
"debit": flt(self.base_change_amount),
"debit_in_account_currency": flt(self.base_change_amount)
if self.party_account_currency == self.company_currency
@@ -1527,7 +1545,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict(
{
"account": self.account_for_change_amount,
+ "against_type": "Customer",
"against": self.customer,
+ "against_link": self.customer,
"credit": self.base_change_amount,
"cost_center": self.cost_center,
},
@@ -1553,7 +1573,9 @@ class SalesInvoice(SellingController):
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
+ "against_type": "Account",
"against": self.write_off_account,
+ "against_link": self.write_off_account,
"credit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")),
"credit_in_account_currency": (
flt(self.base_write_off_amount, self.precision("base_write_off_amount"))
@@ -1573,7 +1595,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict(
{
"account": self.write_off_account,
+ "against_type": "Customer",
"against": self.customer,
+ "against_link": self.customer,
"debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")),
"debit_in_account_currency": (
flt(self.base_write_off_amount, self.precision("base_write_off_amount"))
@@ -1601,7 +1625,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict(
{
"account": round_off_account,
+ "against_type": "Customer",
"against": self.customer,
+ "against_link": self.customer,
"credit_in_account_currency": flt(
self.rounding_adjustment, self.precision("rounding_adjustment")
),
@@ -2356,9 +2382,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def get_received_items(reference_name, doctype, reference_fieldname):
+ reference_field = "inter_company_invoice_reference"
+ if doctype == "Purchase Order":
+ reference_field = "inter_company_order_reference"
+
+ filters = {
+ reference_field: reference_name,
+ "docstatus": 1,
+ }
+
target_doctypes = frappe.get_all(
doctype,
- filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
+ filters=filters,
as_list=True,
)
@@ -2540,10 +2575,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)
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 030a41e5cd..91ba239f33 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -280,6 +280,7 @@ def check_if_in_list(gle, gl_map, dimensions=None):
"project",
"finance_book",
"voucher_no",
+ "against_link",
]
if dimensions:
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 50d5eae407..9feda11d3f 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -225,7 +225,7 @@ class ReceivablePayableReport(object):
if not row:
return
- if self.filters.get("in_party_currency"):
+ if self.filters.get("in_party_currency") or self.filters.get("party_account"):
amount = ple.amount_in_account_currency
else:
amount = ple.amount
@@ -244,8 +244,12 @@ class ReceivablePayableReport(object):
row.invoiced_in_account_currency += amount_in_account_currency
else:
if self.is_invoice(ple):
- row.credit_note -= amount
- row.credit_note_in_account_currency -= amount_in_account_currency
+ if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
+ row.paid -= amount
+ row.paid_in_account_currency -= amount_in_account_currency
+ else:
+ row.credit_note -= amount
+ row.credit_note_in_account_currency -= amount_in_account_currency
else:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
@@ -451,7 +455,7 @@ class ReceivablePayableReport(object):
party_details = self.get_party_details(row.party) or {}
row.update(party_details)
- if self.filters.get("in_party_currency"):
+ if self.filters.get("in_party_currency") or self.filters.get("party_account"):
row.currency = row.account_currency
else:
row.currency = self.company_currency
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index dd0842df04..976935b99f 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
return credit_note
+ def test_pos_receivable(self):
+ filters = {
+ "company": self.company,
+ "party_type": "Customer",
+ "party": [self.customer],
+ "report_date": add_days(today(), 2),
+ "based_on_payment_terms": 0,
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "show_remarks": False,
+ }
+
+ pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
+ pos_inv.posting_date = add_days(today(), 2)
+ pos_inv.is_pos = 1
+ pos_inv.append(
+ "payments",
+ frappe._dict(
+ mode_of_payment="Cash",
+ amount=flt(pos_inv.grand_total / 2),
+ ),
+ )
+ pos_inv.disable_rounded_total = 1
+ pos_inv.save()
+ pos_inv.submit()
+
+ report = execute(filters)
+ expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]
+
+ row = report[1][-1]
+ self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
+ pos_inv.cancel()
+
def test_accounts_receivable(self):
filters = {
"company": self.company,
@@ -544,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]
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index ac06a1227e..4054dca360 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -200,10 +200,10 @@ def get_gl_entries(filters, accounting_dimensions):
"""
select
name as gl_entry, posting_date, account, party_type, party,
- voucher_type, voucher_no, {dimension_fields}
+ voucher_type, voucher_subtype, voucher_no, {dimension_fields}
cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
- against, is_opening, creation {select_fields}
+ against_link, against, is_opening, creation {select_fields}
from `tabGL Entry`
where company=%(company)s {conditions}
{order_by_statement}
@@ -238,6 +238,9 @@ def get_conditions(filters):
if filters.get("voucher_no"):
conditions.append("voucher_no=%(voucher_no)s")
+ if filters.get("voucher_no_not_in"):
+ conditions.append("voucher_no not in %(voucher_no_not_in)s")
+
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
conditions.append("party_type in ('Customer', 'Supplier')")
@@ -392,6 +395,7 @@ def initialize_gle_map(gl_entries, filters):
group_by = group_by_field(filters.get("group_by"))
for gle in gl_entries:
+ gle.against = gle.get("against_link") or gle.get("against")
gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[]))
return gle_map
@@ -605,6 +609,12 @@ def get_columns(filters):
columns += [
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
+ {
+ "label": _("Voucher Subtype"),
+ "fieldname": "voucher_subtype",
+ "fieldtype": "Data",
+ "width": 180,
+ },
{
"label": _("Voucher No"),
"fieldname": "voucher_no",
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 0f21c864b0..adec0ab4e0 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -642,6 +642,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
new_row.set("reference_name", d["against_voucher"])
new_row.against_account = cstr(jv_detail.against_account)
+ new_row.against_account_link = cstr(jv_detail.against_account)
new_row.is_advance = cstr(jv_detail.is_advance)
new_row.docstatus = 1
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 540a4f5549..ac712d4431 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -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",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 707ce199c3..dd34189391 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -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):
@@ -689,7 +692,9 @@ class Asset(AccountsController):
self.get_gl_dict(
{
"account": cwip_account,
+ "against_type": "Account",
"against": fixed_asset_account,
+ "against_link": fixed_asset_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date,
"credit": self.purchase_receipt_amount,
@@ -704,7 +709,9 @@ class Asset(AccountsController):
self.get_gl_dict(
{
"account": fixed_asset_account,
+ "against_type": "Account",
"against": cwip_account,
+ "against_link": cwip_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date,
"debit": self.purchase_receipt_amount,
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index dc80aa5c5c..0773698420 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -251,7 +251,16 @@ class TestAsset(AssetSetup):
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
- ("_Test Fixed Asset - _TC", 0.0, 100000.0),
+ (
+ "_Test Fixed Asset - _TC",
+ 0.0,
+ flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
+ ),
+ (
+ "_Test Fixed Asset - _TC",
+ 0.0,
+ flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
+ ),
(
"_Test Gain/Loss on Asset Disposal - _TC",
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 66997ca59c..de758419e0 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -485,7 +485,9 @@ class AssetCapitalization(StockController):
self.get_gl_dict(
{
"account": account,
+ "against_type": "Account",
"against": target_account,
+ "against_link": target_account,
"cost_center": item_row.cost_center,
"project": item_row.get("project") or self.get("project"),
"remarks": self.get("remarks") or "Accounting Entry for Stock",
@@ -526,7 +528,9 @@ class AssetCapitalization(StockController):
self.set_consumed_asset_status(asset)
for gle in fixed_asset_gl_entries:
+ gle["against_type"] = "Account"
gle["against"] = target_account
+ gle["against_link"] = target_account
gl_entries.append(self.get_gl_dict(gle, item=item))
target_against.add(gle["account"])
@@ -542,7 +546,9 @@ class AssetCapitalization(StockController):
self.get_gl_dict(
{
"account": item_row.expense_account,
+ "against_type": "Account",
"against": target_account,
+ "against_link": target_account,
"cost_center": item_row.cost_center,
"project": item_row.get("project") or self.get("project"),
"remarks": self.get("remarks") or "Accounting Entry for Stock",
@@ -553,41 +559,46 @@ class AssetCapitalization(StockController):
)
def get_gl_entries_for_target_item(self, gl_entries, target_against, precision):
- if self.target_is_fixed_asset:
- # Capitalization
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": self.target_fixed_asset_account,
- "against": ", ".join(target_against),
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "debit": flt(self.total_value, precision),
- "cost_center": self.get("cost_center"),
- },
- item=self,
- )
- )
- else:
- # Target Stock Item
- sle_list = self.sle_map.get(self.name)
- for sle in sle_list:
- stock_value_difference = flt(sle.stock_value_difference, precision)
- account = self.warehouse_account[sle.warehouse]["account"]
-
+ for target_account in target_against:
+ if self.target_is_fixed_asset:
+ # Capitalization
gl_entries.append(
self.get_gl_dict(
{
- "account": account,
- "against": ", ".join(target_against),
- "cost_center": self.cost_center,
- "project": self.get("project"),
- "remarks": self.get("remarks") or "Accounting Entry for Stock",
- "debit": stock_value_difference,
+ "account": self.target_fixed_asset_account,
+ "against_type": "Account",
+ "against": target_account,
+ "against_link": target_account,
+ "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
+ "debit": flt(self.total_value, precision) / len(target_against),
+ "cost_center": self.get("cost_center"),
},
- self.warehouse_account[sle.warehouse]["account_currency"],
item=self,
)
)
+ else:
+ # Target Stock Item
+ sle_list = self.sle_map.get(self.name)
+ for sle in sle_list:
+ stock_value_difference = flt(sle.stock_value_difference, precision)
+ account = self.warehouse_account[sle.warehouse]["account"]
+
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": account,
+ "against_type": "Account",
+ "against": target_account,
+ "against_link": target_account,
+ "cost_center": self.cost_center,
+ "project": self.get("project"),
+ "remarks": self.get("remarks") or "Accounting Entry for Stock",
+ "debit": stock_value_difference / len(target_against),
+ },
+ self.warehouse_account[sle.warehouse]["account_currency"],
+ item=self,
+ )
+ )
def create_target_asset(self):
if (
diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
index ac7c90d9e6..7a7a10de20 100644
--- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
@@ -98,12 +98,12 @@ class TestAssetCapitalization(unittest.TestCase):
# Test General Ledger Entries
expected_gle = {
- "_Test Fixed Asset - TCP1": 3000,
+ "_Test Fixed Asset - TCP1": 2999.99,
"Expenses Included In Asset Valuation - TCP1": -1000,
"_Test Warehouse - TCP1": -2000,
+ "Round Off - TCP1": 0.01,
}
actual_gle = get_actual_gle_dict(asset_capitalization.name)
-
self.assertEqual(actual_gle, expected_gle)
# Test Stock Ledger Entries
@@ -189,9 +189,10 @@ class TestAssetCapitalization(unittest.TestCase):
# Test General Ledger Entries
default_expense_account = frappe.db.get_value("Company", company, "default_expense_account")
expected_gle = {
- "_Test Fixed Asset - _TC": 3000,
+ "_Test Fixed Asset - _TC": 2999.99,
"Expenses Included In Asset Valuation - _TC": -1000,
default_expense_account: -2000,
+ "Round Off - _TC": 0.01,
}
actual_gle = get_actual_gle_dict(asset_capitalization.name)
@@ -376,9 +377,10 @@ class TestAssetCapitalization(unittest.TestCase):
# Test General Ledger Entries
expected_gle = {
- "_Test Warehouse - TCP1": consumed_asset_value_before_disposal,
"_Test Accumulated Depreciations - TCP1": accumulated_depreciation,
"_Test Fixed Asset - TCP1": -consumed_asset_purchase_value,
+ "_Test Warehouse - TCP1": consumed_asset_value_before_disposal - 0.01,
+ "Round Off - TCP1": 0.01,
}
actual_gle = get_actual_gle_dict(asset_capitalization.name)
self.assertEqual(actual_gle, expected_gle)
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index c0fb3c2923..bb627d408c 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -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"):
@@ -277,7 +285,9 @@ class AssetRepair(AccountsController):
"account": fixed_asset_account,
"debit": self.repair_cost,
"debit_in_account_currency": self.repair_cost,
+ "against_type": "Account",
"against": pi_expense_account,
+ "against_link": pi_expense_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
@@ -296,7 +306,9 @@ class AssetRepair(AccountsController):
"account": pi_expense_account,
"credit": self.repair_cost,
"credit_in_account_currency": self.repair_cost,
+ "against_type": "Account",
"against": fixed_asset_account,
+ "against_link": fixed_asset_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
@@ -330,7 +342,9 @@ class AssetRepair(AccountsController):
"account": item.expense_account or default_expense_account,
"credit": item.amount,
"credit_in_account_currency": item.amount,
+ "against_type": "Account",
"against": fixed_asset_account,
+ "against_link": fixed_asset_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
@@ -347,7 +361,9 @@ class AssetRepair(AccountsController):
"account": fixed_asset_account,
"debit": item.amount,
"debit_in_account_currency": item.amount,
+ "against_type": "Account",
"against": item.expense_account or default_expense_account,
+ "against_link": item.expense_account or default_expense_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 88faeee982..3b671bb239 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -214,7 +214,7 @@ frappe.ui.form.on("Purchase Order Item", {
}
},
- fg_item_qty: async function(frm, cdt, cdn) {
+ qty: async function (frm, cdt, cdn) {
if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
var row = locals[cdt][cdn];
@@ -222,7 +222,7 @@ frappe.ui.form.on("Purchase Order Item", {
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item)
if (result.message && row.item_code == result.message.service_item && row.uom == result.message.service_item_uom) {
- frappe.model.set_value(cdt, cdn, "qty", flt(row.fg_item_qty) * flt(result.message.conversion_factor));
+ frappe.model.set_value(cdt, cdn, "fg_item_qty", flt(row.qty) / flt(result.message.conversion_factor));
}
}
}
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 98c1b388c1..5a24cc2e92 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -123,8 +123,7 @@
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
- "reqd": 1,
- "search_index": 1
+ "reqd": 1
},
{
"fieldname": "supplier_part_no",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 05c6a359c6..febad18058 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -874,6 +874,7 @@ class AccountsController(TransactionBase):
"project": self.get("project"),
"post_net_value": args.get("post_net_value"),
"voucher_detail_no": args.get("voucher_detail_no"),
+ "voucher_subtype": self.get_voucher_subtype(),
}
)
@@ -929,6 +930,25 @@ class AccountsController(TransactionBase):
return gl_dict
+ def get_voucher_subtype(self):
+ voucher_subtypes = {
+ "Journal Entry": "voucher_type",
+ "Payment Entry": "payment_type",
+ "Stock Entry": "stock_entry_type",
+ "Asset Capitalization": "entry_type",
+ }
+ if self.doctype in voucher_subtypes:
+ return self.get(voucher_subtypes[self.doctype])
+ elif self.doctype == "Purchase Receipt" and self.is_return:
+ return "Purchase Return"
+ elif self.doctype == "Delivery Note" and self.is_return:
+ return "Sales Return"
+ elif (self.doctype == "Sales Invoice" and self.is_return) or self.doctype == "Purchase Invoice":
+ return "Credit Note"
+ elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice":
+ return "Debit Note"
+ return self.doctype
+
def get_value_in_transaction_currency(self, account_currency, args, field):
if account_currency == self.get("currency"):
return args.get(field + "_in_account_currency")
@@ -1120,6 +1140,7 @@ class AccountsController(TransactionBase):
)
credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
+ against_type = "Supplier" if self.doctype == "Purchase Invoice" else "Customer"
against = self.supplier if self.doctype == "Purchase Invoice" else self.customer
if precision_loss:
@@ -1127,7 +1148,9 @@ class AccountsController(TransactionBase):
self.get_gl_dict(
{
"account": round_off_account,
+ "against_type": against_type,
"against": against,
+ "against_link": against,
credit_or_debit: precision_loss,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
@@ -1476,11 +1499,13 @@ class AccountsController(TransactionBase):
if self.doctype == "Purchase Invoice":
dr_or_cr = "credit"
rev_dr_cr = "debit"
+ against_type = "Supplier"
supplier_or_customer = self.supplier
else:
dr_or_cr = "debit"
rev_dr_cr = "credit"
+ against_type = "Customer"
supplier_or_customer = self.customer
if enable_discount_accounting:
@@ -1505,7 +1530,9 @@ class AccountsController(TransactionBase):
self.get_gl_dict(
{
"account": item.discount_account,
+ "against_type": against_type,
"against": supplier_or_customer,
+ "against_link": supplier_or_customer,
dr_or_cr: flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
),
@@ -1523,7 +1550,9 @@ class AccountsController(TransactionBase):
self.get_gl_dict(
{
"account": income_or_expense_account,
+ "against_type": against_type,
"against": supplier_or_customer,
+ "against_link": supplier_or_customer,
rev_dr_cr: flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
),
@@ -1546,7 +1575,9 @@ class AccountsController(TransactionBase):
self.get_gl_dict(
{
"account": self.additional_discount_account,
+ "against_type": against_type,
"against": supplier_or_customer,
+ "against_link": supplier_or_customer,
dr_or_cr: self.base_discount_amount,
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
},
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 3d863e9b87..572fa519e1 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -381,7 +381,11 @@ class BuyingController(SubcontractingController):
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else:
- field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
+ field = (
+ "incoming_rate"
+ if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
+ else "rate"
+ )
rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
* (d.conversion_factor or 1),
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index c8785a5a72..ea7fb23cb6 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -56,10 +56,24 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part
copy_attributes_to_variant(template, variant)
- variant.manufacturer = manufacturer
- variant.manufacturer_part_no = manufacturer_part_no
-
variant.item_code = append_number_if_name_exists("Item", template.name)
+ variant.flags.ignore_mandatory = True
+ variant.save()
+
+ if not frappe.db.exists(
+ "Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer}
+ ):
+ manufacturer_doc = frappe.new_doc("Item Manufacturer")
+ manufacturer_doc.update(
+ {
+ "item_code": variant.name,
+ "manufacturer": manufacturer,
+ "manufacturer_part_no": manufacturer_part_no,
+ }
+ )
+
+ manufacturer_doc.flags.ignore_mandatory = True
+ manufacturer_doc.save(ignore_permissions=True)
return variant
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 63dca630c7..2650753275 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -896,3 +896,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)
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 81e71e3aa2..81080f0266 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -8,6 +8,8 @@ from frappe.model.meta import get_field_precision
from frappe.utils import flt, format_datetime, get_datetime
import erpnext
+from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
+from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
from erpnext.stock.utils import get_incoming_rate
@@ -69,8 +71,6 @@ def validate_return_against(doc):
def validate_returned_items(doc):
- from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
-
valid_items = frappe._dict()
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor"
@@ -123,26 +123,6 @@ def validate_returned_items(doc):
)
)
- elif ref.batch_no and d.batch_no not in ref.batch_no:
- frappe.throw(
- _("Row # {0}: Batch No must be same as {1} {2}").format(
- d.idx, doc.doctype, doc.return_against
- )
- )
-
- elif ref.serial_no:
- if d.qty and not d.serial_no:
- frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
- else:
- serial_nos = get_serial_nos(d.serial_no)
- for s in serial_nos:
- if s not in ref.serial_no:
- frappe.throw(
- _("Row # {0}: Serial No {1} does not match with {2} {3}").format(
- d.idx, s, doc.doctype, doc.return_against
- )
- )
-
if (
warehouse_mandatory
and not d.get("warehouse")
@@ -397,71 +377,92 @@ def make_return_doc(
else:
doc.run_method("calculate_taxes_and_totals")
- def update_item(source_doc, target_doc, source_parent):
+ def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
- target_doc.qty = -1 * source_doc.qty
- item_details = frappe.get_cached_value(
- "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
- )
-
returned_serial_nos = []
- if source_doc.get("serial_and_batch_bundle"):
- if item_details.has_serial_no:
- returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
+ returned_batches = frappe._dict()
+ serial_and_batch_field = (
+ "serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle"
+ )
+ old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no"
+ old_batch_no_field = "batch_no"
- type_of_transaction = "Inward"
- if (
- frappe.db.get_value(
- "Serial and Batch Bundle", source_doc.serial_and_batch_bundle, "type_of_transaction"
- )
- == "Inward"
- ):
- type_of_transaction = "Outward"
-
- cls_obj = SerialBatchCreation(
- {
- "type_of_transaction": type_of_transaction,
- "serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
- "returned_against": source_doc.name,
- "item_code": source_doc.item_code,
- "returned_serial_nos": returned_serial_nos,
- }
- )
-
- cls_obj.duplicate_package()
- if cls_obj.serial_and_batch_bundle:
- target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
-
- if source_doc.get("rejected_serial_and_batch_bundle"):
+ if (
+ source_doc.get(serial_and_batch_field)
+ or source_doc.get(old_serial_no_field)
+ or source_doc.get(old_batch_no_field)
+ ):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos(
- source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle"
+ source_doc, source_parent, serial_no_field=serial_and_batch_field
+ )
+ else:
+ returned_batches = get_returned_batches(
+ source_doc, source_parent, batch_no_field=serial_and_batch_field
)
type_of_transaction = "Inward"
- if (
+ if source_doc.get(serial_and_batch_field) and (
frappe.db.get_value(
- "Serial and Batch Bundle", source_doc.rejected_serial_and_batch_bundle, "type_of_transaction"
+ "Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction"
)
== "Inward"
):
type_of_transaction = "Outward"
+ elif source_parent.doctype in [
+ "Purchase Invoice",
+ "Purchase Receipt",
+ "Subcontracting Receipt",
+ ]:
+ type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
- "serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
+ "serial_and_batch_bundle": source_doc.get(serial_and_batch_field),
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
+ "voucher_type": source_parent.doctype,
+ "do_not_submit": True,
+ "warehouse": source_doc.warehouse,
+ "has_serial_no": item_details.has_serial_no,
+ "has_batch_no": item_details.has_batch_no,
}
)
- cls_obj.duplicate_package()
- if cls_obj.serial_and_batch_bundle:
- target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
+ serial_nos = []
+ batches = frappe._dict()
+ if source_doc.get(old_batch_no_field):
+ batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)})
+ elif source_doc.get(old_serial_no_field):
+ serial_nos = get_serial_nos(source_doc.get(old_serial_no_field))
+ elif source_doc.get(serial_and_batch_field):
+ if item_details.has_serial_no:
+ serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field))
+ else:
+ batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field))
+ if serial_nos:
+ cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos)))
+ elif batches:
+ for batch in batches:
+ if batch in returned_batches:
+ batches[batch] -= flt(returned_batches.get(batch))
+
+ cls_obj.batches = batches
+
+ if source_doc.get(serial_and_batch_field):
+ cls_obj.duplicate_package()
+ if cls_obj.serial_and_batch_bundle:
+ target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle)
+ else:
+ target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name)
+
+ def update_item(source_doc, target_doc, source_parent):
+ target_doc.qty = -1 * source_doc.qty
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
@@ -561,6 +562,17 @@ def make_return_doc(
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
+ item_details = frappe.get_cached_value(
+ "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
+ )
+
+ if not item_details.has_batch_no and not item_details.has_serial_no:
+ return
+
+ for qty_field in ["stock_qty", "rejected_qty"]:
+ if target_doc.get(qty_field):
+ update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
+
def update_terms(source_doc, target_doc, source_parent):
target_doc.payment_amount = -source_doc.payment_amount
@@ -716,6 +728,9 @@ def get_returned_serial_nos(
[parent_doc.doctype, "docstatus", "=", 1],
]
+ if serial_no_field == "rejected_serial_and_batch_bundle":
+ filters.append([child_doc.doctype, "rejected_qty", ">", 0])
+
# Required for POS Invoice
if ignore_voucher_detail_no:
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
@@ -723,9 +738,57 @@ def get_returned_serial_nos(
ids = []
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
ids.append(row.get("serial_and_batch_bundle"))
- if row.get(old_field):
+ if row.get(old_field) and not row.get(serial_no_field):
serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field)))
- serial_nos.extend(get_serial_nos(ids))
+ if ids:
+ serial_nos.extend(get_serial_nos(ids))
return serial_nos
+
+
+def get_returned_batches(
+ child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None
+):
+ from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
+
+ batches = frappe._dict()
+
+ old_field = "batch_no"
+ if not batch_no_field:
+ batch_no_field = "serial_and_batch_bundle"
+
+ return_ref_field = frappe.scrub(child_doc.doctype)
+ if child_doc.doctype == "Delivery Note Item":
+ return_ref_field = "dn_detail"
+
+ fields = [
+ f"`{'tab' + child_doc.doctype}`.`{batch_no_field}`",
+ f"`{'tab' + child_doc.doctype}`.`batch_no`",
+ f"`{'tab' + child_doc.doctype}`.`stock_qty`",
+ ]
+
+ filters = [
+ [parent_doc.doctype, "return_against", "=", parent_doc.name],
+ [parent_doc.doctype, "is_return", "=", 1],
+ [child_doc.doctype, return_ref_field, "=", child_doc.name],
+ [parent_doc.doctype, "docstatus", "=", 1],
+ ]
+
+ if batch_no_field == "rejected_serial_and_batch_bundle":
+ filters.append([child_doc.doctype, "rejected_qty", ">", 0])
+
+ # Required for POS Invoice
+ if ignore_voucher_detail_no:
+ filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
+
+ ids = []
+ for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
+ ids.append(row.get("serial_and_batch_bundle"))
+ if row.get(old_field) and not row.get(batch_no_field):
+ batches.setdefault(row.get(old_field), row.get("stock_qty"))
+
+ if ids:
+ batches.update(get_batches_from_bundle(ids))
+
+ return batches
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index fdadb30e93..919e459c9e 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.item.item import set_item_default
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
-from erpnext.stock.utils import get_incoming_rate
+from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class SellingController(StockController):
@@ -308,6 +308,8 @@ class SellingController(StockController):
"warehouse": p.warehouse or d.warehouse,
"item_code": p.item_code,
"qty": flt(p.qty),
+ "serial_no": p.serial_no if self.docstatus == 2 else None,
+ "batch_no": p.batch_no if self.docstatus == 2 else None,
"uom": p.uom,
"serial_and_batch_bundle": p.serial_and_batch_bundle
or get_serial_and_batch_bundle(p, self),
@@ -330,6 +332,8 @@ class SellingController(StockController):
"warehouse": d.warehouse,
"item_code": d.item_code,
"qty": d.stock_qty,
+ "serial_no": d.serial_no if self.docstatus == 2 else None,
+ "batch_no": d.batch_no if self.docstatus == 2 else None,
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
@@ -428,11 +432,15 @@ class SellingController(StockController):
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
- if not self.get("return_against"):
+ if not self.get("return_against") or (
+ get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
+ ):
# 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 (self.get("is_return") and 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,
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2fda9ccd6e..671d2fb7a5 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -162,7 +162,9 @@ class StockController(AccountsController):
self.get_gl_dict(
{
"account": warehouse_account[sle.warehouse]["account"],
+ "against_type": "Account",
"against": expense_account,
+ "against_link": expense_account,
"cost_center": item_row.cost_center,
"project": item_row.project or self.get("project"),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
@@ -178,7 +180,9 @@ class StockController(AccountsController):
self.get_gl_dict(
{
"account": expense_account,
+ "against_type": "Account",
"against": warehouse_account[sle.warehouse]["account"],
+ "against_link": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(sle.stock_value_difference, precision),
@@ -210,7 +214,9 @@ class StockController(AccountsController):
self.get_gl_dict(
{
"account": expense_account,
+ "against_type": "Account",
"against": warehouse_asset_account,
+ "against_link": warehouse_asset_account,
"cost_center": item_row.cost_center,
"project": item_row.project or self.get("project"),
"remarks": _("Rounding gain/loss Entry for Stock Transfer"),
@@ -226,7 +232,9 @@ class StockController(AccountsController):
self.get_gl_dict(
{
"account": warehouse_asset_account,
+ "against_type": "Account",
"against": expense_account,
+ "against_link": expense_account,
"cost_center": item_row.cost_center,
"remarks": _("Rounding gain/loss Entry for Stock Transfer"),
"credit": sle_rounding_diff,
@@ -455,6 +463,12 @@ class StockController(AccountsController):
sl_dict.update(args)
self.update_inventory_dimensions(d, sl_dict)
+ if self.docstatus == 2:
+ # To handle denormalized serial no records, will br deprecated in v16
+ for field in ["serial_no", "batch_no"]:
+ if d.get(field):
+ sl_dict[field] = d.get(field)
+
return sl_dict
def update_inventory_dimensions(self, row, sl_dict) -> None:
@@ -826,6 +840,7 @@ class StockController(AccountsController):
credit,
remarks,
against_account,
+ against_type="Account",
debit_in_account_currency=None,
credit_in_account_currency=None,
account_currency=None,
@@ -840,7 +855,9 @@ class StockController(AccountsController):
"cost_center": cost_center,
"debit": debit,
"credit": credit,
+ "against_type": against_type,
"against": against_account,
+ "against_link": against_account,
"remarks": remarks,
}
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index e8d3542835..5083873681 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -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",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index f0381d2cef..d86b6d426e 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -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)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 051b475bcc..2debf9191e 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -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
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json
index cb58af1f29..dfd6612098 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.json
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json
@@ -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",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index dd102b0fae..cd92263543 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -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 {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 49386c4ebc..257b60c486 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -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",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index c201c4f7be..2bfd4be53a 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -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
@@ -583,6 +584,7 @@ class ProductionPlan(Document):
if close:
self.db_set("status", "Closed")
+ self.update_bin_qty()
return
if self.total_produced_qty > 0:
@@ -597,6 +599,9 @@ class ProductionPlan(Document):
if close is not None:
self.db_set("status", self.status)
+ if self.docstatus == 1 and self.status != "Completed":
+ self.update_bin_qty()
+
def update_ordered_status(self):
update_status = False
for d in self.po_items:
@@ -1207,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"]
@@ -1216,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"]:
@@ -1555,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,
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index cc9d9a0311..cb99b8845a 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1458,6 +1458,70 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(row.get("uom"), "Nos")
self.assertEqual(row.get("conversion_factor"), 10.0)
+ def test_unreserve_qty_on_closing_of_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, "stock_uom": "_Test UOM 1"}).name
+ rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
+
+ store_warehouse = create_warehouse("Store Warehouse", company="_Test Company")
+ 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, stock_uom="_Test UOM 1", do_not_submit=1
+ )
+
+ pln.for_warehouse = rm_warehouse
+ mr_items = get_items_for_material_requests(pln.as_dict())
+ for d in mr_items:
+ pln.append("mr_items", d)
+
+ pln.save()
+ pln.submit()
+
+ bin_name = get_or_make_bin(rm_item, rm_warehouse)
+ before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
+
+ pln.reload()
+ pln.set_status(close=True)
+
+ bin_name = get_or_make_bin(rm_item, rm_warehouse)
+ after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
+ self.assertAlmostEqual(after_qty, before_qty - 10)
+
+ pln.reload()
+ pln.set_status(close=False)
+
+ bin_name = get_or_make_bin(rm_item, rm_warehouse)
+ 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):
"""
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 9b070bdaf7..7ade21df60 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -352,5 +352,8 @@ 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")
execute:frappe.db.set_default("date_format", frappe.db.get_single_value("System Settings", "date_format"))
+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 # 2023-12-20
+erpnext.patches.v14_0.set_maintain_stock_for_bom_item
diff --git a/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py b/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py
new file mode 100644
index 0000000000..f0b618f32d
--- /dev/null
+++ b/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py
@@ -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
+ """
+ )
diff --git a/erpnext/patches/v14_0/update_total_asset_cost_field.py b/erpnext/patches/v14_0/update_total_asset_cost_field.py
new file mode 100644
index 0000000000..57cf71b613
--- /dev/null
+++ b/erpnext/patches/v14_0/update_total_asset_cost_field.py
@@ -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()
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 3ed7fc75cf..77ecf75e0c 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -361,9 +361,14 @@ erpnext.buying = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
+ let qty = Math.abs(r.total_qty);
+ if (doc.is_return) {
+ qty = qty * -1;
+ }
+
let update_values = {
"serial_and_batch_bundle": r.name,
- "qty": Math.abs(r.total_qty)
+ "qty": qty
}
if (r.warehouse) {
@@ -396,9 +401,14 @@ erpnext.buying = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
+ let qty = Math.abs(r.total_qty);
+ if (doc.is_return) {
+ qty = qty * -1;
+ }
+
let update_values = {
"serial_and_batch_bundle": r.name,
- "rejected_qty": Math.abs(r.total_qty)
+ "rejected_qty": qty
}
if (r.warehouse) {
diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js
index 5514963c96..b92b02e826 100644
--- a/erpnext/public/js/utils/sales_common.js
+++ b/erpnext/public/js/utils/sales_common.js
@@ -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"));
@@ -317,9 +323,14 @@ erpnext.sales_common = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
+ let qty = Math.abs(r.total_qty);
+ if (doc.is_return) {
+ qty = qty * -1;
+ }
+
frappe.model.set_value(item.doctype, item.name, {
"serial_and_batch_bundle": r.name,
- "qty": Math.abs(r.total_qty)
+ "qty": qty
});
}
}
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 7b9cdfef2a..4abc8fa395 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -32,22 +32,39 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
});
this.dialog.show();
-
- let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty;
- this.dialog.set_value("qty", qty).then(() => {
- if (this.item.serial_no) {
- this.dialog.set_value("scan_serial_no", this.item.serial_no);
- frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
- } else if (this.item.batch_no) {
- this.dialog.set_value("scan_batch_no", this.item.batch_no);
- frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', '');
- }
-
- this.dialog.fields_dict.entries.grid.refresh();
- });
-
this.$scan_btn = this.dialog.$wrapper.find(".link-btn");
this.$scan_btn.css("display", "inline");
+
+ let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty;
+
+ if (this.item?.is_rejected) {
+ qty = this.item.rejected_qty;
+ }
+
+ qty = Math.abs(qty);
+ if (qty > 0) {
+ this.dialog.set_value("qty", qty).then(() => {
+ if (this.item.serial_no && !this.item.serial_and_batch_bundle) {
+ let serial_nos = this.item.serial_no.split('\n');
+ if (serial_nos.length > 1) {
+ serial_nos.forEach(serial_no => {
+ this.dialog.fields_dict.entries.df.data.push({
+ serial_no: serial_no,
+ batch_no: this.item.batch_no
+ });
+ });
+ } else {
+ this.dialog.set_value("scan_serial_no", this.item.serial_no);
+ }
+ frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
+ } else if (this.item.batch_no && !this.item.serial_and_batch_bundle) {
+ this.dialog.set_value("scan_batch_no", this.item.batch_no);
+ frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', '');
+ }
+
+ this.dialog.fields_dict.entries.grid.refresh();
+ });
+ }
}
get_serial_no_filters() {
@@ -467,13 +484,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
render_data() {
- if (!this.frm.is_new() && this.bundle) {
+ if (this.bundle) {
frappe.call({
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers',
args: {
item_code: this.item.item_code,
name: this.bundle,
- voucher_no: this.item.parent,
+ voucher_no: !this.frm.is_new() ? this.item.parent : "",
}
}).then(r => {
if (r.message) {
diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py
index efeaeed324..250a4b1495 100644
--- a/erpnext/regional/united_arab_emirates/utils.py
+++ b/erpnext/regional/united_arab_emirates/utils.py
@@ -153,7 +153,9 @@ def make_gl_entry(tax, gl_entries, doc, tax_accounts):
"account": tax.account_head,
"cost_center": tax.cost_center,
"posting_date": doc.posting_date,
+ "against_type": "Supplier",
"against": doc.supplier,
+ "against_link": doc.supplier,
dr_or_cr: tax.base_tax_amount_after_discount_amount,
dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount
if account_currency == doc.company_currency
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 97b214e33e..b206e3fe33 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -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
});
}
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 193048f676..bd8579203c 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -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;
diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py
index a58f40362b..40aa9acc3c 100644
--- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py
+++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py
@@ -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(
diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py
index 926283ff1c..4bc98b91bd 100644
--- a/erpnext/setup/demo.py
+++ b/erpnext/setup/demo.py
@@ -112,9 +112,9 @@ def create_transaction(doctype, company, start_date):
warehouse = get_warehouse(company)
if document_type == "Purchase Order":
- posting_date = get_random_date(start_date, 1, 30)
+ posting_date = get_random_date(start_date, 1, 25)
else:
- posting_date = get_random_date(start_date, 31, 364)
+ posting_date = get_random_date(start_date, 31, 350)
doctype.update(
{
diff --git a/erpnext/setup/demo_data/journal_entry.json b/erpnext/setup/demo_data/journal_entry.json
index b751c7cf24..a681be4f5b 100644
--- a/erpnext/setup/demo_data/journal_entry.json
+++ b/erpnext/setup/demo_data/journal_entry.json
@@ -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"
}
-]
\ No newline at end of file
+]
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 0e8ee2dda4..9897847896 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -274,7 +274,7 @@ class Company(NestedSet):
"parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr)
if not wh_detail["is_group"]
else "",
- "warehouse_type": wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None,
+ "warehouse_type": wh_detail.get("warehouse_type"),
}
)
warehouse.flags.ignore_permissions = True
diff --git a/erpnext/setup/doctype/customer_group/customer_group.js b/erpnext/setup/doctype/customer_group/customer_group.js
index 3c81b0283c..e3528189dc 100644
--- a/erpnext/setup/doctype/customer_group/customer_group.js
+++ b/erpnext/setup/doctype/customer_group/customer_group.js
@@ -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);
+ }
+ },
});
diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js
index d86a8f3d98..f0d9aa87bc 100644
--- a/erpnext/setup/doctype/sales_person/sales_person.js
+++ b/erpnext/setup/doctype/sales_person/sales_person.js
@@ -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) {
diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.js b/erpnext/setup/doctype/supplier_group/supplier_group.js
index 33629297ff..c697a99cb4 100644
--- a/erpnext/setup/doctype/supplier_group/supplier_group.js
+++ b/erpnext/setup/doctype/supplier_group/supplier_group.js
@@ -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);
+ }
}
});
diff --git a/erpnext/setup/doctype/territory/territory.js b/erpnext/setup/doctype/territory/territory.js
index 3caf814c90..e11d20b7bf 100644
--- a/erpnext/setup/doctype/territory/territory.js
+++ b/erpnext/setup/doctype/territory/territory.js
@@ -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{
diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json
index 312470d50e..10d9511357 100644
--- a/erpnext/stock/doctype/bin/bin.json
+++ b/erpnext/stock/doctype/bin/bin.json
@@ -52,8 +52,7 @@
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1,
- "reqd": 1,
- "search_index": 1
+ "reqd": 1
},
{
"default": "0.00",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index b85f296d0b..7873d3e6de 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -301,7 +301,8 @@
"no_copy": 1,
"options": "Delivery Note",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"collapsible": 1,
@@ -1401,7 +1402,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2023-09-04 14:15:28.363184",
+ "modified": "2023-12-18 17:19:39.368239",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index a101bdf244..675f8e9158 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -1294,7 +1294,3 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
)
return doclist
-
-
-def on_doctype_update():
- frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"])
diff --git a/erpnext/stock/doctype/delivery_note/patches/__init__.py b/erpnext/stock/doctype/delivery_note/patches/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py
new file mode 100644
index 0000000000..cc29e67fa7
--- /dev/null
+++ b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py
@@ -0,0 +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():
+ 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(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
+ click.echo(f"✓ dropped {index} index from {table}")
+ except Exception:
+ frappe.log_error("Failed to drop index")
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 94655747e4..933be53b07 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -174,6 +174,115 @@ class TestDeliveryNote(FrappeTestCase):
for field, value in field_values.items():
self.assertEqual(cstr(serial_no.get(field)), value)
+ def test_delivery_note_return_against_denormalized_serial_no(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ frappe.flags.ignore_serial_batch_bundle_validation = True
+ sn_item = "Old Serial NO Item Return Test - 1"
+ make_item(
+ sn_item,
+ {
+ "has_serial_no": 1,
+ "serial_no_series": "OSN-.####",
+ "is_stock_item": 1,
+ },
+ )
+
+ frappe.flags.ignore_serial_batch_bundle_validation = True
+ serial_nos = [
+ "OSN-1",
+ "OSN-2",
+ "OSN-3",
+ "OSN-4",
+ "OSN-5",
+ "OSN-6",
+ "OSN-7",
+ "OSN-8",
+ "OSN-9",
+ "OSN-10",
+ "OSN-11",
+ "OSN-12",
+ ]
+
+ for sn in serial_nos:
+ if not frappe.db.exists("Serial No", sn):
+ sn_doc = frappe.get_doc(
+ {
+ "doctype": "Serial No",
+ "item_code": sn_item,
+ "serial_no": sn,
+ }
+ )
+ sn_doc.insert()
+
+ warehouse = "_Test Warehouse - _TC"
+ company = frappe.db.get_value("Warehouse", warehouse, "company")
+ se_doc = make_stock_entry(
+ item_code=sn_item,
+ company=company,
+ target="_Test Warehouse - _TC",
+ qty=12,
+ basic_rate=100,
+ do_not_submit=1,
+ )
+
+ se_doc.items[0].serial_no = "\n".join(serial_nos)
+ se_doc.submit()
+
+ self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
+
+ dn = create_delivery_note(
+ item_code=sn_item,
+ qty=12,
+ rate=500,
+ warehouse=warehouse,
+ company=company,
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ do_not_submit=1,
+ )
+
+ dn.items[0].serial_no = "\n".join(serial_nos)
+ dn.submit()
+ dn.reload()
+
+ self.assertTrue(dn.items[0].serial_no)
+
+ frappe.flags.ignore_serial_batch_bundle_validation = False
+
+ # return entry
+ dn1 = make_sales_return(dn.name)
+
+ dn1.items[0].qty = -2
+
+ bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle)
+ bundle_doc.set("entries", bundle_doc.entries[:2])
+ bundle_doc.save()
+
+ dn1.save()
+ dn1.submit()
+
+ returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
+ for serial_no in returned_serial_nos1:
+ self.assertTrue(serial_no in serial_nos)
+
+ dn2 = make_sales_return(dn.name)
+
+ dn2.items[0].qty = -2
+
+ bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle)
+ bundle_doc.set("entries", bundle_doc.entries[:2])
+ bundle_doc.save()
+
+ dn2.save()
+ dn2.submit()
+
+ returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
+ for serial_no in returned_serial_nos2:
+ self.assertTrue(serial_no in serial_nos)
+ self.assertFalse(serial_no in returned_serial_nos1)
+
def test_sales_return_for_non_bundled_items_partial(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -1266,6 +1375,109 @@ class TestDeliveryNote(FrappeTestCase):
dn.reload()
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
+ def test_sales_return_valuation_for_moving_average(self):
+ item_code = make_item(
+ "_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"}
+ ).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, qty=5, rate=500, posting_date=add_days(nowdate(), -4)
+ )
+ self.assertEqual(dn.items[0].incoming_rate, 100.0)
+
+ make_stock_entry(
+ item_code=item_code,
+ target="_Test Warehouse - _TC",
+ qty=5,
+ basic_rate=200.0,
+ posting_date=add_days(nowdate(), -3),
+ )
+ make_stock_entry(
+ item_code=item_code,
+ target="_Test Warehouse - _TC",
+ qty=5,
+ basic_rate=300.0,
+ posting_date=add_days(nowdate(), -2),
+ )
+
+ dn1 = create_delivery_note(
+ is_return=1,
+ item_code=item_code,
+ return_against=dn.name,
+ qty=-5,
+ rate=500,
+ company=dn.company,
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ do_not_submit=1,
+ posting_date=add_days(nowdate(), -1),
+ )
+
+ # (300 * 5) + (200 * 5) = 2500
+ # 2500 / 10 = 250
+
+ 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")
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index a942f58bd6..b237f73026 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -522,39 +522,25 @@ class TestItem(FrappeTestCase):
self.assertEqual(factor, 1.0)
def test_item_variant_by_manufacturer(self):
- fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}]
- set_item_variant_settings(fields)
+ template = make_item(
+ "_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"}
+ ).name
- if frappe.db.exists("Item", "_Test Variant Mfg"):
- frappe.delete_doc("Item", "_Test Variant Mfg")
- if frappe.db.exists("Item", "_Test Variant Mfg-1"):
- frappe.delete_doc("Item", "_Test Variant Mfg-1")
- if frappe.db.exists("Manufacturer", "MSG1"):
- frappe.delete_doc("Manufacturer", "MSG1")
+ for manufacturer in ["DFSS", "DASA", "ASAAS"]:
+ if not frappe.db.exists("Manufacturer", manufacturer):
+ m_doc = frappe.new_doc("Manufacturer")
+ m_doc.short_name = manufacturer
+ m_doc.insert()
- template = frappe.get_doc(
- dict(
- doctype="Item",
- item_code="_Test Variant Mfg",
- has_variant=1,
- item_group="Products",
- variant_based_on="Manufacturer",
- )
- ).insert()
+ self.assertFalse(frappe.db.exists("Item Manufacturer", {"manufacturer": "DFSS"}))
+ variant = get_variant(template, manufacturer="DFSS", manufacturer_part_no="DFSS-123")
- manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert()
+ item_manufacturer = frappe.db.exists(
+ "Item Manufacturer", {"manufacturer": "DFSS", "item_code": variant.name}
+ )
+ self.assertTrue(item_manufacturer)
- variant = get_variant(template.name, manufacturer=manufacturer.name)
- self.assertEqual(variant.item_code, "_Test Variant Mfg-1")
- self.assertEqual(variant.description, "_Test Variant Mfg")
- self.assertEqual(variant.manufacturer, "MSG1")
- variant.insert()
-
- variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007")
- self.assertEqual(variant.item_code, "_Test Variant Mfg-2")
- self.assertEqual(variant.description, "_Test Variant Mfg")
- self.assertEqual(variant.manufacturer, "MSG1")
- self.assertEqual(variant.manufacturer_part_no, "007")
+ frappe.delete_doc("Item Manufacturer", item_manufacturer)
def test_stock_exists_against_template_item(self):
stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 3e90ed5a3b..ad9b34c9ac 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -169,7 +169,9 @@ class MaterialRequest(BuyingController):
def on_submit(self):
self.update_requested_qty_in_production_plan()
self.update_requested_qty()
- if self.material_request_type == "Purchase":
+ if self.material_request_type == "Purchase" and frappe.db.exists(
+ "Budget", {"applicable_on_material_request": 1, "docstatus": 1}
+ ):
self.validate_budget()
def before_save(self):
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 6c9d3392e3..2cbccb0774 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -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);
},
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index c7ad660497..a181022121 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -289,7 +289,8 @@
"no_copy": 1,
"options": "Purchase Receipt",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "section_addresses",
@@ -1251,7 +1252,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2023-11-28 13:14:15.243474",
+ "modified": "2023-12-18 17:26:41.279663",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index ab0727163e..10d9eaa3db 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -518,6 +518,7 @@ class PurchaseReceipt(BuyingController):
debit=0.0,
credit=discrepancy_caused_by_exchange_rate_difference,
remarks=remarks,
+ against_type="Supplier",
against_account=self.supplier,
debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=account_currency,
@@ -531,6 +532,7 @@ class PurchaseReceipt(BuyingController):
debit=discrepancy_caused_by_exchange_rate_difference,
credit=0.0,
remarks=remarks,
+ against_type="Supplier",
against_account=self.supplier,
debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=account_currency,
@@ -796,7 +798,7 @@ class PurchaseReceipt(BuyingController):
# Backward compatibility:
# and charges added via Landed Cost Voucher,
# post valuation related charges on "Stock Received But Not Billed"
- against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
+ against_accounts = [d.account for d in gl_entries if flt(d.debit) > 0]
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked
stock_rbnb = (
@@ -828,16 +830,17 @@ class PurchaseReceipt(BuyingController):
)
amount_including_divisional_loss -= applicable_amount
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=tax.cost_center,
- debit=0.0,
- credit=applicable_amount,
- remarks=self.remarks or _("Accounting Entry for Stock"),
- against_account=against_account,
- item=tax,
- )
+ for against in against_accounts:
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=tax.cost_center,
+ debit=0.0,
+ credit=flt(applicable_amount) / len(against_accounts),
+ remarks=self.remarks or _("Accounting Entry for Stock"),
+ against_account=against,
+ item=tax,
+ )
i += 1
@@ -1357,10 +1360,6 @@ def get_item_account_wise_additional_cost(purchase_document):
return item_account_wise_cost
-def on_doctype_update():
- frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
-
-
@erpnext.allow_regional
def update_regional_gl_entries(gl_list, doc):
return
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 146cbff1aa..d2740694b3 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -4,6 +4,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, nowtime, today
+from pypika import Order
from pypika import functions as fn
import erpnext
@@ -990,7 +991,7 @@ class TestPurchaseReceipt(FrappeTestCase):
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
sl_entries = get_sl_entries("Purchase Receipt", pr.name)
- self.assertFalse(gl_entries)
+ self.assertEqual(len(gl_entries), 2)
expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5}
@@ -2235,13 +2236,13 @@ def get_sl_entries(voucher_type, voucher_no):
def get_gl_entries(voucher_type, voucher_no):
- return frappe.db.sql(
- """select account, debit, credit, cost_center, is_cancelled
- from `tabGL Entry` where voucher_type=%s and voucher_no=%s
- order by account desc""",
- (voucher_type, voucher_no),
- as_dict=1,
- )
+ gle = frappe.qb.DocType("GL Entry")
+ return (
+ frappe.qb.from_(gle)
+ .select(gle.account, gle.debit, gle.credit, gle.cost_center, gle.is_cancelled)
+ .where((gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no))
+ .orderby(gle.account, gle.debit, order=Order.desc)
+ ).run(as_dict=True)
def get_taxes(**args):
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 7344d2a599..9bd692ad61 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -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",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index ecb9314fbb..afb53fb112 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -23,7 +23,11 @@ from frappe.utils import (
)
from frappe.utils.csvutils import build_csv_response
-from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
+from erpnext.stock.serial_batch_bundle import (
+ BatchNoValuation,
+ SerialNoValuation,
+ get_batches_from_bundle,
+)
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
@@ -81,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()
@@ -95,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
@@ -123,6 +148,11 @@ class SerialandBatchBundle(Document):
)
def validate_serial_nos_duplicate(self):
+ # Don't inward same serial number multiple times
+
+ if not self.warehouse:
+ return
+
if self.voucher_type in ["Stock Reconciliation", "Stock Entry"] and self.docstatus != 1:
return
@@ -146,7 +176,6 @@ class SerialandBatchBundle(Document):
kwargs["voucher_no"] = self.voucher_no
available_serial_nos = get_available_serial_nos(kwargs)
-
for data in available_serial_nos:
if data.serial_no in serial_nos:
self.throw_error_message(
@@ -327,6 +356,19 @@ class SerialandBatchBundle(Document):
):
values_to_set["posting_time"] = parent.posting_time
+ if parent.doctype in [
+ "Delivery Note",
+ "Purchase Receipt",
+ "Purchase Invoice",
+ "Sales Invoice",
+ ] and parent.get("is_return"):
+ return_ref_field = frappe.scrub(parent.doctype) + "_item"
+ if parent.doctype == "Delivery Note":
+ return_ref_field = "dn_detail"
+
+ if row.get(return_ref_field):
+ values_to_set["returned_against"] = row.get(return_ref_field)
+
if values_to_set:
self.db_set(values_to_set)
@@ -438,7 +480,7 @@ class SerialandBatchBundle(Document):
qty_field = "qty"
precision = row.precision
- if self.voucher_type in ["Subcontracting Receipt"]:
+ if row.get("doctype") in ["Subcontracting Receipt Supplied Item"]:
qty_field = "consumed_qty"
if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01:
@@ -509,7 +551,6 @@ class SerialandBatchBundle(Document):
batch_nos = []
serial_batches = {}
-
for row in self.entries:
if self.has_serial_no and not row.serial_no:
frappe.throw(
@@ -590,6 +631,67 @@ class SerialandBatchBundle(Document):
f"Batch Nos {bold(incorrect_batch_nos)} does not belong to Item {bold(self.item_code)}"
)
+ def validate_serial_and_batch_no_for_returned(self):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ if not self.returned_against:
+ return
+
+ if self.voucher_type not in [
+ "Purchase Receipt",
+ "Purchase Invoice",
+ "Sales Invoice",
+ "Delivery Note",
+ ]:
+ return
+
+ data = self.get_orignal_document_data()
+ if not data:
+ return
+
+ serial_nos, batches = [], []
+ current_serial_nos = [d.serial_no for d in self.entries if d.serial_no]
+ current_batches = [d.batch_no for d in self.entries if d.batch_no]
+
+ for d in data:
+ if self.has_serial_no:
+ if d.serial_and_batch_bundle:
+ serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle)
+ else:
+ serial_nos = get_serial_nos(d.serial_no)
+
+ elif self.has_batch_no:
+ if d.serial_and_batch_bundle:
+ batches = get_batches_from_bundle(d.serial_and_batch_bundle)
+ else:
+ batches = frappe._dict({d.batch_no: d.stock_qty})
+
+ if batches:
+ batches = [d for d in batches if batches[d] > 0]
+
+ if serial_nos:
+ if not set(current_serial_nos).issubset(set(serial_nos)):
+ self.throw_error_message(
+ f"Serial Nos {bold(', '.join(serial_nos))} are not part of the original document."
+ )
+
+ if batches:
+ if not set(current_batches).issubset(set(batches)):
+ self.throw_error_message(
+ f"Batch Nos {bold(', '.join(batches))} are not part of the original document."
+ )
+
+ def get_orignal_document_data(self):
+ fields = ["serial_and_batch_bundle", "stock_qty"]
+ if self.has_serial_no:
+ fields.append("serial_no")
+
+ elif self.has_batch_no:
+ fields.append("batch_no")
+
+ child_doc = self.voucher_type + " Item"
+ return frappe.get_all(child_doc, fields=fields, filters={"name": self.returned_against})
+
def validate_duplicate_serial_and_batch_no(self):
serial_nos = []
batch_nos = []
@@ -688,9 +790,29 @@ class SerialandBatchBundle(Document):
for batch in batches:
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
+ def before_submit(self):
+ self.validate_serial_and_batch_no_for_returned()
+ self.set_purchase_document_no()
+
def on_submit(self):
self.validate_serial_nos_inventory()
+ def set_purchase_document_no(self):
+ if not self.has_serial_no:
+ return
+
+ if self.total_qty > 0:
+ serial_nos = [d.serial_no for d in self.entries if d.serial_no]
+ sn_table = frappe.qb.DocType("Serial No")
+ (
+ frappe.qb.update(sn_table)
+ .set(
+ sn_table.purchase_document_no,
+ self.voucher_no if not sn_table.purchase_document_no else self.voucher_no,
+ )
+ .where(sn_table.name.isin(serial_nos))
+ ).run()
+
def validate_serial_and_batch_inventory(self):
self.check_future_entries_exists()
self.validate_batch_inventory()
@@ -1063,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,
@@ -1091,7 +1213,7 @@ def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
doc.append(
"entries",
{
- "qty": (d.get("qty") or 1.0) * (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"),
diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json
index b4ece00fe6..2d7fcac89a 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.json
+++ b/erpnext/stock/doctype/serial_no/serial_no.json
@@ -27,8 +27,6 @@
"column_break_24",
"location",
"employee",
- "delivery_details",
- "delivery_document_type",
"warranty_amc_details",
"column_break6",
"warranty_expiry_date",
@@ -39,7 +37,8 @@
"more_info",
"company",
"column_break_2cmm",
- "work_order"
+ "work_order",
+ "purchase_document_no"
],
"fields": [
{
@@ -153,20 +152,6 @@
"options": "Employee",
"read_only": 1
},
- {
- "fieldname": "delivery_details",
- "fieldtype": "Section Break",
- "label": "Delivery Details",
- "oldfieldtype": "Column Break"
- },
- {
- "fieldname": "delivery_document_type",
- "fieldtype": "Link",
- "label": "Delivery Document Type",
- "no_copy": 1,
- "options": "DocType",
- "read_only": 1
- },
{
"fieldname": "warranty_amc_details",
"fieldtype": "Section Break",
@@ -275,12 +260,19 @@
{
"fieldname": "column_break_2cmm",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "purchase_document_no",
+ "fieldtype": "Data",
+ "label": "Creation Document No",
+ "no_copy": 1,
+ "read_only": 1
}
],
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
- "modified": "2023-11-28 15:37:59.489945",
+ "modified": "2023-12-17 10:52:55.767839",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index d562560da1..122664c2dd 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -41,7 +41,6 @@ class SerialNo(StockController):
batch_no: DF.Link | None
brand: DF.Link | None
company: DF.Link
- delivery_document_type: DF.Link | None
description: DF.Text | None
employee: DF.Link | None
item_code: DF.Link
@@ -51,6 +50,7 @@ class SerialNo(StockController):
maintenance_status: DF.Literal[
"", "Under Warranty", "Out of Warranty", "Under AMC", "Out of AMC"
]
+ purchase_document_no: DF.Data | None
purchase_rate: DF.Float
serial_no: DF.Data
status: DF.Literal["", "Active", "Inactive", "Delivered", "Expired"]
@@ -231,26 +231,6 @@ def auto_fetch_serial_number(
return sorted([d.get("name") for d in serial_numbers])
-def get_delivered_serial_nos(serial_nos):
- """
- Returns serial numbers that delivered from the list of serial numbers
- """
- from frappe.query_builder.functions import Coalesce
-
- SerialNo = frappe.qb.DocType("Serial No")
- serial_nos = get_serial_nos(serial_nos)
- query = (
- frappe.qb.select(SerialNo.name)
- .from_(SerialNo)
- .where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != ""))
- )
-
- result = query.run()
- if result and len(result) > 0:
- delivered_serial_nos = [row[0] for row in result]
- return delivered_serial_nos
-
-
@frappe.whitelist()
def get_pos_reserved_serial_nos(filters):
if isinstance(filters, str):
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 7af5d1aa37..8da3e8fdd0 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -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);
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 84e99c5155..6521394eef 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1463,7 +1463,9 @@ class StockEntry(StockController):
self.get_gl_dict(
{
"account": account,
+ "against_type": "Account",
"against": d.expense_account,
+ "against_link": d.expense_account,
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit_in_account_currency": flt(amount["amount"]),
@@ -1477,7 +1479,9 @@ class StockEntry(StockController):
self.get_gl_dict(
{
"account": d.expense_account,
+ "against_type": "Account",
"against": account,
+ "against_link": account,
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": -1
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index e0e364f397..d4003125c3 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -504,7 +504,14 @@ class TestStockEntry(FrappeTestCase):
self.check_gl_entries(
"Stock Entry",
repack.name,
- sorted([[stock_in_hand_account, 1200, 0.0], ["Cost of Goods Sold - TCP1", 0.0, 1200.0]]),
+ sorted(
+ [
+ ["Cost of Goods Sold - TCP1", 0.0, 1200.0],
+ ["Stock Adjustment - TCP1", 0.0, 1200.0],
+ ["Stock Adjustment - TCP1", 1200.0, 0.0],
+ [stock_in_hand_account, 1200.0, 0.0],
+ ]
+ ),
)
def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle):
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 23788cf46b..6e7af6815f 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -181,6 +181,9 @@ class StockLedgerEntry(Document):
frappe.throw(_("Actual Qty is mandatory"))
def validate_serial_batch_no_bundle(self):
+ if self.is_cancelled == 1:
+ return
+
item_detail = frappe.get_cached_value(
"Item",
self.item_code,
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index e8d652e2b2..6819968394 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -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)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 1ec99bf9a5..70e9fb2205 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -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)
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index 24650fde5f..7e03ac3357 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -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}.
- The Max Reserved Qty is calculated as follows:
+ Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.
+ The Allowed Qty is calculated as follows: