Merge branch 'develop' into unreconcile-bank-transaction-on-cancel

This commit is contained in:
barredterra 2024-01-09 18:12:30 +01:00
commit 5b4c5d59d8
300 changed files with 9638 additions and 7721 deletions

View File

@ -21,6 +21,6 @@ jobs:
- name: Run backport
uses: ./actions/backport
with:
token: ${{secrets.BACKPORT_BOT_TOKEN}}
token: ${{secrets.RELEASE_TOKEN}}
labelsToAdd: "backport"
title: "{{originalTitle}}"

View File

@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
version: ["13", "14", "15"]
version: ["14", "15"]
steps:
- uses: octokit/request-action@v2.x

21
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: 'Lock threads'
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: 14
pr-inactive-days: 14

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

@ -91,8 +91,8 @@ class Account(NestedSet):
super(Account, self).on_update()
def onload(self):
frozen_accounts_modifier = frappe.db.get_value(
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
)
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
self.set_onload("can_freeze_account", True)

View File

@ -77,7 +77,7 @@ frappe.treeview_settings["Account"] = {
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? "Dr": "Cr";
const dr_or_cr = balance > 0 ? __("Dr"): __("Cr");
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) {

View File

@ -74,7 +74,7 @@ def create_charts(
# after all accounts are already inserted.
frappe.local.flags.ignore_update_nsm = True
_import_accounts(chart, None, None, root_account=True)
rebuild_tree("Account", "parent_account")
rebuild_tree("Account")
frappe.local.flags.ignore_update_nsm = False
@ -231,6 +231,8 @@ def build_account_tree(tree, parent, all_accounts):
tree[child.account_name]["account_type"] = child.account_type
if child.tax_rate:
tree[child.account_name]["tax_rate"] = child.tax_rate
if child.account_currency:
tree[child.account_name]["account_currency"] = child.account_currency
if not parent:
tree[child.account_name]["root_type"] = child.root_type

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

@ -11,6 +11,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company",
@ -19,7 +20,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-05-01 12:32:34.044911",
"modified": "2024-01-03 11:13:02.669632",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Allowed To Transact With",
@ -28,5 +29,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -137,7 +137,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
args: {
bank_account: frm.doc.bank_account,
till_date: frm.doc.bank_statement_from_date,
till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1)
},
callback: (response) => {
frm.set_value("account_opening_balance", response.message);

View File

@ -444,6 +444,10 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers)
transaction.validate_duplicate_references()
transaction.allocate_payment_entries()
transaction.update_allocated_amount()
transaction.set_status()
transaction.save()
return transaction

View File

@ -4,12 +4,11 @@
import frappe
from frappe import _
from frappe.model.docstatus import DocStatus
from frappe.model.document import Document
from frappe.utils import flt
from erpnext.controllers.status_updater import StatusUpdater
class BankTransaction(StatusUpdater):
class BankTransaction(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@ -51,6 +50,15 @@ class BankTransaction(StatusUpdater):
def validate(self):
self.validate_duplicate_references()
def set_status(self):
if self.docstatus == 2:
self.db_set("status", "Cancelled")
elif self.docstatus == 1:
if self.unallocated_amount > 0:
self.db_set("status", "Unreconciled")
elif self.unallocated_amount <= 0:
self.db_set("status", "Reconciled")
def validate_duplicate_references(self):
"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
if not self.payment_entries:
@ -84,12 +92,13 @@ class BankTransaction(StatusUpdater):
self.validate_duplicate_references()
self.allocate_payment_entries()
self.update_allocated_amount()
self.set_status()
def on_cancel(self):
for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.set_status(update=True)
self.set_status()
def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
@ -367,15 +376,17 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
):
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=doctype, parent=docname),
"clearance_date",
clearance_date,
)
if doctype == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=doctype, parent=docname),
"clearance_date",
clearance_date,
)
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund

View File

@ -0,0 +1,100 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Bisect Accounting Statements", {
onload(frm) {
frm.trigger("render_heatmap");
},
refresh(frm) {
frm.add_custom_button(__('Bisect Left'), () => {
frm.trigger("bisect_left");
});
frm.add_custom_button(__('Bisect Right'), () => {
frm.trigger("bisect_right");
});
frm.add_custom_button(__('Up'), () => {
frm.trigger("move_up");
});
frm.add_custom_button(__('Build Tree'), () => {
frm.trigger("build_tree");
});
},
render_heatmap(frm) {
let bisect_heatmap = frm.get_field("bisect_heatmap").$wrapper;
bisect_heatmap.addClass("bisect_heatmap_location");
// milliseconds in a day
let msiad=24*60*60*1000;
let datapoints = {};
let fr_dt = new Date(frm.doc.from_date).getTime();
let to_dt = new Date(frm.doc.to_date).getTime();
let bisect_start = new Date(frm.doc.current_from_date).getTime();
let bisect_end = new Date(frm.doc.current_to_date).getTime();
for(let x=fr_dt; x <= to_dt; x+=msiad){
let epoch_in_seconds = x/1000;
if ((bisect_start <= x) && (x <= bisect_end )) {
datapoints[epoch_in_seconds] = 1.0;
} else {
datapoints[epoch_in_seconds] = 0.0;
}
}
new frappe.Chart(".bisect_heatmap_location", {
type: "heatmap",
data: {
dataPoints: datapoints,
start: new Date(frm.doc.from_date),
end: new Date(frm.doc.to_date),
},
countLabel: 'Bisecting',
discreteDomains: 1,
});
},
bisect_left(frm) {
frm.call({
doc: frm.doc,
method: 'bisect_left',
freeze: true,
freeze_message: __("Bisecting Left ..."),
callback: (r) => {
frm.trigger("render_heatmap");
}
});
},
bisect_right(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Bisecting Right ..."),
method: 'bisect_right',
callback: (r) => {
frm.trigger("render_heatmap");
}
});
},
move_up(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Moving up in tree ..."),
method: 'move_up',
callback: (r) => {
frm.trigger("render_heatmap");
}
});
},
build_tree(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Rebuilding BTree for period ..."),
method: 'build_tree',
callback: (r) => {
frm.trigger("render_heatmap");
}
});
},
});

View File

