Merge remote-tracking branch 'upstream/develop' into remove-unused-coa

This commit is contained in:
barredterra 2023-12-26 16:06:43 +01:00
commit 5a5758423e
97 changed files with 2139 additions and 625 deletions

View File

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

View File

@ -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,

View File

@ -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"
}
}
}
}

View File

@ -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": {}

View File

@ -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"
}
},

View File

@ -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": []
}
}

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -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"
}
]

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -280,6 +280,7 @@ def check_if_in_list(gle, gl_map, dimensions=None):
"project",
"finance_book",
"voucher_no",
"against_link",
]
if dimensions:

View File

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

View File

@ -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]

View File

@ -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",

View File

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

View File

@ -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",

View File

@ -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,

View File

@ -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")),

View File

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

View File

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

View File

@ -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,

View File

@ -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));
}
}
}

View File

@ -123,8 +123,7 @@
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"fieldname": "supplier_part_no",

View File

@ -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),
},

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

@ -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",

View File

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

View File

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

View File

@ -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",

View File

@ -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 {

View File

@ -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",

View File

@ -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,

View File

@ -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):
"""

View File

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

View File

@ -0,0 +1,19 @@
import frappe
def execute():
if not frappe.db.exists("BOM", {"docstatus": 1}):
return
# Added is_stock_item to handle Read Only based on condition for the rate field
frappe.db.sql(
"""
UPDATE
`tabBOM Item` boi,
`tabItem` i
SET
boi.is_stock_item = i.is_stock_item
WHERE
boi.item_code = i.name
"""
)

View File

@ -0,0 +1,17 @@
import frappe
def execute():
asset = frappe.qb.DocType("Asset")
frappe.qb.update(asset).set(asset.total_asset_cost, asset.gross_purchase_amount).run()
asset_repair_list = frappe.db.get_all(
"Asset Repair",
filters={"docstatus": 1, "repair_status": "Completed", "capitalize_repair_cost": 1},
fields=["asset", "repair_cost"],
)
for asset_repair in asset_repair_list:
frappe.qb.update(asset).set(
asset.total_asset_cost, asset.total_asset_cost + asset_repair.repair_cost
).where(asset.name == asset_repair.asset).run()

View File

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

View File

@ -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
});
}
}

View File

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

View File

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

View File

@ -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
});
}
}

View File

@ -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;

View File

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

View File

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

View File

@ -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"
}
]
]

View File

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

View File

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

View File

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

View File

@ -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);
}
}
});

View File

@ -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{

View File

@ -52,8 +52,7 @@
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1,
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"default": "0.00",

View File

@ -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",

View File

@ -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"])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

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

View File

@ -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",

View File

@ -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"),

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

@ -9,7 +9,7 @@ from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt
from erpnext.stock.utils import get_or_make_bin
from erpnext.stock.utils import get_or_make_bin, get_stock_balance
class StockReservationEntry(Document):
@ -151,7 +151,7 @@ class StockReservationEntry(Document):
"""Validates `Reserved Qty` when `Reservation Based On` is `Qty`."""
if self.reservation_based_on == "Qty":
self.validate_with_max_reserved_qty(self.reserved_qty)
self.validate_with_allowed_qty(self.reserved_qty)
def auto_reserve_serial_and_batch(self, based_on: str = None) -> None:
"""Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`."""
@ -324,7 +324,7 @@ class StockReservationEntry(Document):
frappe.throw(msg)
# Should be called after validating Serial and Batch Nos.
self.validate_with_max_reserved_qty(qty_to_be_reserved)
self.validate_with_allowed_qty(qty_to_be_reserved)
self.db_set("reserved_qty", qty_to_be_reserved)
def update_reserved_qty_in_voucher(
@ -429,7 +429,7 @@ class StockReservationEntry(Document):
msg = _("Stock Reservation Entry cannot be updated as it has been delivered.")
frappe.throw(msg)
def validate_with_max_reserved_qty(self, qty_to_be_reserved: float) -> None:
def validate_with_allowed_qty(self, qty_to_be_reserved: float) -> None:
"""Validates `Reserved Qty` with `Max Reserved Qty`."""
self.db_set(
@ -448,12 +448,12 @@ class StockReservationEntry(Document):
)
voucher_delivered_qty = flt(delivered_qty) * flt(conversion_factor)
max_reserved_qty = min(
allowed_qty = min(
self.available_qty, (self.voucher_qty - voucher_delivered_qty - total_reserved_qty)
)
if max_reserved_qty <= 0 and self.voucher_type == "Sales Order":
msg = _("Item {0} is already delivered for Sales Order {1}.").format(
if self.get("_action") != "submit" and self.voucher_type == "Sales Order" and allowed_qty <= 0:
msg = _("Item {0} is already reserved/delivered against Sales Order {1}.").format(
frappe.bold(self.item_code), frappe.bold(self.voucher_no)
)
@ -463,19 +463,33 @@ class StockReservationEntry(Document):
else:
frappe.throw(msg)
if qty_to_be_reserved > max_reserved_qty:
if qty_to_be_reserved > allowed_qty:
actual_qty = get_stock_balance(self.item_code, self.warehouse)
msg = """
Cannot reserve more than Max Reserved Qty {0} {1}.<br /><br />
The <b>Max Reserved Qty</b> is calculated as follows:<br />
Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.<br /><br />
The <b>Allowed Qty</b> is calculated as follows:<br />
<ul>
<li><b>Available Qty To Reserve</b> = (Actual Stock Qty - Reserved Stock Qty)</li>
<li><b>Voucher Qty</b> = Voucher Item Qty</li>
<li><b>Delivered Qty</b> = Qty delivered against the Voucher Item</li>
<li><b>Total Reserved Qty</b> = Qty reserved against the Voucher Item</li>
<li><b>Max Reserved Qty</b> = Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))</li>
<li>Actual Qty [Available Qty at Warehouse] = {5}</li>
<li>Reserved Stock [Ignore current SRE] = {6}</li>
<li>Available Qty To Reserve [Actual Qty - Reserved Stock] = {7}</li>
<li>Voucher Qty [Voucher Item Qty] = {8}</li>
<li>Delivered Qty [Qty delivered against the Voucher Item] = {9}</li>
<li>Total Reserved Qty [Qty reserved against the Voucher Item] = {10}</li>
<li>Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {11}</li>
</ul>
""".format(
frappe.bold(max_reserved_qty), self.stock_uom
frappe.bold(allowed_qty),
self.stock_uom,
frappe.bold(self.item_code),
self.voucher_type,
frappe.bold(self.voucher_no),
actual_qty,
actual_qty - self.available_qty,
self.available_qty,
self.voucher_qty,
voucher_delivered_qty,
total_reserved_qty,
allowed_qty,
)
frappe.throw(msg)
@ -509,7 +523,6 @@ def get_available_qty_to_reserve(
"""Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item, Warehouse and Batch combination."""
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.utils import get_stock_balance
if batch_no:
return get_batch_qty(

View File

@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.utils import cint, flt, getdate
from frappe.utils import cint, flt, get_table_name, getdate
from frappe.utils.deprecations import deprecated
from pypika import functions as fn
@ -13,11 +13,22 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
SLE_COUNT_LIMIT = 10_000
def _estimate_table_row_count(doctype: str):
table = get_table_name(doctype)
return cint(
frappe.db.sql(
f"""select table_rows
from information_schema.tables
where table_name = '{table}' ;"""
)[0][0]
)
def execute(filters=None):
if not filters:
filters = {}
sle_count = frappe.db.count("Stock Ledger Entry")
sle_count = _estimate_table_row_count("Stock Ledger Entry")
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))

View File

@ -149,34 +149,36 @@ frappe.query_reports["Reserved Stock"] = {
formatter: (value, row, column, data, default_formatter) => {
value = default_formatter(value, row, column, data);
if (column.fieldname == "status") {
switch (data.status) {
case "Partially Reserved":
value = "<span style='color:orange'>" + value + "</span>";
break;
case "Reserved":
value = "<span style='color:blue'>" + value + "</span>";
break;
case "Partially Delivered":
value = "<span style='color:purple'>" + value + "</span>";
break;
case "Delivered":
value = "<span style='color:green'>" + value + "</span>";
break;
if (data) {
if (column.fieldname == "status") {
switch (data.status) {
case "Partially Reserved":
value = "<span style='color:orange'>" + value + "</span>";
break;
case "Reserved":
value = "<span style='color:blue'>" + value + "</span>";
break;
case "Partially Delivered":
value = "<span style='color:purple'>" + value + "</span>";
break;
case "Delivered":
value = "<span style='color:green'>" + value + "</span>";
break;
}
}
}
else if (column.fieldname == "delivered_qty") {
if (data.delivered_qty > 0) {
if (data.reserved_qty > data.delivered_qty) {
value = "<span style='color:blue'>" + value + "</span>";
else if (column.fieldname == "delivered_qty") {
if (data.delivered_qty > 0) {
if (data.reserved_qty > data.delivered_qty) {
value = "<span style='color:blue'>" + value + "</span>";
}
else {
value = "<span style='color:green'>" + value + "</span>";
}
}
else {
value = "<span style='color:green'>" + value + "</span>";
value = "<span style='color:red'>" + value + "</span>";
}
}
else {
value = "<span style='color:red'>" + value + "</span>";
}
}
return value;

View File

@ -218,15 +218,16 @@ class SerialBatchBundle:
).validate_serial_and_batch_inventory()
def post_process(self):
if not self.sle.serial_and_batch_bundle:
if not self.sle.serial_and_batch_bundle and not self.sle.serial_no and not self.sle.batch_no:
return
docstatus = frappe.get_cached_value(
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
)
if self.sle.serial_and_batch_bundle:
docstatus = frappe.get_cached_value(
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
)
if docstatus != 1:
self.submit_serial_and_batch_bundle()
if docstatus != 1:
self.submit_serial_and_batch_bundle()
if self.item_details.has_serial_no == 1:
self.set_warehouse_and_status_in_serial_nos()
@ -249,7 +250,12 @@ class SerialBatchBundle:
doc.submit()
def set_warehouse_and_status_in_serial_nos(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
serial_nos = get_parsed_serial_nos(self.sle.serial_no)
warehouse = self.warehouse if self.sle.actual_qty > 0 else None
if not serial_nos:
@ -263,7 +269,14 @@ class SerialBatchBundle:
(
frappe.qb.update(sn_table)
.set(sn_table.warehouse, warehouse)
.set(sn_table.status, "Active" if warehouse else status)
.set(
sn_table.status,
"Active"
if warehouse
else status
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
else "Inactive",
)
.where(sn_table.name.isin(serial_nos))
).run()
@ -290,6 +303,8 @@ class SerialBatchBundle:
from erpnext.stock.doctype.batch.batch import get_available_batches
batches = get_batch_nos(self.sle.serial_and_batch_bundle)
if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
batches_qty = get_available_batches(
frappe._dict(
@ -312,13 +327,35 @@ def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
if serial_nos:
filters["serial_no"] = ("in", serial_nos)
entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters)
entries = frappe.get_all(
"Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx"
)
if not entries:
return []
return [d.serial_no for d in entries if d.serial_no]
def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
if not serial_and_batch_bundle:
return []
filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")}
if isinstance(serial_and_batch_bundle, list):
filters = {"parent": ("in", serial_and_batch_bundle)}
if batches:
filters["batch_no"] = ("in", batches)
entries = frappe.get_all(
"Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1
)
if not entries:
return frappe._dict({})
return frappe._dict(entries)
def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)

View File

@ -25,6 +25,7 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor
)
from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_incoming_rate,
get_or_make_bin,
get_stock_balance,
get_valuation_method,
@ -841,14 +842,33 @@ class update_entries_after(object):
get_rate_for_return, # don't move this import to top
)
rate = get_rate_for_return(
sle.voucher_type,
sle.voucher_no,
sle.item_code,
voucher_detail_no=sle.voucher_detail_no,
sle=sle,
)
if self.valuation_method == "Moving Average":
rate = get_incoming_rate(
{
"item_code": sle.item_code,
"warehouse": sle.warehouse,
"posting_date": sle.posting_date,
"posting_time": sle.posting_time,
"qty": sle.actual_qty,
"serial_no": sle.get("serial_no"),
"batch_no": sle.get("batch_no"),
"serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
"company": sle.company,
"voucher_type": sle.voucher_type,
"voucher_no": sle.voucher_no,
"allow_zero_valuation": self.allow_zero_rate,
"sle": sle.name,
}
)
else:
rate = get_rate_for_return(
sle.voucher_type,
sle.voucher_no,
sle.item_code,
voucher_detail_no=sle.voucher_detail_no,
sle=sle,
)
elif (
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
and sle.voucher_detail_no

View File

@ -167,13 +167,13 @@ class SubcontractingReceipt(SubcontractingController):
)
self.update_status_updater_args()
self.update_prevdoc_status()
self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order()
self.set_subcontracting_order_status()
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.update_status()
self.delete_auto_created_batches()
def validate_items_qty(self):
for item in self.items:

View File

@ -365,24 +365,17 @@ class TestSubcontractingReceipt(FrappeTestCase):
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
supplier_warehouse_ac = get_inventory_account(scr.company, scr.supplier_warehouse)
expense_account = scr.items[0].expense_account
expected_values = [
[fg_warehouse_ac, 2100.0, 0.0], # FG Amount (D)
[supplier_warehouse_ac, 0.0, 1000.0], # RM Cost (C)
[additional_costs_expense_account, 0.0, 100.0], # Additional Cost (C)
[expense_account, 0.0, 1000.0], # Service Cost (C)
]
if fg_warehouse_ac == supplier_warehouse_ac:
expected_values = {
fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C)
expense_account: [0.0, 1000.0], # Service Cost (C)
additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C)
}
else:
expected_values = {
fg_warehouse_ac: [2100.0, 0.0], # FG Amount (D)
supplier_warehouse_ac: [0.0, 1000.0], # RM Cost (C)
expense_account: [0.0, 1000.0], # Service Cost (C)
additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C)
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
for i in range(len(expected_values)):
self.assertEqual(expected_values[i][0], gl_entries[i]["account"])
self.assertEqual(expected_values[i][1], gl_entries[i]["debit"])
self.assertEqual(expected_values[i][2], gl_entries[i]["credit"])
scr.reload()
scr.cancel()
@ -953,6 +946,91 @@ class TestSubcontractingReceipt(FrappeTestCase):
scr.submit()
def test_subcontracting_receipt_cancel_with_batch(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
# Step - 1: Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")
# Step - 2: Create FG and RM Items
fg_item = make_item(
properties={"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1}
).name
rm_item1 = make_item(properties={"is_stock_item": 1}).name
rm_item2 = make_item(properties={"is_stock_item": 1}).name
make_item("Subcontracted Service Item Test For Batch 1", {"is_stock_item": 0})
# Step - 3: Create BOM for FG Item
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
for rm_item in bom.items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
bom = bom.name
# Step - 4: Create PO and SCO
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item Test For Batch 1",
"qty": 100,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 100,
},
]
sco = get_subcontracting_order(service_items=service_items)
for rm_item in sco.supplied_items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
# Step - 5: Inward Raw Materials
rm_items = get_rm_items(sco.supplied_items)
for rm_item in rm_items:
rm_item["rate"] = 100
itemwise_details = make_stock_in_entry(rm_items=rm_items)
# Step - 6: Transfer RM's to Subcontractor
se = make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
for item in se.items:
self.assertEqual(item.qty, 100)
self.assertEqual(item.basic_rate, 100)
self.assertEqual(item.amount, item.qty * item.basic_rate)
batch_doc = frappe.get_doc(
{
"doctype": "Batch",
"item": fg_item,
"batch_id": frappe.generate_hash(length=10),
}
).insert(ignore_permissions=True)
serial_batch_bundle = frappe.get_doc(
{
"doctype": "Serial and Batch Bundle",
"item_code": fg_item,
"warehouse": sco.items[0].warehouse,
"has_batch_no": 1,
"type_of_transaction": "Inward",
"voucher_type": "Subcontracting Receipt",
"entries": [{"batch_no": batch_doc.name, "qty": 100}],
}
).insert(ignore_permissions=True)
# Step - 7: Create Subcontracting Receipt
scr = make_subcontracting_receipt(sco.name)
scr.items[0].serial_and_batch_bundle = serial_batch_bundle.name
scr.save()
scr.submit()
scr.load_from_db()
# Step - 8: Cancel Subcontracting Receipt
scr.cancel()
self.assertTrue(scr.docstatus == 2)
@change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
def test_auto_create_purchase_receipt(self):
fg_item = "Subcontracted Item SA1"

View File

@ -58,7 +58,9 @@ frappe.ui.form.on("Issue", {
frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", {
reason: values.reason,
user: frappe.session.user_email
user: frappe.session.user_email,
doctype: frm.doc.doctype,
docname: frm.doc.name,
}, () => {
reset_sla.enable_primary_action();
frm.refresh();

View File

@ -774,10 +774,12 @@ def get_response_and_resolution_duration(doc):
return priority
def reset_service_level_agreement(doc, reason, user):
@frappe.whitelist()
def reset_service_level_agreement(doctype: str, docname: str, reason, user):
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
doc = frappe.get_doc(doctype, docname)
frappe.get_doc(
{
"doctype": "Comment",

View File

@ -0,0 +1,24 @@
import frappe
from frappe.tests.utils import FrappeTestCase
INDEXED_FIELDS = {
"Bin": ["item_code"],
"GL Entry": ["voucher_type", "against_voucher_type"],
"Purchase Order Item": ["item_code"],
"Stock Ledger Entry": ["warehouse"],
}
class TestPerformance(FrappeTestCase):
def test_ensure_indexes(self):
# These fields are not explicitly indexed BUT they are prefix in some
# other composite index. If those are removed this test should be
# updated accordingly.
for doctype, fields in INDEXED_FIELDS.items():
for field in fields:
self.assertTrue(
frappe.db.sql(
f"""SHOW INDEX FROM `tab{doctype}`
WHERE Column_name = "{field}" AND Seq_in_index = 1"""
)
)

View File

@ -103,6 +103,7 @@ Actual Qty is mandatory,Die tatsächliche Menge ist zwingend erforderlich,
Actual Qty {0} / Waiting Qty {1},Tatsächliche Menge {0} / Wartezeit {1},
Actual Qty: Quantity available in the warehouse.,Tatsächliche Menge: Menge verfügbar im Lager.,
Actual qty in stock,Tatsächliche Menge auf Lager,
Actual Time (in Hours via Time Sheet), IST Zeit (in Stunden aus Zeiterfassung),
Actual type tax cannot be included in Item rate in row {0},Tatsächliche Steuerart kann nicht im Artikelpreis in Zeile {0} beinhaltet sein,
Add,Hinzufügen,
Add / Edit Prices,Preise hinzufügen / bearbeiten,

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