@ -0,0 +1,194 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-09-15 21:28:28.054773",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_cvfg",
"company",
"column_break_hcam",
"from_date",
"column_break_qxbi",
"to_date",
"column_break_iwny",
"algorithm",
"section_break_8ph9",
"current_node",
"section_break_ngid",
"bisect_heatmap",
"section_break_hmsy",
"bisecting_from",
"current_from_date",
"column_break_uqyd",
"bisecting_to",
"current_to_date",
"section_break_hbyo",
"heading_cppb",
"p_l_summary",
"column_break_aivo",
"balance_sheet_summary",
"b_s_summary",
"column_break_gvwx",
"difference_heading",
"difference"
],
"fields": [
{
"fieldname": "column_break_qxbi",
"fieldtype": "Column Break"
},
{
"fieldname": "from_date",
"fieldtype": "Datetime",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Datetime",
"label": "To Date"
},
{
"default": "BFS",
"fieldname": "algorithm",
"fieldtype": "Select",
"label": "Algorithm",
"options": "BFS\nDFS"
},
{
"fieldname": "column_break_iwny",
"fieldtype": "Column Break"
},
{
"fieldname": "current_node",
"fieldtype": "Link",
"label": "Current Node",
"options": "Bisect Nodes"
},
{
"fieldname": "section_break_hmsy",
"fieldtype": "Section Break"
},
{
"fieldname": "current_from_date",
"fieldtype": "Datetime",
"read_only": 1
},
{
"fieldname": "current_to_date",
"fieldtype": "Datetime",
"read_only": 1
},
{
"fieldname": "column_break_uqyd",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_hbyo",
"fieldtype": "Section Break"
},
{
"fieldname": "p_l_summary",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "b_s_summary",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "difference",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "column_break_aivo",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_gvwx",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "column_break_hcam",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ngid",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_8ph9",
"fieldtype": "Section Break",
"hidden": 1
},
{
"fieldname": "bisect_heatmap",
"fieldtype": "HTML",
"label": "Heatmap"
},
{
"fieldname": "heading_cppb",
"fieldtype": "Heading",
"label": "Profit and Loss Summary"
},
{
"fieldname": "balance_sheet_summary",
"fieldtype": "Heading",
"label": "Balance Sheet Summary"
},
{
"fieldname": "difference_heading",
"fieldtype": "Heading",
"label": "Difference"
},
{
"fieldname": "bisecting_from",
"fieldtype": "Heading",
"label": "Bisecting From"
},
{
"fieldname": "bisecting_to",
"fieldtype": "Heading",
"label": "Bisecting To"
},
{
"fieldname": "section_break_cvfg",
"fieldtype": "Section Break"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-12-01 16:49:54.073890",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bisect Accounting Statements",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Administrator",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,226 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import datetime
from collections import deque
from math import floor
import frappe
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
from frappe.utils.data import guess_date_format
class BisectAccountingStatements(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
algorithm: DF.Literal["BFS", "DFS"]
b_s_summary: DF.Float
company: DF.Link | None
current_from_date: DF.Datetime | None
current_node: DF.Link | None
current_to_date: DF.Datetime | None
difference: DF.Float
from_date: DF.Datetime | None
p_l_summary: DF.Float
to_date: DF.Datetime | None
# end: auto-generated types
def validate(self):
self.validate_dates()
def validate_dates(self):
if getdate(self.from_date) > getdate(self.to_date):
frappe.throw(
_("From Date: {0} cannot be greater than To date: {1}").format(
frappe.bold(self.from_date), frappe.bold(self.to_date)
)
)
def bfs(self, from_date: datetime, to_date: datetime):
# Make Root node
node = frappe.new_doc("Bisect Nodes")
node.root = None
node.period_from_date = from_date
node.period_to_date = to_date
node.insert()
period_queue = deque([node])
while period_queue:
cur_node = period_queue.popleft()
delta = cur_node.period_to_date - cur_node.period_from_date
if delta.days == 0:
continue
else:
cur_floor = floor(delta.days / 2)
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
left_node = frappe.new_doc("Bisect Nodes")
left_node.period_from_date = cur_node.period_from_date
left_node.period_to_date = next_to_date
left_node.root = cur_node.name
left_node.generated = False
left_node.insert()
cur_node.left_child = left_node.name
period_queue.append(left_node)
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
right_node = frappe.new_doc("Bisect Nodes")
right_node.period_from_date = next_from_date
right_node.period_to_date = cur_node.period_to_date
right_node.root = cur_node.name
right_node.generated = False
right_node.insert()
cur_node.right_child = right_node.name
period_queue.append(right_node)
cur_node.save()
def dfs(self, from_date: datetime, to_date: datetime):
# Make Root node
node = frappe.new_doc("Bisect Nodes")
node.root = None
node.period_from_date = from_date
node.period_to_date = to_date
node.insert()
period_stack = [node]
while period_stack:
cur_node = period_stack.pop()
delta = cur_node.period_to_date - cur_node.period_from_date
if delta.days == 0:
continue
else:
cur_floor = floor(delta.days / 2)
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
left_node = frappe.new_doc("Bisect Nodes")
left_node.period_from_date = cur_node.period_from_date
left_node.period_to_date = next_to_date
left_node.root = cur_node.name
left_node.generated = False
left_node.insert()
cur_node.left_child = left_node.name
period_stack.append(left_node)
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
right_node = frappe.new_doc("Bisect Nodes")
right_node.period_from_date = next_from_date
right_node.period_to_date = cur_node.period_to_date
right_node.root = cur_node.name
right_node.generated = False
right_node.insert()
cur_node.right_child = right_node.name
period_stack.append(right_node)
cur_node.save()
@frappe.whitelist()
def build_tree(self):
frappe.db.delete("Bisect Nodes")
# Convert str to datetime format
dt_format = guess_date_format(self.from_date)
from_date = datetime.datetime.strptime(self.from_date, dt_format)
to_date = datetime.datetime.strptime(self.to_date, dt_format)
if self.algorithm == "BFS":
self.bfs(from_date, to_date)
if self.algorithm == "DFS":
self.dfs(from_date, to_date)
# set root as current node
root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0]
self.get_report_summary()
self.current_node = root.name
self.current_from_date = self.from_date
self.current_to_date = self.to_date
self.save()
def get_report_summary(self):
filters = {
"company": self.company,
"filter_based_on": "Date Range",
"period_start_date": self.current_from_date,
"period_end_date": self.current_to_date,
"periodicity": "Yearly",
}
pl_summary = frappe.get_doc("Report", "Profit and Loss Statement")
self.p_l_summary = pl_summary.execute_script_report(filters=filters)[5]
bs_summary = frappe.get_doc("Report", "Balance Sheet")
self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5]
self.difference = abs(self.p_l_summary - self.b_s_summary)
def update_node(self):
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
current_node.balance_sheet_summary = self.b_s_summary
current_node.profit_loss_summary = self.p_l_summary
current_node.difference = self.difference
current_node.generated = True
current_node.save()
def current_node_has_summary_info(self):
"Assertion method"
return frappe.db.get_value("Bisect Nodes", self.current_node, "generated")
def fetch_summary_info_from_current_node(self):
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
self.p_l_summary = current_node.balance_sheet_summary
self.b_s_summary = current_node.profit_loss_summary
self.difference = abs(self.p_l_summary - self.b_s_summary)
def fetch_or_calculate(self):
if self.current_node_has_summary_info():
self.fetch_summary_info_from_current_node()
else:
self.get_report_summary()
self.update_node()
@frappe.whitelist()
def bisect_left(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.left_child is not None:
lft_node = frappe.get_doc("Bisect Nodes", cur_node.left_child)
self.current_node = cur_node.left_child
self.current_from_date = lft_node.period_from_date
self.current_to_date = lft_node.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("No more children on Left"))
@frappe.whitelist()
def bisect_right(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.right_child is not None:
rgt_node = frappe.get_doc("Bisect Nodes", cur_node.right_child)
self.current_node = cur_node.right_child
self.current_from_date = rgt_node.period_from_date
self.current_to_date = rgt_node.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("No more children on Right"))
@frappe.whitelist()
def move_up(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.root is not None:
root = frappe.get_doc("Bisect Nodes", cur_node.root)
self.current_node = cur_node.root
self.current_from_date = root.period_from_date
self.current_to_date = root.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("Reached Root"))

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestBisectAccountingStatements(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Bisect Nodes", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,97 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2023-09-27 14:56:38.112462",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"root",
"left_child",
"right_child",
"period_from_date",
"period_to_date",
"difference",
"balance_sheet_summary",
"profit_loss_summary",
"generated"
],
"fields": [
{
"fieldname": "root",
"fieldtype": "Link",
"label": "Root",
"options": "Bisect Nodes"
},
{
"fieldname": "left_child",
"fieldtype": "Link",
"label": "Left Child",
"options": "Bisect Nodes"
},
{
"fieldname": "right_child",
"fieldtype": "Link",
"label": "Right Child",
"options": "Bisect Nodes"
},
{
"fieldname": "period_from_date",
"fieldtype": "Datetime",
"label": "Period_from_date"
},
{
"fieldname": "period_to_date",
"fieldtype": "Datetime",
"label": "Period To Date"
},
{
"fieldname": "difference",
"fieldtype": "Float",
"label": "Difference"
},
{
"fieldname": "balance_sheet_summary",
"fieldtype": "Float",
"label": "Balance Sheet Summary"
},
{
"fieldname": "profit_loss_summary",
"fieldtype": "Float",
"label": "Profit and Loss Summary"
},
{
"default": "0",
"fieldname": "generated",
"fieldtype": "Check",
"label": "Generated"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-12-01 17:46:12.437996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bisect Nodes",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,29 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class BisectNodes(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
balance_sheet_summary: DF.Float
difference: DF.Float
generated: DF.Check
left_child: DF.Link | None
name: DF.Int | None
period_from_date: DF.Datetime | None
period_to_date: DF.Datetime | None
profit_loss_summary: DF.Float
right_child: DF.Link | None
root: DF.Link | None
# end: auto-generated types
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestBisectNodes(FrappeTestCase):
pass

View File

@ -1,457 +1,152 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"autoname": "naming_series:",
"beta": 0,
"creation": "2018-06-18 16:51:49.994750",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"user",
"date",
"from_time",
"time",
"expense",
"custody",
"returns",
"outstanding_amount",
"payments",
"net_amount",
"amended_from"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "POS-CLO-",
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Series",
"length": 0,
"no_copy": 0,
"options": "POS-CLO-",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today",
"fieldname": "date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "From Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "To Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00",
"fieldname": "expense",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Expense",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Expense"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00",
"fieldname": "custody",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Custody",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Custody"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00",
"fieldname": "returns",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Returns",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"precision": "2"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00",
"fieldname": "outstanding_amount",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Outstanding Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.0",
"fieldname": "payments",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payments",
"length": 0,
"no_copy": 0,
"options": "Cashier Closing Payments",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Cashier Closing Payments"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "net_amount",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Net Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Cashier Closing",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-02-19 08:35:24.157327",
"links": [],
"modified": "2023-12-28 13:15:46.858427",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cashier Closing",
"name_case": "",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
"states": [],
"track_changes": 1
}

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
@ -432,8 +435,8 @@ def update_outstanding_amt(
def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == "Yes" and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value(
"Accounts Settings", None, "frozen_accounts_modifier"
frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
)
if not frozen_accounts_modifier:

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,42 +1031,108 @@ class JournalEntry(AccountsController):
def build_gl_map(self):
gl_map = []
conversion_rate_map = self.get_conversion_rate_map()
transaction_currency_map = self.get_transaction_currency_map()
company_currency = erpnext.get_company_currency(self.company)
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,
"conversion_rate": conversion_rate_map.get(d.against_account_link, 1)
if d.account_currency == company_currency
else 1,
"currency": transaction_currency_map.get(d.against_account_link, d.account_currency)
if d.account_currency == company_currency
else d.account_currency,
},
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 get_transaction_currency_map(self):
transaction_currency_map = {}
for account in self.get("accounts"):
transaction_currency_map.setdefault(account.party or account.account, account.account_currency)
return transaction_currency_map
def get_conversion_rate_map(self):
conversion_rate_map = {}
for account in self.get("accounts"):
conversion_rate_map.setdefault(account.party or account.account, account.exchange_rate)
return conversion_rate_map
def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries
@ -1625,3 +1755,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

@ -747,6 +747,10 @@ frappe.ui.form.on('Payment Entry', {
args["get_orders_to_be_billed"] = true;
}
if (frm.doc.book_advance_payments_in_separate_party_account) {
args["book_advance_payments_in_separate_party_account"] = true;
}
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({

View File

@ -223,6 +223,7 @@
"fieldname": "party_balance",
"fieldtype": "Currency",
"label": "Party Balance",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@ -759,7 +760,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2023-11-23 12:07:20.887885",
"modified": "2024-01-08 13:17:15.744754",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@ -50,6 +50,88 @@ class InvalidPaymentEntry(ValidationError):
class PaymentEntry(AccountsController):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.advance_taxes_and_charges.advance_taxes_and_charges import (
AdvanceTaxesandCharges,
)
from erpnext.accounts.doctype.payment_entry_deduction.payment_entry_deduction import (
PaymentEntryDeduction,
)
from erpnext.accounts.doctype.payment_entry_reference.payment_entry_reference import (
PaymentEntryReference,
)
amended_from: DF.Link | None
apply_tax_withholding_amount: DF.Check
auto_repeat: DF.Link | None
bank: DF.ReadOnly | None
bank_account: DF.Link | None
bank_account_no: DF.ReadOnly | None
base_paid_amount: DF.Currency
base_paid_amount_after_tax: DF.Currency
base_received_amount: DF.Currency
base_received_amount_after_tax: DF.Currency
base_total_allocated_amount: DF.Currency
base_total_taxes_and_charges: DF.Currency
book_advance_payments_in_separate_party_account: DF.Check
clearance_date: DF.Date | None
company: DF.Link
contact_email: DF.Data | None
contact_person: DF.Link | None
cost_center: DF.Link | None
custom_remarks: DF.Check
deductions: DF.Table[PaymentEntryDeduction]
difference_amount: DF.Currency
letter_head: DF.Link | None
mode_of_payment: DF.Link | None
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
paid_amount: DF.Currency
paid_amount_after_tax: DF.Currency
paid_from: DF.Link
paid_from_account_balance: DF.Currency
paid_from_account_currency: DF.Link
paid_from_account_type: DF.Data | None
paid_to: DF.Link
paid_to_account_balance: DF.Currency
paid_to_account_currency: DF.Link
paid_to_account_type: DF.Data | None
party: DF.DynamicLink | None
party_balance: DF.Currency
party_bank_account: DF.Link | None
party_name: DF.Data | None
party_type: DF.Link | None
payment_order: DF.Link | None
payment_order_status: DF.Literal["Initiated", "Payment Ordered"]
payment_type: DF.Literal["Receive", "Pay", "Internal Transfer"]
posting_date: DF.Date
print_heading: DF.Link | None
project: DF.Link | None
purchase_taxes_and_charges_template: DF.Link | None
received_amount: DF.Currency
received_amount_after_tax: DF.Currency
reference_date: DF.Date | None
reference_no: DF.Data | None
references: DF.Table[PaymentEntryReference]
remarks: DF.SmallText | None
sales_taxes_and_charges_template: DF.Link | None
source_exchange_rate: DF.Float
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
target_exchange_rate: DF.Float
tax_withholding_category: DF.Link | None
taxes: DF.Table[AdvanceTaxesandCharges]
title: DF.Data | None
total_allocated_amount: DF.Currency
total_taxes_and_charges: DF.Currency
unallocated_amount: DF.Currency
# end: auto-generated types
def __init__(self, *args, **kwargs):
super(PaymentEntry, self).__init__(*args, **kwargs)
if not self.is_new():
@ -256,6 +338,7 @@ class PaymentEntry(AccountsController):
"get_outstanding_invoices": True,
"get_orders_to_be_billed": True,
"vouchers": vouchers,
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
},
validate=True,
)
@ -1061,7 +1144,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 +1311,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 +1328,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 +1354,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 +1363,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 +1390,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 +1417,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,
@ -1615,11 +1711,16 @@ def get_outstanding_reference_documents(args, validate=False):
outstanding_invoices = []
negative_outstanding_invoices = []
if args.get("book_advance_payments_in_separate_party_account"):
party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
else:
party_account = args.get("party_account")
if args.get("get_outstanding_invoices"):
outstanding_invoices = get_outstanding_invoices(
args.get("party_type"),
args.get("party"),
get_party_account(args.get("party_type"), args.get("party"), args.get("company")),
party_account,
common_filter=common_filter,
posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"),

View File

@ -765,7 +765,7 @@ def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
.select(Sum(p_item.qty).as_("qty"))
.select(Sum(p_item.stock_qty).as_("stock_qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
@ -775,7 +775,7 @@ def get_pos_reserved_qty(item_code, warehouse):
)
).run(as_dict=True)
return reserved_qty[0].qty or 0 if reserved_qty else 0
return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
@frappe.whitelist()

View File

@ -527,7 +527,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
values.extend(warehouses)
if items:
condition = " and `tab{child_doc}`.{apply_on} in ({items})".format(
condition += " and `tab{child_doc}`.{apply_on} in ({items})".format(
child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items))
)

View File

@ -440,7 +440,7 @@ def reconcile(doc: None | str = None) -> None:
# Update the parent doc about the exception
frappe.db.rollback()
traceback = frappe.get_traceback()
traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)

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

@ -35,7 +35,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload();
// Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"];
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Serial and Batch Bundle", "Bank Transaction"];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
@ -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);
@ -396,6 +408,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
on_submit() {
super.on_submit();
$.each(this.frm.doc["items"] || [], function(i, row) {
if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt)
})

View File

@ -552,7 +552,7 @@ class PurchaseInvoice(BuyingController):
self.against_expense_account = ",".join(against_accounts)
def po_required(self):
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes":
if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
@ -572,7 +572,7 @@ class PurchaseInvoice(BuyingController):
def pr_required(self):
stock_items = self.get_stock_items()
if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes":
if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes":
if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
@ -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,
@ -1135,11 +1157,17 @@ class PurchaseInvoice(BuyingController):
)
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
"Asset",
filters={"purchase_invoice": self.name, "item_code": item.item_code},
fields=["name", "asset_quantity"],
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
frappe.db.set_value(
"Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity
)
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity
)
def make_stock_adjustment_entry(
self, gl_entries, item, voucher_wise_stock_value, account_currency
@ -1168,7 +1196,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 +1228,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 +1278,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 +1299,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 +1316,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 +1339,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 +1362,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 +1388,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 +1410,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 +1439,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 +1864,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

@ -14,7 +14,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.controllers.accounts_controller import InvalidQtyError, get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
@ -51,6 +51,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
def tearDown(self):
frappe.db.rollback()
def test_purchase_invoice_qty(self):
pi = make_purchase_invoice(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
pi.save()
# No error with qty=1
pi.items[0].qty = 1
pi.save()
self.assertEqual(pi.items[0].qty, 1)
def test_purchase_invoice_received_qty(self):
"""
1. Test if received qty is validated against accepted + rejected
@ -1227,8 +1237,8 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
unlink_enabled = frappe.db.get_single_value(
"Accounts Settings", "unlink_payment_on_cancel_of_invoice"
)
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
@ -1985,6 +1995,26 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
def test_debit_note_without_item(self):
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
pi.items[0].item_code = ""
pi.save()
self.assertFalse(pi.items[0].item_code)
pi.submit()
return_pi = make_purchase_invoice(
item_name="_Test Item",
is_return=1,
return_against=pi.name,
qty=-10,
do_not_save=True,
)
return_pi.items[0].item_code = ""
return_pi.save()
return_pi.submit()
self.assertEqual(return_pi.docstatus, 1)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
@ -2094,7 +2124,7 @@ def make_purchase_invoice(**args):
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
batches = {}
qty = args.qty or 5
qty = args.qty if args.qty is not None else 5
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@ -2121,8 +2151,9 @@ def make_purchase_invoice(**args):
"items",
{
"item_code": args.item or args.item_code or "_Test Item",
"item_name": args.item_name,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 5,
"qty": args.qty if args.qty is not None else 5,
"received_qty": args.received_qty or 0,
"rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50,

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

@ -43,7 +43,7 @@ def start_payment_ledger_repost(docname=None):
except Exception as e:
frappe.db.rollback()
traceback = frappe.get_traceback()
traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)

View File

@ -37,7 +37,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"];
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Serial and Batch Bundle", "Bank Transaction",
];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
@ -197,6 +198,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
on_submit(doc, dt, dn) {
var me = this;
super.on_submit();
if (frappe.get_route()[0] != 'Form') {
return
}

View File

@ -138,6 +138,7 @@
"loyalty_amount",
"column_break_77",
"loyalty_program",
"dont_create_loyalty_points",
"loyalty_redemption_account",
"loyalty_redemption_cost_center",
"contact_and_address_tab",
@ -1041,8 +1042,7 @@
"label": "Loyalty Program",
"no_copy": 1,
"options": "Loyalty Program",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"allow_on_submit": 1,
@ -2162,6 +2162,14 @@
"fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note"
},
{
"default": "0",
"depends_on": "loyalty_program",
"fieldname": "dont_create_loyalty_points",
"fieldtype": "Check",
"label": "Don't Create Loyalty Points",
"no_copy": 1
}
],
"icon": "fa fa-file-text",
@ -2174,7 +2182,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-11-23 16:56:29.679499",
"modified": "2024-01-02 17:25:46.027523",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

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
@ -117,6 +117,7 @@ class SalesInvoice(SellingController):
discount_amount: DF.Currency
dispatch_address: DF.SmallText | None
dispatch_address_name: DF.Link | None
dont_create_loyalty_points: DF.Check
due_date: DF.Date | None
from_date: DF.Date | None
grand_total: DF.Currency
@ -471,7 +472,12 @@ class SalesInvoice(SellingController):
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
if not self.is_return and not self.is_consolidated and self.loyalty_program:
if (
not self.is_return
and not self.is_consolidated
and self.loyalty_program
and not self.dont_create_loyalty_points
):
self.make_loyalty_point_entry()
elif (
self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program
@ -586,6 +592,8 @@ class SalesInvoice(SellingController):
"Serial and Batch Bundle",
)
self.delete_auto_created_batches()
def update_status_updater_args(self):
if cint(self.update_stock):
self.status_updater.append(
@ -1225,7 +1233,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 +1264,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 +1287,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 +1357,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 +1380,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 +1436,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 +1452,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 +1481,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 +1504,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 +1530,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 +1553,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 +1581,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 +1603,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 +1633,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 +2390,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 +2583,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

@ -23,7 +23,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_d
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.accounts_controller import InvalidQtyError, update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
@ -72,6 +72,16 @@ class TestSalesInvoice(FrappeTestCase):
def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0)
def test_sales_invoice_qty(self):
si = create_sales_invoice(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
si.save()
# No error with qty=1
si.items[0].qty = 1
si.save()
self.assertEqual(si.items[0].qty, 1)
def test_timestamp_change(self):
w = frappe.copy_doc(test_records[0])
w.docstatus = 0
@ -1414,10 +1424,11 @@ class TestSalesInvoice(FrappeTestCase):
def test_serialized_cancel(self):
si = self.test_serialized()
si.cancel()
si.reload()
serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle)
si.cancel()
self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
)
@ -3635,7 +3646,7 @@ def create_sales_invoice(**args):
bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
batches = {}
qty = args.qty or 1
qty = args.qty if args.qty is not None else 1
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@ -3667,7 +3678,7 @@ def create_sales_invoice(**args):
"description": args.description or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"target_warehouse": args.target_warehouse,
"qty": args.qty or 1,
"qty": args.qty if args.qty is not None else 1,
"uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100,

View File

@ -81,6 +81,7 @@
"warehouse",
"target_warehouse",
"quality_inspection",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"batch_no",
"incoming_rate",
@ -897,12 +898,18 @@
"options": "Serial and Batch Bundle",
"print_hide": 1,
"search_index": 1
},
{
"depends_on": "eval:parent.update_stock === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:34:10.479329",
"modified": "2023-12-29 13:03:14.121298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@ -148,13 +148,13 @@
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Percent",
"label": "Additional DIscount Percentage"
"label": "Additional Discount Percentage"
},
{
"collapsible": 1,
"fieldname": "additional_discount_amount",
"fieldtype": "Currency",
"label": "Additional DIscount Amount"
"label": "Additional Discount Amount"
},
{
"collapsible": 1,
@ -267,7 +267,7 @@
"link_fieldname": "subscription"
}
],
"modified": "2023-09-18 17:48:21.900252",
"modified": "2023-12-28 17:20:42.687789",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",

View File

@ -356,18 +356,20 @@ class Subscription(Document):
self,
from_date: Optional[Union[str, datetime.date]] = None,
to_date: Optional[Union[str, datetime.date]] = None,
posting_date: Optional[Union[str, datetime.date]] = None,
) -> Document:
"""
Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
saves the `Subscription`.
Backwards compatibility
"""
return self.create_invoice(from_date=from_date, to_date=to_date)
return self.create_invoice(from_date=from_date, to_date=to_date, posting_date=posting_date)
def create_invoice(
self,
from_date: Optional[Union[str, datetime.date]] = None,
to_date: Optional[Union[str, datetime.date]] = None,
posting_date: Optional[Union[str, datetime.date]] = None,
) -> Document:
"""
Creates a `Invoice`, submits it and returns it
@ -385,11 +387,13 @@ class Subscription(Document):
invoice = frappe.new_doc(self.invoice_document_type)
invoice.company = company
invoice.set_posting_time = 1
invoice.posting_date = (
self.current_invoice_start
if self.generate_invoice_at == "Beginning of the current subscription period"
else self.current_invoice_end
)
if self.generate_invoice_at == "Beginning of the current subscription period":
invoice.posting_date = self.current_invoice_start
elif self.generate_invoice_at == "Days before the current subscription period":
invoice.posting_date = posting_date or self.current_invoice_start
else:
invoice.posting_date = self.current_invoice_end
invoice.cost_center = self.cost_center
@ -413,6 +417,7 @@ class Subscription(Document):
# Subscription is better suited for service items. I won't update `update_stock`
# for that reason
items_list = self.get_items_from_plans(self.plans, is_prorate())
for item in items_list:
item["cost_center"] = self.cost_center
invoice.append("items", item)
@ -556,7 +561,7 @@ class Subscription(Document):
if not self.is_current_invoice_generated(
self.current_invoice_start, self.current_invoice_end
) and self.can_generate_new_invoice(posting_date):
self.generate_invoice()
self.generate_invoice(posting_date=posting_date)
self.update_subscription_period(add_days(self.current_invoice_end, 1))
if self.cancel_at_period_end and (

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:
@ -653,10 +654,10 @@ def check_freezing_date(posting_date, adv_adj=False):
Hence stop admin to bypass if accounts are freezed
"""
if not adv_adj:
acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto")
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
if acc_frozen_upto:
frozen_accounts_modifier = frappe.db.get_value(
"Accounts Settings", None, "frozen_accounts_modifier"
frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
)
if getdate(posting_date) <= getdate(acc_frozen_upto) and (
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"

View File

@ -114,14 +114,12 @@ def _get_party_details(
set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)
)
party = party_details[party_type.lower()]
if not ignore_permissions and not (
frappe.has_permission(party_type, "read", party)
or frappe.has_permission(party_type, "select", party)
):
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party)
if not ignore_permissions:
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details(
@ -637,9 +635,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
return due_date
def validate_due_date(
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
):
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else:

View File

@ -55,8 +55,8 @@ class ReceivablePayableReport(object):
def run(self, args):
self.filters.update(args)
self.set_defaults()
self.party_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1]
self.party_naming_by = frappe.db.get_single_value(
args.get("naming_by")[0], args.get("naming_by")[1]
)
self.get_columns()
self.get_data()
@ -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

@ -24,8 +24,8 @@ class AccountsReceivableSummary(ReceivablePayableReport):
def run(self, args):
self.account_type = args.get("account_type")
self.party_type = get_party_types_from_account_type(self.account_type)
self.party_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1]
self.party_naming_by = frappe.db.get_single_value(
args.get("naming_by")[0], args.get("naming_by")[1]
)
self.get_columns()
self.get_data(args)

View File

@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"reqd": 1
},
{
"fieldname":"group_by",
"label": __("Group By"),
"fieldtype": "Select",
"options": ["Asset Category", "Asset"],
"default": "Asset Category",
},
{
"fieldname":"asset_category",
"label": __("Asset Category"),
"fieldtype": "Link",
"options": "Asset Category"
}
"options": "Asset Category",
"depends_on": "eval: doc.group_by == 'Asset Category'",
},
{
"fieldname":"asset",
"label": __("Asset"),
"fieldtype": "Link",
"options": "Asset",
"depends_on": "eval: doc.group_by == 'Asset'",
},
]
}

View File

@ -14,10 +14,17 @@ def execute(filters=None):
def get_data(filters):
if filters.get("group_by") == "Asset Category":
return get_group_by_asset_category_data(filters)
elif filters.get("group_by") == "Asset":
return get_group_by_asset_data(filters)
def get_group_by_asset_category_data(filters):
data = []
asset_categories = get_asset_categories(filters)
assets = get_assets(filters)
asset_categories = get_asset_categories_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_category(filters)
for asset_category in asset_categories:
row = frappe._dict()
@ -38,6 +45,7 @@ def get_data(filters):
if asset["asset_category"] == asset_category.get("asset_category", "")
)
)
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
@ -57,7 +65,7 @@ def get_data(filters):
return data
def get_asset_categories(filters):
def get_asset_categories_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
@ -116,7 +124,105 @@ def get_asset_categories(filters):
)
def get_assets(filters):
def get_asset_details_for_grouped_by_category(filters):
condition = ""
if filters.get("asset"):
condition += " and name = %(asset)s"
return frappe.db.sql(
"""
SELECT name,
ifnull(sum(case when purchase_date < %(from_date)s then
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_as_on_from_date,
ifnull(sum(case when purchase_date >= %(from_date)s then
gross_purchase_amount
else
0
end), 0) as cost_of_new_purchase,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Sold" then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_sold_asset,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Scrapped" then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
group by name
""".format(
condition
),
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"asset": filters.get("asset"),
},
as_dict=1,
)
def get_group_by_asset_data(filters):
data = []
asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(filters)
for asset_detail in asset_details:
row = frappe._dict()
# row.asset_category = asset_category
row.update(asset_detail)
row.cost_as_on_to_date = (
flt(row.cost_as_on_from_date)
+ flt(row.cost_of_new_purchase)
- flt(row.cost_of_sold_asset)
- flt(row.cost_of_scrapped_asset)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
)
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
data.append(row)
return data
def get_assets_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
@ -178,15 +284,93 @@ def get_assets(filters):
)
def get_assets_for_grouped_by_asset(filters):
condition = ""
if filters.get("asset"):
condition = " and a.name = '{}'".format(filters.get("asset"))
return frappe.db.sql(
"""
SELECT results.name as asset,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.name as name,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
gle.debit
else
0
end), 0) as depreciation_amount_during_the_period
from `tabGL Entry` gle
join `tabAsset` a on
gle.against_voucher = a.name
join `tabAsset Category Account` aca on
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
group by a.name
union
SELECT a.name as name,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
0
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
group by a.name) as results
group by results.name
""".format(
condition
),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
as_dict=1,
)
def get_columns(filters):
return [
{
"label": _("Asset Category"),
"fieldname": "asset_category",
"fieldtype": "Link",
"options": "Asset Category",
"width": 120,
},
columns = []
if filters.get("group_by") == "Asset Category":
columns.append(
{
"label": _("Asset Category"),
"fieldname": "asset_category",
"fieldtype": "Link",
"options": "Asset Category",
"width": 120,
}
)
elif filters.get("group_by") == "Asset":
columns.append(
{
"label": _("Asset"),
"fieldname": "asset",
"fieldtype": "Link",
"options": "Asset",
"width": 120,
}
)
columns += [
{
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "cost_as_on_from_date",
@ -254,3 +438,5 @@ def get_columns(filters):
"width": 200,
},
]
return columns

View File

@ -97,11 +97,11 @@ def execute(filters=None):
chart = get_chart_data(filters, columns, asset, liability, equity)
report_summary = get_report_summary(
report_summary, primitive_summary = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
)
return columns, data, message, chart, report_summary
return columns, data, message, chart, report_summary, primitive_summary
def get_provisional_profit_loss(
@ -217,7 +217,7 @@ def get_report_summary(
"datatype": "Currency",
"currency": currency,
},
]
], (net_asset - net_liability + net_equity)
def get_chart_data(filters, columns, asset, liability, equity):

View File

@ -2,7 +2,44 @@
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Budget Variance Report"] = {
"filters": [
"filters": get_filters(),
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes(__("variance"))) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
}
function get_filters() {
function get_dimensions() {
let result = [];
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
args: {
'with_cost_center_and_project': true
},
async: false,
callback: function(r) {
if(!r.exc) {
result = r.message[0].map(elem => elem.document_type);
}
}
});
return result;
}
let budget_against_options = get_dimensions();
let filters = [
{
fieldname: "from_fiscal_year",
label: __("From Fiscal Year"),
@ -44,9 +81,13 @@ frappe.query_reports["Budget Variance Report"] = {
fieldname: "budget_against",
label: __("Budget Against"),
fieldtype: "Select",
options: ["Cost Center", "Project"],
options: budget_against_options,
default: "Cost Center",
reqd: 1,
get_data: function() {
console.log(this.options);
return ["Emacs", "Rocks"];
},
on_change: function() {
frappe.query_report.set_filter_value("budget_against_filter", []);
frappe.query_report.refresh();
@ -71,24 +112,8 @@ frappe.query_reports["Budget Variance Report"] = {
fieldtype: "Check",
default: 0,
},
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
]
if (column.fieldname.includes(__("variance"))) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
return filters;
}
erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
});

View File

@ -3,7 +3,7 @@
import frappe
from frappe import _, scrub
from frappe import _, qb, scrub
from frappe.utils import getdate, nowdate
@ -21,8 +21,8 @@ class PartyLedgerSummaryReport(object):
frappe.throw(_("From Date must be before To Date"))
self.filters.party_type = args.get("party_type")
self.party_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1]
self.party_naming_by = frappe.db.get_single_value(
args.get("naming_by")[0], args.get("naming_by")[1]
)
self.get_gl_entries()
@ -38,7 +38,6 @@ class PartyLedgerSummaryReport(object):
"""
Additional Columns for 'User Permission' based access control
"""
from frappe import qb
if self.filters.party_type == "Customer":
self.territories = frappe._dict({})
@ -365,13 +364,29 @@ class PartyLedgerSummaryReport(object):
def get_party_adjustment_amounts(self):
conditions = self.prepare_conditions()
income_or_expense = (
"Expense Account" if self.filters.party_type == "Customer" else "Income Account"
account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
income_or_expense_accounts = frappe.db.get_all(
"Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
)
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
gl = qb.DocType("GL Entry")
if not income_or_expense_accounts:
# prevent empty 'in' condition
income_or_expense_accounts.append("")
accounts_query = (
qb.from_(gl)
.select(gl.voucher_type, gl.voucher_no)
.where(
(gl.account.isin(income_or_expense_accounts))
& (gl.posting_date.gte(self.filters.from_date))
& (gl.posting_date.lte(self.filters.to_date))
)
)
gl_entries = frappe.db.sql(
"""
select
@ -381,16 +396,15 @@ class PartyLedgerSummaryReport(object):
where
docstatus < 2 and is_cancelled = 0
and (voucher_type, voucher_no) in (
select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc
where acc.name = gle.account and acc.account_type = '{income_or_expense}'
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2
{accounts_query}
) and (voucher_type, voucher_no) in (
select voucher_type, voucher_no from `tabGL Entry` gle
where gle.party_type=%(party_type)s and ifnull(party, '') != ''
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions}
)
""".format(
conditions=conditions, income_or_expense=income_or_expense
""".format(
accounts_query=accounts_query,
conditions=conditions,
),
self.filters,
as_dict=True,
@ -414,7 +428,7 @@ class PartyLedgerSummaryReport(object):
elif gle.party:
parties.setdefault(gle.party, 0)
parties[gle.party] += gle.get(reverse_dr_or_cr) - gle.get(invoice_dr_or_cr)
elif frappe.get_cached_value("Account", gle.account, "account_type") == income_or_expense:
elif frappe.get_cached_value("Account", gle.account, "account_type") == account_type:
accounts.setdefault(gle.account, 0)
accounts[gle.account] += gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
else:

View File

@ -211,7 +211,13 @@ def get_data(
ignore_accumulated_values_for_fy,
)
accumulate_values_into_parents(accounts, accounts_by_name, period_list)
out = prepare_data(accounts, balance_must_be, period_list, company_currency)
out = prepare_data(
accounts,
balance_must_be,
period_list,
company_currency,
accumulated_values=filters.accumulated_values,
)
out = filter_out_zero_value_rows(out, parent_children_map)
if out and total:
@ -270,7 +276,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, period_list):
) + d.get("opening_balance", 0.0)
def prepare_data(accounts, balance_must_be, period_list, company_currency):
def prepare_data(accounts, balance_must_be, period_list, company_currency, accumulated_values):
data = []
year_start_date = period_list[0]["year_start_date"].strftime("%Y-%m-%d")
year_end_date = period_list[-1]["year_end_date"].strftime("%Y-%m-%d")
@ -310,8 +316,14 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency):
has_value = True
total += flt(row[period.key])
row["has_value"] = has_value
row["total"] = total
if accumulated_values:
# when 'accumulated_values' is enabled, periods have running balance.
# so, last period will have the net amount.
row["has_value"] = has_value
row["total"] = flt(d.get(period_list[-1].key, 0.0), 3)
else:
row["has_value"] = has_value
row["total"] = total
data.append(row)
return data

View File

@ -52,6 +52,11 @@ frappe.query_reports["General Ledger"] = {
frappe.query_report.set_filter_value('group_by', "Group by Voucher (Consolidated)");
}
},
{
"fieldname":"against_voucher_no",
"label": __("Against Voucher No"),
"fieldtype": "Data",
},
{
"fieldtype": "Break",
},

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,12 @@ def get_conditions(filters):
if filters.get("voucher_no"):
conditions.append("voucher_no=%(voucher_no)s")
if filters.get("against_voucher_no"):
conditions.append("against_voucher=%(against_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 +398,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 +612,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

@ -66,11 +66,11 @@ def execute(filters=None):
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
report_summary = get_report_summary(
report_summary, primitive_summary = get_report_summary(
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
)
return columns, data, None, chart, report_summary
return columns, data, None, chart, report_summary, primitive_summary
def get_report_summary(
@ -82,14 +82,25 @@ def get_report_summary(
if filters.get("accumulated_in_group_company"):
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
for period in period_list:
key = period if consolidated else period.key
if filters.accumulated_values:
# when 'accumulated_values' is enabled, periods have running balance.
# so, last period will have the net amount.
key = period_list[-1].key
if income:
net_income += income[-2].get(key)
net_income = income[-2].get(key)
if expense:
net_expense += expense[-2].get(key)
net_expense = expense[-2].get(key)
if net_profit_loss:
net_profit += net_profit_loss.get(key)
net_profit = net_profit_loss.get(key)
else:
for period in period_list:
key = period if consolidated else period.key
if income:
net_income += income[-2].get(key)
if expense:
net_expense += expense[-2].get(key)
if net_profit_loss:
net_profit += net_profit_loss.get(key)
if len(period_list) == 1 and periodicity == "Yearly":
profit_label = _("Profit This Year")
@ -112,7 +123,7 @@ def get_report_summary(
"datatype": "Currency",
"currency": currency,
},
]
], net_profit
def get_net_profit_loss(income, expense, period_list, company, currency=None, consolidated=False):

View File

@ -0,0 +1,94 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.financial_statements import get_period_list
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
def tearDown(self):
frappe.db.rollback()
def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=rate,
price_list_rate=rate,
qty=qty,
do_not_save=1,
)
si = si.save()
if not do_not_submit:
si = si.submit()
return si
def get_fiscal_year(self):
active_fy = frappe.db.get_all(
"Fiscal Year",
filters={"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())},
)[0]
return frappe.get_doc("Fiscal Year", active_fy.name)
def get_report_filters(self):
fy = self.get_fiscal_year()
return frappe._dict(
company=self.company,
from_fiscal_year=fy.name,
to_fiscal_year=fy.name,
period_start_date=fy.year_start_date,
period_end_date=fy.year_end_date,
filter_based_on="Fiscal Year",
periodicity="Monthly",
accumulated_vallues=True,
)
def test_profit_and_loss_output_and_summary(self):
si = self.create_sales_invoice(qty=1, rate=150)
filters = self.get_report_filters()
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
filters.period_start_date,
filters.period_end_date,
filters.filter_based_on,
filters.periodicity,
company=filters.company,
)
result = execute(filters)[1]
current_period = [x for x in period_list if x.from_date <= getdate() and x.to_date >= getdate()][
0
]
current_period_key = current_period.key
without_current_period = [x for x in period_list if x.key != current_period.key]
# all period except current period(whence invoice was posted), should be '0'
for acc in result:
if acc:
with self.subTest(acc=acc):
for period in without_current_period:
self.assertEqual(acc[period.key], 0)
for acc in result:
if acc:
with self.subTest(current_period_key=current_period_key):
self.assertEqual(acc[current_period_key], 150)
self.assertEqual(acc["total"], 150)

View File

@ -117,8 +117,3 @@ frappe.query_reports["Profitability Analysis"] = {
"parent_field": "parent_account",
"initial_depth": 3
}
erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]);
});

View File

@ -251,6 +251,7 @@ def get_journal_entries(filters, args):
)
.where(
(je.voucher_type == "Journal Entry")
& (je.docstatus == 1)
& (journal_account.party == filters.get(args.party))
& (journal_account.account.isin(args.party_account))
)
@ -281,7 +282,9 @@ def get_payment_entries(filters, args):
pe.cost_center,
)
.where(
(pe.party == filters.get(args.party)) & (pe[args.account_fieldname].isin(args.party_account))
(pe.docstatus == 1)
& (pe.party == filters.get(args.party))
& (pe[args.account_fieldname].isin(args.party_account))
)
.orderby(pe.posting_date, pe.name, order=Order.desc)
)

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
@ -1115,12 +1116,16 @@ def get_companies():
@frappe.whitelist()
def get_children(doctype, parent, company, is_root=False):
def get_children(doctype, parent, company, is_root=False, include_disabled=False):
if isinstance(include_disabled, str):
include_disabled = frappe.json.loads(include_disabled)
from erpnext.accounts.report.financial_statements import sort_accounts
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
fields = ["name as value", "is_group as expandable"]
filters = [["docstatus", "<", 2]]
if frappe.db.has_column(doctype, "disabled") and not include_disabled:
filters.append(["disabled", "=", False])
filters.append(['ifnull(`{0}`,"")'.format(parent_fieldname), "=", "" if is_root else parent])
@ -1275,7 +1280,7 @@ def parse_naming_series_variable(doc, variable):
else:
date = getdate()
company = None
return get_fiscal_year(date=date, company=company)[0]
return get_fiscal_year(date=date, company=company).name
@frappe.whitelist()

View File

@ -5,7 +5,7 @@
"label": "Profit and Loss"
}
],
"content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Payment\",\"col\":3}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"iAwpe-Chra\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Accounting\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Payment\",\"col\":3}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"iAwpe-Chra\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Accounting\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Banking\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 15:41:59.515192",
"custom_blocks": [],
"docstatus": 0,
@ -652,14 +652,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Bank Statement",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -1059,9 +1051,83 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Banking",
"link_count": 6,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank",
"link_count": 0,
"link_to": "Bank",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Account",
"link_count": 0,
"link_to": "Bank Account",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Clearance",
"link_count": 0,
"link_to": "Bank Clearance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Reconciliation Tool",
"link_count": 0,
"link_to": "Bank Reconciliation Tool",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Bank Reconciliation Statement",
"link_count": 0,
"link_to": "Bank Reconciliation Statement",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Plaid Settings",
"link_count": 0,
"link_to": "Plaid Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2023-08-10 17:41:14.059005",
"modified": "2024-01-02 15:21:09.895531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",

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",
@ -200,9 +202,9 @@
"fieldname": "purchase_date",
"fieldtype": "Date",
"label": "Purchase Date",
"mandatory_depends_on": "eval:!doc.is_existing_asset",
"read_only": 1,
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
"reqd": 1
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
},
{
"fieldname": "disposal_date",
@ -225,15 +227,15 @@
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"label": "Gross Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency",
"read_only_depends_on": "eval:!doc.is_existing_asset",
"reqd": 1
"read_only_depends_on": "eval:!doc.is_existing_asset"
},
{
"fieldname": "available_for_use_date",
"fieldtype": "Date",
"label": "Available-for-use Date",
"reqd": 1
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)"
},
{
"default": "0",
@ -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": "2024-01-05 17:36:53.131512",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@ -50,13 +50,14 @@ 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
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
asset_owner_company: DF.Link | None
asset_quantity: DF.Int
available_for_use_date: DF.Date
available_for_use_date: DF.Date | None
booked_fixed_asset: DF.Check
calculate_depreciation: DF.Check
capitalized_in: DF.Link | None
@ -91,7 +92,7 @@ class Asset(AccountsController):
number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency
policy_number: DF.Data | None
purchase_date: DF.Date
purchase_date: DF.Date | None
purchase_invoice: DF.Link | None
purchase_receipt: DF.Link | None
purchase_receipt_amount: DF.Currency
@ -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):
@ -313,7 +316,12 @@ class Asset(AccountsController):
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category):
if not self.is_existing_asset and not self.purchase_receipt and not self.purchase_invoice:
if (
not self.is_existing_asset
and not self.is_composite_asset
and not self.purchase_receipt
and not self.purchase_invoice
):
frappe.throw(
_("Please create purchase receipt or purchase invoice for the item {0}").format(
self.item_code
@ -326,7 +334,7 @@ class Asset(AccountsController):
and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock")
):
frappe.throw(
_("Update stock must be enable for the purchase invoice {0}").format(self.purchase_invoice)
_("Update stock must be enabled for the purchase invoice {0}").format(self.purchase_invoice)
)
if not self.calculate_depreciation:
@ -689,7 +697,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 +714,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

@ -35,7 +35,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
def post_depreciation_entries(date=None):
# Return if automatic booking of asset depreciation is disabled
if not cint(
frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically")
frappe.db.get_single_value("Accounts Settings", "book_asset_depreciation_entry_automatically")
):
return

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

@ -3,6 +3,7 @@
import frappe
from frappe.model.document import Document
from frappe.utils import now_datetime
class AssetActivity(Document):
@ -30,5 +31,6 @@ def add_asset_activity(asset, subject):
"asset": asset,
"subject": subject,
"user": frappe.session.user,
"date": now_datetime(),
}
).insert(ignore_permissions=True, ignore_links=True)

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

@ -86,12 +86,12 @@ class AssetCategory(Document):
if selected_key_type not in expected_key_types:
frappe.throw(
_(
"Row #{}: {} of {} should be {}. Please modify the account or select a different account."
"Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account."
).format(
d.idx,
frappe.unscrub(key_to_match),
frappe.bold(selected_account),
frappe.bold(expected_key_types),
frappe.bold(" or ".join(expected_key_types)),
),
title=_("Invalid Account"),
)

View File

@ -9,6 +9,7 @@
"field_order": [
"asset",
"naming_series",
"company",
"column_break_2",
"gross_purchase_amount",
"opening_accumulated_depreciation",
@ -193,12 +194,20 @@
"fieldtype": "Check",
"label": "Depreciate based on shifts",
"read_only": 1
},
{
"fetch_from": "asset.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-11-29 00:57:00.461998",
"modified": "2024-01-08 16:31:04.533928",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",

View File

@ -35,6 +35,7 @@ class AssetDepreciationSchedule(Document):
amended_from: DF.Link | None
asset: DF.Link
company: DF.Link | None
daily_prorata_based: DF.Check
depreciation_method: DF.Literal[
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"

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 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-11-28 13:01:18.403492",
"modified": "2024-01-05 15:26:02.320942",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@ -238,6 +238,41 @@
"role": "Purchase Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts User",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Stock Manager",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Stock User",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Purchase User",
"share": 1
}
],
"sort_field": "modified",

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

@ -452,6 +452,7 @@ class PurchaseOrder(BuyingController):
self.update_requested_qty()
self.update_ordered_qty()
self.update_reserved_qty_for_subcontract()
self.update_subcontracting_order_status()
self.notify_update()
clear_doctype_notifications(self)
@ -627,6 +628,17 @@ class PurchaseOrder(BuyingController):
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
make_subcontracting_order(self.name, save=True, notify=True)
def update_subcontracting_order_status(self):
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
update_subcontracting_order_status as update_sco_status,
)
if self.is_subcontracted and not self.is_old_subcontracting_flow:
sco = frappe.db.get_value("Subcontracting Order", {"purchase_order": self.name, "docstatus": 1})
if sco:
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item"""

View File

@ -29,6 +29,8 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
class TestPurchaseOrder(FrappeTestCase):
def test_purchase_order_qty(self):
po = create_purchase_order(qty=1, do_not_save=True)
# NonNegativeError with qty=-1
po.append(
"items",
{
@ -39,9 +41,15 @@ class TestPurchaseOrder(FrappeTestCase):
)
self.assertRaises(frappe.NonNegativeError, po.save)
# InvalidQtyError with qty=0
po.items[1].qty = 0
self.assertRaises(InvalidQtyError, po.save)
# No error with qty=1
po.items[1].qty = 1
po.save()
self.assertEqual(po.items[1].qty, 1)
def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
@ -1108,7 +1116,7 @@ def create_purchase_order(**args):
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"from_warehouse": args.from_warehouse,
"qty": args.qty or 10,
"qty": args.qty if args.qty is not None else 10,
"rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1),

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

@ -65,6 +65,7 @@ class RequestforQuotation(BuyingController):
def validate(self):
self.validate_duplicate_supplier()
self.validate_supplier_list()
super(RequestforQuotation, self).validate_qty_is_not_zero()
validate_for_items(self)
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
self.update_email_id()
@ -357,8 +358,8 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=
target_doc.currency = args.currency or get_party_account_currency(
"Supplier", for_supplier, source.company
)
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value(
"Buying Settings", None, "buying_price_list"
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_single_value(
"Buying Settings", "buying_price_list"
)
set_missing_values(source, target_doc)
@ -398,7 +399,7 @@ def create_supplier_quotation(doc):
"currency": doc.get("currency")
or get_party_account_currency("Supplier", doc.get("supplier"), doc.get("company")),
"buying_price_list": doc.get("buying_price_list")
or frappe.db.get_value("Buying Settings", None, "buying_price_list"),
or frappe.db.get_single_value("Buying Settings", "buying_price_list"),
}
)
add_items(sq_doc, doc.get("supplier"), doc.get("items"))

View File

@ -14,6 +14,7 @@ from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
get_pdf,
make_supplier_quotation_from_rfq,
)
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
from erpnext.stock.doctype.item.test_item import make_item
@ -21,6 +22,16 @@ from erpnext.templates.pages.rfq import check_supplier_has_docname_access
class TestRequestforQuotation(FrappeTestCase):
def test_rfq_qty(self):
rfq = make_request_for_quotation(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
rfq.save()
# No error with qty=1
rfq.items[0].qty = 1
rfq.save()
self.assertEqual(rfq.items[0].qty, 1)
def test_quote_status(self):
rfq = make_request_for_quotation()
@ -161,14 +172,17 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
"description": "_Test Item",
"uom": args.uom or "_Test UOM",
"stock_uom": args.stock_uom or "_Test UOM",
"qty": args.qty or 5,
"qty": args.qty if args.qty is not None else 5,
"conversion_factor": args.conversion_factor or 1.0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"schedule_date": nowdate(),
},
)
rfq.submit()
if not args.do_not_save:
rfq.insert()
if not args.do_not_submit:
rfq.submit()
return rfq

View File

@ -5,8 +5,21 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.accounts_controller import InvalidQtyError
class TestPurchaseOrder(FrappeTestCase):
def test_supplier_quotation_qty(self):
sq = frappe.copy_doc(test_records[0])
sq.items[0].qty = 0
with self.assertRaises(InvalidQtyError):
sq.save()
# No error with qty=1
sq.items[0].qty = 1
sq.save()
self.assertEqual(sq.items[0].qty, 1)
def test_make_purchase_order(self):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order

View File

@ -44,11 +44,6 @@ def update_last_purchase_rate(doc, is_submit) -> None:
def validate_for_items(doc) -> None:
items = []
for d in doc.get("items"):
if not d.qty:
if doc.doctype == "Purchase Receipt" and d.rejected_qty:
continue
frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
set_stock_levels(row=d) # update with latest quantities
item = validate_item_and_get_basic_data(row=d)
validate_stock_item_warehouse(row=d, item=item)

View File

@ -129,6 +129,17 @@ class AccountsController(TransactionBase):
if self.doctype in relevant_docs:
self.set_payment_schedule()
def remove_bundle_for_non_stock_invoices(self):
has_sabb = False
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.update_stock:
for item in self.get("items"):
if item.serial_and_batch_bundle:
item.serial_and_batch_bundle = None
has_sabb = True
if has_sabb:
self.remove_serial_and_batch_bundle()
def ensure_supplier_is_not_blocked(self):
is_supplier_payment = self.doctype == "Payment Entry" and self.party_type == "Supplier"
is_buying_invoice = self.doctype in ["Purchase Invoice", "Purchase Order"]
@ -156,6 +167,9 @@ class AccountsController(TransactionBase):
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
if self.get("_action") == "submit":
self.remove_bundle_for_non_stock_invoices()
self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year()
@ -225,6 +239,11 @@ class AccountsController(TransactionBase):
apply_pricing_rule_on_transaction(self)
self.set_total_in_words()
self.set_default_letter_head()
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head")
def init_internal_values(self):
# init all the internal values as 0 on sa
@ -556,18 +575,12 @@ class AccountsController(TransactionBase):
validate_due_date(
self.posting_date,
self.due_date,
"Customer",
self.customer,
self.company,
self.payment_terms_template,
)
elif self.doctype == "Purchase Invoice":
validate_due_date(
self.bill_date or self.posting_date,
self.due_date,
"Supplier",
self.supplier,
self.company,
self.bill_date,
self.payment_terms_template,
)
@ -874,6 +887,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(),
}
)
@ -916,7 +930,7 @@ class AccountsController(TransactionBase):
# Update details in transaction currency
gl_dict.update(
{
"transaction_currency": self.get("currency") or self.company_currency,
"transaction_currency": args.get("currency") or self.get("currency") or self.company_currency,
"transaction_exchange_rate": self.get("conversion_rate", 1),
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
account_currency, args, "debit"
@ -927,22 +941,49 @@ class AccountsController(TransactionBase):
}
)
if not args.get("against_voucher_type") and self.get("against_voucher_type"):
gl_dict.update({"against_voucher_type": self.get("against_voucher_type")})
if not args.get("against_voucher") and self.get("against_voucher"):
gl_dict.update({"against_voucher": self.get("against_voucher")})
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"):
if account_currency == args.get("currency") or self.get("currency"):
return args.get(field + "_in_account_currency")
else:
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
return flt(args.get(field, 0) / (args.get("conversion_rate") or self.get("conversion_rate", 1)))
def validate_qty_is_not_zero(self):
if self.doctype == "Purchase Receipt":
return
for item in self.items:
if self.doctype == "Purchase Receipt" and item.rejected_qty:
continue
if not flt(item.qty):
frappe.throw(
msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx),
msg=_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
item.idx, frappe.bold(item.item_code)
),
title=_("Invalid Quantity"),
exc=InvalidQtyError,
)
@ -1120,6 +1161,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 +1169,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
@ -1481,11 +1525,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:
@ -1510,7 +1556,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")
),
@ -1528,7 +1576,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")
),
@ -1551,7 +1601,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),
},
@ -2400,6 +2452,7 @@ def validate_taxes_and_charges(tax):
def validate_account_head(idx, account, company, context=""):
account_company = frappe.get_cached_value("Account", account, "company")
is_group = frappe.get_cached_value("Account", account, "is_group")
if account_company != company:
frappe.throw(
@ -2409,6 +2462,12 @@ def validate_account_head(idx, account, company, context=""):
title=_("Invalid Account"),
)
if is_group:
frappe.throw(
_("Row {0}: Account {1} is a Group Account").format(idx, frappe.bold(account)),
title=_("Invalid Account"),
)
def validate_cost_center(tax, doc):
if not tax.cost_center:
@ -3045,7 +3104,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
def validate_quantity(child_item, new_data):
if not flt(new_data.get("qty")):
frappe.throw(
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
),
title=_("Invalid Qty"),

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,7 +8,9 @@ from frappe.model.meta import get_field_precision
from frappe.utils import flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.utils import get_incoming_rate
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, get_valuation_method
class StockOverReturnError(frappe.ValidationError):
@ -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"
@ -116,33 +116,18 @@ def validate_returned_items(doc):
ref = valid_items.get(d.item_code, frappe._dict())
validate_quantity(doc, d, ref, valid_items, already_returned_items)
if ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") and flt(d.rate) > ref.rate:
if (
ref.rate
and flt(d.rate) > ref.rate
and doc.doctype in ("Delivery Note", "Sales Invoice")
and get_valuation_method(ref.item_code) != "Moving Average"
):
frappe.throw(
_("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format(
d.idx, doc.doctype, doc.return_against
)
)
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 +382,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 +567,18 @@ def make_return_doc(
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
if source_doc.item_code:
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 +734,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 +744,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):
@ -295,9 +295,6 @@ class SellingController(StockController):
def get_item_list(self):
il = []
for d in self.get("items"):
if d.qty is None:
frappe.throw(_("Row {0}: Qty is mandatory").format(d.idx))
if self.has_product_bundle(d.item_code):
for p in self.get("packed_items"):
if p.parent_detail_docname == d.name and p.parent_item == d.item_code:
@ -308,6 +305,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 +329,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 +429,18 @@ class SellingController(StockController):
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not self.get("return_against"):
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
continue
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,

Some files were not shown because too many files have changed in this diff Show More