Merge branch 'develop' into quotation-to-customer

This commit is contained in:
barredterra 2024-01-11 14:04:09 +01:00
commit e67ed4fb2d
375 changed files with 1496039 additions and 573505 deletions

View File

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

View File

@ -15,7 +15,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
version: ["13", "14", "15"] version: ["14", "15"]
steps: steps:
- uses: octokit/request-action@v2.x - 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: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1 rev: v4.3.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
files: "erpnext.*" files: "erpnext.*"
@ -15,6 +15,10 @@ repos:
args: ['--branch', 'develop'] args: ['--branch', 'develop']
- id: check-merge-conflict - id: check-merge-conflict
- id: check-ast - id: check-ast
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/pre-commit/mirrors-eslint - repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0 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) account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice": if doc.doctype == "Sales Invoice":
against_type = "Customer"
against, project = doc.customer, doc.project against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account credit_account, debit_account = item.income_account, item.deferred_revenue_account
else: else:
against_type = "Supplier"
against, project = doc.supplier, item.project against, project = doc.supplier, item.project
credit_account, debit_account = item.deferred_expense_account, item.expense_account 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, doc,
credit_account, credit_account,
debit_account, debit_account,
against_type,
against, against,
amount, amount,
base_amount, base_amount,
@ -494,6 +497,7 @@ def make_gl_entries(
doc, doc,
credit_account, credit_account,
debit_account, debit_account,
against_type,
against, against,
amount, amount,
base_amount, base_amount,
@ -515,7 +519,9 @@ def make_gl_entries(
doc.get_gl_dict( doc.get_gl_dict(
{ {
"account": credit_account, "account": credit_account,
"against_type": against_type,
"against": against, "against": against,
"against_link": against,
"credit": base_amount, "credit": base_amount,
"credit_in_account_currency": amount, "credit_in_account_currency": amount,
"cost_center": cost_center, "cost_center": cost_center,
@ -534,7 +540,9 @@ def make_gl_entries(
doc.get_gl_dict( doc.get_gl_dict(
{ {
"account": debit_account, "account": debit_account,
"against_type": against_type,
"against": against, "against": against,
"against_link": against,
"debit": base_amount, "debit": base_amount,
"debit_in_account_currency": amount, "debit_in_account_currency": amount,
"cost_center": cost_center, "cost_center": cost_center,

View File

@ -91,8 +91,8 @@ class Account(NestedSet):
super(Account, self).on_update() super(Account, self).on_update()
def onload(self): def onload(self):
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier" "Accounts Settings", "frozen_accounts_modifier"
) )
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles(): if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
self.set_onload("can_freeze_account", True) 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 // show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance; 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); const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) { if (account.balance!==undefined) {

View File

@ -74,7 +74,7 @@ def create_charts(
# after all accounts are already inserted. # after all accounts are already inserted.
frappe.local.flags.ignore_update_nsm = True frappe.local.flags.ignore_update_nsm = True
_import_accounts(chart, None, None, root_account=True) _import_accounts(chart, None, None, root_account=True)
rebuild_tree("Account", "parent_account") rebuild_tree("Account")
frappe.local.flags.ignore_update_nsm = False 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 tree[child.account_name]["account_type"] = child.account_type
if child.tax_rate: if child.tax_rate:
tree[child.account_name]["tax_rate"] = 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: if not parent:
tree[child.account_name]["root_type"] = child.root_type tree[child.account_name]["root_type"] = child.root_type

View File

@ -77,7 +77,6 @@
"1500 Fertige Erzeugnisse": {"account_type": "Stock"}, "1500 Fertige Erzeugnisse": {"account_type": "Stock"},
"1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"}, "1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"},
"1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"}, "1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"},
"1900 Wertberichtigungen": {"account_type": "Stock"},
"1800 Geleistete Anzahlungen": {"account_type": "Stock"}, "1800 Geleistete Anzahlungen": {"account_type": "Stock"},
"1900 Wertberichtigungen": {"account_type": "Stock"}, "1900 Wertberichtigungen": {"account_type": "Stock"},
"root_type": "Asset" "root_type": "Asset"

View File

@ -33,7 +33,9 @@
}, },
"Stocks": { "Stocks": {
"Mati\u00e8res premi\u00e8res": {}, "Mati\u00e8res premi\u00e8res": {},
"Stock de produits fini": {}, "Stock de produits fini": {
"account_type": "Stock"
},
"Stock exp\u00e9di\u00e9 non-factur\u00e9": {}, "Stock exp\u00e9di\u00e9 non-factur\u00e9": {},
"Travaux en cours": {}, "Travaux en cours": {},
"account_type": "Stock" "account_type": "Stock"
@ -397,7 +399,9 @@
"Revenus de ventes": { "Revenus de ventes": {
"Escomptes de volume sur ventes": {}, "Escomptes de volume sur ventes": {},
"Autres produits d'exploitation": {}, "Autres produits d'exploitation": {},
"Ventes": {}, "Ventes": {
"account_type": "Income Account"
},
"Ventes avec des provinces harmonis\u00e9es": {}, "Ventes avec des provinces harmonis\u00e9es": {},
"Ventes avec des provinces non-harmonis\u00e9es": {}, "Ventes avec des provinces non-harmonis\u00e9es": {},
"Ventes \u00e0 l'\u00e9tranger": {} "Ventes \u00e0 l'\u00e9tranger": {}

View File

@ -53,8 +53,13 @@
}, },
"II. Forderungen und sonstige Vermögensgegenstände": { "II. Forderungen und sonstige Vermögensgegenstände": {
"is_group": 1, "is_group": 1,
"Ford. a. Lieferungen und Leistungen": { "Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1400", "account_number": "1400",
"account_type": "Receivable",
"is_group": 1
},
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
"account_number": "1410",
"account_type": "Receivable" "account_type": "Receivable"
}, },
"Durchlaufende Posten": { "Durchlaufende Posten": {
@ -180,8 +185,13 @@
}, },
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": { "IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
"is_group": 1, "is_group": 1,
"Verbindlichkeiten aus Lieferungen u. Leistungen": { "Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1600", "account_number": "1600",
"account_type": "Payable",
"is_group": 1
},
"Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
"account_number": "1610",
"account_type": "Payable" "account_type": "Payable"
} }
}, },

View File

@ -11,6 +11,7 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
@ -19,7 +20,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-01 12:32:34.044911", "modified": "2024-01-03 11:13:02.669632",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Allowed To Transact With", "name": "Allowed To Transact With",
@ -28,5 +29,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "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", "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
args: { args: {
bank_account: frm.doc.bank_account, 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) => { callback: (response) => {
frm.set_value("account_opening_balance", response.message); 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) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers) transaction.add_payment_entries(vouchers)
transaction.validate_duplicate_references()
transaction.allocate_payment_entries()
transaction.update_allocated_amount()
transaction.set_status()
transaction.save() transaction.save()
return transaction return transaction

View File

@ -3,12 +3,11 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document
from frappe.utils import flt from frappe.utils import flt
from erpnext.controllers.status_updater import StatusUpdater
class BankTransaction(Document):
class BankTransaction(StatusUpdater):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # This code is auto-generated. Do not modify anything in this block.
@ -50,6 +49,15 @@ class BankTransaction(StatusUpdater):
def validate(self): def validate(self):
self.validate_duplicate_references() 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): def validate_duplicate_references(self):
"""Make sure the same voucher is not allocated twice within the same Bank Transaction""" """Make sure the same voucher is not allocated twice within the same Bank Transaction"""
if not self.payment_entries: if not self.payment_entries:
@ -83,12 +91,13 @@ class BankTransaction(StatusUpdater):
self.validate_duplicate_references() self.validate_duplicate_references()
self.allocate_payment_entries() self.allocate_payment_entries()
self.update_allocated_amount() self.update_allocated_amount()
self.set_status()
def on_cancel(self): def on_cancel(self):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel=True) self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.set_status(update=True) self.set_status()
def add_payment_entries(self, vouchers): def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance" "Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
@ -366,15 +375,17 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
and len(get_reconciled_bank_transactions(doctype, docname)) < 2 and len(get_reconciled_bank_transactions(doctype, docname)) < 2
): ):
return return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Sales Invoice": if doctype == "Sales Invoice":
frappe.db.set_value( frappe.db.set_value(
"Sales Invoice Payment", "Sales Invoice Payment",
dict(parenttype=doctype, parent=docname), dict(parenttype=doctype, parent=docname),
"clearance_date", "clearance_date",
clearance_date, clearance_date,
) )
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Bank Transaction": elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund # 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, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 0,
"creation": "2018-06-18 16:51:49.994750", "creation": "2018-06-18 16:51:49.994750",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"naming_series",
"user",
"date",
"from_time",
"time",
"expense",
"custody",
"returns",
"outstanding_amount",
"payments",
"net_amount",
"amended_from"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "POS-CLO-", "default": "POS-CLO-",
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Series", "label": "Series",
"length": 0,
"no_copy": 0,
"options": "POS-CLO-", "options": "POS-CLO-",
"permlevel": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user", "fieldname": "user",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "User", "label": "User",
"length": 0,
"no_copy": 0,
"options": "User", "options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today", "default": "Today",
"fieldname": "date", "fieldname": "date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Date", "label": "Date",
"length": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_time", "fieldname": "from_time",
"fieldtype": "Time", "fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "From Time", "label": "From Time",
"length": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "time", "fieldname": "time",
"fieldtype": "Time", "fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "To Time", "label": "To Time",
"length": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00", "default": "0.00",
"fieldname": "expense", "fieldname": "expense",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0, "label": "Expense"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00", "default": "0.00",
"fieldname": "custody", "fieldname": "custody",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0, "label": "Custody"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00", "default": "0.00",
"fieldname": "returns", "fieldname": "returns",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Returns", "label": "Returns",
"length": 0, "precision": "2"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00", "default": "0.00",
"fieldname": "outstanding_amount", "fieldname": "outstanding_amount",
"fieldtype": "Float", "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", "label": "Outstanding Amount",
"length": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.0",
"fieldname": "payments", "fieldname": "payments",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payments", "label": "Payments",
"length": 0, "options": "Cashier Closing Payments"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "net_amount", "fieldname": "net_amount",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Net Amount", "label": "Net Amount",
"length": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "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", "label": "Amended From",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Cashier Closing", "options": "Cashier Closing",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "links": [],
"istable": 0, "modified": "2023-12-28 13:15:46.858427",
"max_attachments": 0,
"modified": "2019-02-19 08:35:24.157327",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Cashier Closing", "name": "Cashier Closing",
"name_case": "", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "states": [],
"track_seen": 0, "track_changes": 1
"track_views": 0
} }

View File

@ -17,10 +17,13 @@
"account_currency", "account_currency",
"debit_in_account_currency", "debit_in_account_currency",
"credit_in_account_currency", "credit_in_account_currency",
"against_type",
"against", "against",
"against_link",
"against_voucher_type", "against_voucher_type",
"against_voucher", "against_voucher",
"voucher_type", "voucher_type",
"voucher_subtype",
"voucher_no", "voucher_no",
"voucher_detail_no", "voucher_detail_no",
"project", "project",
@ -128,6 +131,13 @@
"label": "Credit Amount in Account Currency", "label": "Credit Amount in Account Currency",
"options": "account_currency" "options": "account_currency"
}, },
{
"fieldname": "against_type",
"fieldtype": "Link",
"in_filter": 1,
"label": "Against Type",
"options": "DocType"
},
{ {
"fieldname": "against", "fieldname": "against",
"fieldtype": "Text", "fieldtype": "Text",
@ -136,14 +146,20 @@
"oldfieldname": "against", "oldfieldname": "against",
"oldfieldtype": "Text" "oldfieldtype": "Text"
}, },
{
"fieldname": "against_link",
"fieldtype": "Dynamic Link",
"in_filter": 1,
"label": "Against",
"options": "against_type"
},
{ {
"fieldname": "against_voucher_type", "fieldname": "against_voucher_type",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Against Voucher Type", "label": "Against Voucher Type",
"oldfieldname": "against_voucher_type", "oldfieldname": "against_voucher_type",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "DocType", "options": "DocType"
"search_index": 1
}, },
{ {
"fieldname": "against_voucher", "fieldname": "against_voucher",
@ -162,8 +178,7 @@
"label": "Voucher Type", "label": "Voucher Type",
"oldfieldname": "voucher_type", "oldfieldname": "voucher_type",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "DocType", "options": "DocType"
"search_index": 1
}, },
{ {
"fieldname": "voucher_no", "fieldname": "voucher_no",
@ -280,13 +295,18 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Credit Amount in Transaction Currency", "label": "Credit Amount in Transaction Currency",
"options": "transaction_currency" "options": "transaction_currency"
},
{
"fieldname": "voucher_subtype",
"fieldtype": "Small Text",
"label": "Voucher Subtype"
} }
], ],
"icon": "fa fa-list", "icon": "fa fa-list",
"idx": 1, "idx": 1,
"in_create": 1, "in_create": 1,
"links": [], "links": [],
"modified": "2023-08-16 21:38:44.072267", "modified": "2023-12-18 15:38:14.006208",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "GL Entry", "name": "GL Entry",

View File

@ -39,6 +39,8 @@ class GLEntry(Document):
account: DF.Link | None account: DF.Link | None
account_currency: DF.Link | None account_currency: DF.Link | None
against: DF.Text | None against: DF.Text | None
against_link: DF.DynamicLink | None
against_type: DF.Link | None
against_voucher: DF.DynamicLink | None against_voucher: DF.DynamicLink | None
against_voucher_type: DF.Link | None against_voucher_type: DF.Link | None
company: DF.Link | None company: DF.Link | None
@ -66,6 +68,7 @@ class GLEntry(Document):
transaction_exchange_rate: DF.Float transaction_exchange_rate: DF.Float
voucher_detail_no: DF.Data | None voucher_detail_no: DF.Data | None
voucher_no: DF.DynamicLink | None voucher_no: DF.DynamicLink | None
voucher_subtype: DF.SmallText | None
voucher_type: DF.Link | None voucher_type: DF.Link | None
# end: auto-generated types # end: auto-generated types
@ -432,8 +435,8 @@ def update_outstanding_amt(
def validate_frozen_account(account, adv_adj=None): def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account") frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == "Yes" and not adv_adj: if frozen_account == "Yes" and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", None, "frozen_accounts_modifier" "Accounts Settings", "frozen_accounts_modifier"
) )
if not frozen_accounts_modifier: if not frozen_accounts_modifier:

View File

@ -153,7 +153,9 @@ class InvoiceDiscounting(AccountsController):
"account": inv.debit_to, "account": inv.debit_to,
"party_type": "Customer", "party_type": "Customer",
"party": d.customer, "party": d.customer,
"against_type": "Account",
"against": self.accounts_receivable_credit, "against": self.accounts_receivable_credit,
"against_link": self.accounts_receivable_credit,
"credit": outstanding_in_company_currency, "credit": outstanding_in_company_currency,
"credit_in_account_currency": outstanding_in_company_currency "credit_in_account_currency": outstanding_in_company_currency
if inv.party_account_currency == company_currency if inv.party_account_currency == company_currency
@ -173,7 +175,9 @@ class InvoiceDiscounting(AccountsController):
"account": self.accounts_receivable_credit, "account": self.accounts_receivable_credit,
"party_type": "Customer", "party_type": "Customer",
"party": d.customer, "party": d.customer,
"against_type": "Account",
"against": inv.debit_to, "against": inv.debit_to,
"against_link": inv.debit_to,
"debit": outstanding_in_company_currency, "debit": outstanding_in_company_currency,
"debit_in_account_currency": outstanding_in_company_currency "debit_in_account_currency": outstanding_in_company_currency
if ar_credit_account_currency == 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); 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) { me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
@ -591,6 +601,21 @@ $.extend(erpnext.journal_entry, {
return { filters: filters }; 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() { reverse_journal_entry: function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_reverse_journal_entry", 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"), "account": tax_withholding_details.get("account_head"),
rev_debit_or_credit: tax_withholding_details.get("tax_amount"), rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
"against_account": parties[0], "against_account": parties[0],
"against_account_link": parties[0],
}, },
) )
@ -750,27 +751,90 @@ class JournalEntry(AccountsController):
) )
def set_against_account(self): def set_against_account(self):
accounts_debited, accounts_credited = [], []
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"): if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
for d in self.get("accounts"): for d in self.get("accounts"):
if d.reference_type == "Sales Invoice": if d.reference_type == "Sales Invoice":
field = "customer" against_type = "Customer"
else: 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: else:
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()
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"): for d in self.get("accounts"):
if flt(d.debit) > 0: if flt(d.debit) > 0:
accounts_debited.append(d.party or d.account) self.accounts_debited.append(d)
if flt(d.credit) > 0: elif flt(d.credit) > 0:
accounts_credited.append(d.party or d.account) self.accounts_credited.append(d)
for d in self.get("accounts"): 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: if flt(d.debit) > 0:
d.against_account = ", ".join(list(set(accounts_credited))) against_account = self.accounts_credited[0]
if flt(d.credit) > 0: elif flt(d.credit) > 0:
d.against_account = ", ".join(list(set(accounts_debited))) 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): def validate_debit_credit_amount(self):
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
@ -967,20 +1031,23 @@ class JournalEntry(AccountsController):
def build_gl_map(self): def build_gl_map(self):
gl_map = [] 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"): for d in self.get("accounts"):
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"): if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark] r = [d.user_remark, self.remark]
r = [x for x in r if x] r = [x for x in r if x]
remarks = "\n".join(r) remarks = "\n".join(r)
gl_map.append( gl_dict = self.get_gl_dict(
self.get_gl_dict(
{ {
"account": d.account, "account": d.account,
"party_type": d.party_type, "party_type": d.party_type,
"due_date": self.due_date, "due_date": self.due_date,
"party": d.party, "party": d.party,
"against": d.against_account,
"debit": flt(d.debit, d.precision("debit")), "debit": flt(d.debit, d.precision("debit")),
"credit": flt(d.credit, d.precision("credit")), "credit": flt(d.credit, d.precision("credit")),
"account_currency": d.account_currency, "account_currency": d.account_currency,
@ -997,12 +1064,75 @@ class JournalEntry(AccountsController):
"cost_center": d.cost_center, "cost_center": d.cost_center,
"project": d.project, "project": d.project,
"finance_book": self.finance_book, "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, 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 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): def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries 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 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

@ -30,7 +30,6 @@
"voucher_type": "Bank Entry" "voucher_type": "Bank Entry"
}, },
{ {
"cheque_date": "2013-02-14", "cheque_date": "2013-02-14",
"cheque_no": "33", "cheque_no": "33",
@ -62,7 +61,6 @@
"voucher_type": "Bank Entry" "voucher_type": "Bank Entry"
}, },
{ {
"cheque_date": "2013-02-14", "cheque_date": "2013-02-14",
"cheque_no": "33", "cheque_no": "33",
@ -81,7 +79,6 @@
}, },
{ {
"account": "Sales - _TC", "account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC",
"credit_in_account_currency": 400.0, "credit_in_account_currency": 400.0,
"debit_in_account_currency": 0.0, "debit_in_account_currency": 0.0,
"doctype": "Journal Entry Account", "doctype": "Journal Entry Account",

View File

@ -37,7 +37,9 @@
"col_break3", "col_break3",
"is_advance", "is_advance",
"user_remark", "user_remark",
"against_account" "against_type",
"against_account",
"against_account_link"
], ],
"fields": [ "fields": [
{ {
@ -259,6 +261,13 @@
"oldfieldtype": "Text", "oldfieldtype": "Text",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "against_account_link",
"fieldtype": "Dynamic Link",
"label": "Against Account",
"no_copy": 1,
"options": "against_type"
},
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
@ -280,14 +289,19 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Reference Detail No", "label": "Reference Detail No",
"no_copy": 1, "no_copy": 1
"search_index": 1 },
{
"fieldname": "against_type",
"fieldtype": "Link",
"label": "Against Type",
"options": "DocType"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-23 11:44:25.841187", "modified": "2023-12-02 23:21:22.205409",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",

View File

@ -747,6 +747,10 @@ frappe.ui.form.on('Payment Entry', {
args["get_orders_to_be_billed"] = true; 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']; frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({ return frappe.call({

View File

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

View File

@ -50,6 +50,88 @@ class InvalidPaymentEntry(ValidationError):
class PaymentEntry(AccountsController): 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): def __init__(self, *args, **kwargs):
super(PaymentEntry, self).__init__(*args, **kwargs) super(PaymentEntry, self).__init__(*args, **kwargs)
if not self.is_new(): if not self.is_new():
@ -256,6 +338,7 @@ class PaymentEntry(AccountsController):
"get_outstanding_invoices": True, "get_outstanding_invoices": True,
"get_orders_to_be_billed": True, "get_orders_to_be_billed": True,
"vouchers": vouchers, "vouchers": vouchers,
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
}, },
validate=True, validate=True,
) )
@ -1061,7 +1144,9 @@ class PaymentEntry(AccountsController):
"account": self.party_account, "account": self.party_account,
"party_type": self.party_type, "party_type": self.party_type,
"party": self.party, "party": self.party,
"against_type": "Account",
"against": against_account, "against": against_account,
"against_link": against_account,
"account_currency": self.party_account_currency, "account_currency": self.party_account_currency,
"cost_center": self.cost_center, "cost_center": self.cost_center,
}, },
@ -1226,7 +1311,9 @@ class PaymentEntry(AccountsController):
{ {
"account": self.paid_from, "account": self.paid_from,
"account_currency": self.paid_from_account_currency, "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": 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_in_account_currency": self.paid_amount,
"credit": self.base_paid_amount, "credit": self.base_paid_amount,
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -1241,7 +1328,9 @@ class PaymentEntry(AccountsController):
{ {
"account": self.paid_to, "account": self.paid_to,
"account_currency": self.paid_to_account_currency, "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": 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_in_account_currency": self.received_amount,
"debit": self.base_received_amount, "debit": self.base_received_amount,
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -1265,6 +1354,7 @@ class PaymentEntry(AccountsController):
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_to against = self.party or self.paid_to
against_type = self.party_type or "Account"
payment_account = self.get_party_account_for_taxes() payment_account = self.get_party_account_for_taxes()
tax_amount = d.tax_amount tax_amount = d.tax_amount
base_tax_amount = d.base_tax_amount base_tax_amount = d.base_tax_amount
@ -1273,7 +1363,9 @@ class PaymentEntry(AccountsController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": d.account_head, "account": d.account_head,
"against_type": against_type,
"against": against, "against": against,
"against_link": against,
dr_or_cr: tax_amount, dr_or_cr: tax_amount,
dr_or_cr + "_in_account_currency": base_tax_amount dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency == self.company_currency if account_currency == self.company_currency
@ -1298,7 +1390,9 @@ class PaymentEntry(AccountsController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": payment_account, "account": payment_account,
"against_type": against_type,
"against": against, "against": against,
"against_link": against,
rev_dr_or_cr: tax_amount, rev_dr_or_cr: tax_amount,
rev_dr_or_cr + "_in_account_currency": base_tax_amount rev_dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency == self.company_currency if account_currency == self.company_currency
@ -1323,7 +1417,9 @@ class PaymentEntry(AccountsController):
{ {
"account": d.account, "account": d.account,
"account_currency": account_currency, "account_currency": account_currency,
"against_type": self.party_type or "Account",
"against": self.party or self.paid_from, "against": self.party or self.paid_from,
"against_link": self.party or self.paid_from,
"debit_in_account_currency": d.amount, "debit_in_account_currency": d.amount,
"debit": d.amount, "debit": d.amount,
"cost_center": d.cost_center, "cost_center": d.cost_center,
@ -1615,11 +1711,16 @@ def get_outstanding_reference_documents(args, validate=False):
outstanding_invoices = [] outstanding_invoices = []
negative_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"): if args.get("get_outstanding_invoices"):
outstanding_invoices = get_outstanding_invoices( outstanding_invoices = get_outstanding_invoices(
args.get("party_type"), args.get("party_type"),
args.get("party"), args.get("party"),
get_party_account(args.get("party_type"), args.get("party"), args.get("company")), party_account,
common_filter=common_filter, common_filter=common_filter,
posting_date=posting_and_due_date, posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"), min_outstanding=args.get("outstanding_amt_greater_than"),

View File

@ -594,6 +594,27 @@ class PaymentReconciliation(Document):
invoice_exchange_map.update(purchase_invoice_map) invoice_exchange_map.update(purchase_invoice_map)
journals = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
]
journals.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
)
if journals:
journals = list(set(journals))
journals_map = frappe._dict(
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
fields=[
"parent as `name`",
"exchange_rate",
],
as_list=1,
)
)
invoice_exchange_map.update(journals_map)
return invoice_exchange_map return invoice_exchange_map
def validate_allocation(self): def validate_allocation(self):

View File

@ -34,4 +34,6 @@ class PaymentReconciliationAllocation(Document):
unreconciled_amount: DF.Currency unreconciled_amount: DF.Currency
# end: auto-generated types # end: auto-generated types
@staticmethod
def get_list(args):
pass pass

View File

@ -26,4 +26,6 @@ class PaymentReconciliationInvoice(Document):
parenttype: DF.Data parenttype: DF.Data
# end: auto-generated types # end: auto-generated types
@staticmethod
def get_list(args):
pass pass

View File

@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document):
remark: DF.SmallText | None remark: DF.SmallText | None
# end: auto-generated types # end: auto-generated types
@staticmethod
def get_list(args):
pass pass

View File

@ -765,7 +765,7 @@ def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = ( reserved_qty = (
frappe.qb.from_(p_inv) frappe.qb.from_(p_inv)
.from_(p_item) .from_(p_item)
.select(Sum(p_item.qty).as_("qty")) .select(Sum(p_item.stock_qty).as_("stock_qty"))
.where( .where(
(p_inv.name == p_item.parent) (p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "") & (IfNull(p_inv.consolidated_invoice, "") == "")
@ -775,7 +775,7 @@ def get_pos_reserved_qty(item_code, warehouse):
) )
).run(as_dict=True) ).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() @frappe.whitelist()

View File

@ -527,7 +527,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
values.extend(warehouses) values.extend(warehouses)
if items: 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)) 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 # Update the parent doc about the exception
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback(with_context=True)
if traceback: if traceback:
message = "Traceback: <br>" + traceback message = "Traceback: <br>" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message) frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)

View File

@ -15,6 +15,7 @@
"group_by", "group_by",
"cost_center", "cost_center",
"territory", "territory",
"ignore_exchange_rate_revaluation_journals",
"column_break_14", "column_break_14",
"to_date", "to_date",
"finance_book", "finance_book",
@ -376,10 +377,16 @@
"fieldname": "pdf_name", "fieldname": "pdf_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "PDF Name" "label": "PDF Name"
},
{
"default": "0",
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
} }
], ],
"links": [], "links": [],
"modified": "2023-08-28 12:59:53.071334", "modified": "2023-12-18 12:20:08.965120",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Statement Of Accounts", "name": "Process Statement Of Accounts",

View File

@ -56,6 +56,7 @@ class ProcessStatementOfAccounts(Document):
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"] frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
from_date: DF.Date | None from_date: DF.Date | None
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"] group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
ignore_exchange_rate_revaluation_journals: DF.Check
include_ageing: DF.Check include_ageing: DF.Check
include_break: DF.Check include_break: DF.Check
letter_head: DF.Link | None letter_head: DF.Link | None
@ -119,6 +120,18 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {} statement_dict = {}
ageing = "" 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: for entry in doc.customers:
if doc.include_ageing: if doc.include_ageing:
ageing = set_ageing(doc, entry) ageing = set_ageing(doc, entry)
@ -131,6 +144,8 @@ def get_statement_dict(doc, get_statement_dict=False):
) )
filters = get_common_filters(doc) 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": if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) 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(); super.onload();
// Ignore linked advances // 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"]; 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"];
if(!this.frm.doc.__islocal) { if(!this.frm.doc.__islocal) {
// show credit_to in print format // show credit_to in print format
@ -163,6 +163,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
} }
}) })
}, __("Get Items From")); }, __("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); 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() { on_submit() {
super.on_submit();
$.each(this.frm.doc["items"] || [], function(i, row) { $.each(this.frm.doc["items"] || [], function(i, row) {
if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt) if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt)
}) })

View File

@ -296,6 +296,18 @@ class PurchaseInvoice(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
self.set_percentage_received()
def set_percentage_received(self):
total_billed_qty = 0.0
total_received_qty = 0.0
for row in self.items:
if row.purchase_receipt and row.pr_detail and row.received_qty:
total_billed_qty += row.qty
total_received_qty += row.received_qty
if total_billed_qty and total_received_qty:
self.per_received = total_received_qty / total_billed_qty * 100
def validate_release_date(self): def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date): if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@ -552,7 +564,7 @@ class PurchaseInvoice(BuyingController):
self.against_expense_account = ",".join(against_accounts) self.against_expense_account = ",".join(against_accounts)
def po_required(self): 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( if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
@ -572,7 +584,7 @@ class PurchaseInvoice(BuyingController):
def pr_required(self): def pr_required(self):
stock_items = self.get_stock_items() 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( if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt" "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
@ -815,7 +827,9 @@ class PurchaseInvoice(BuyingController):
"party_type": "Supplier", "party_type": "Supplier",
"party": self.supplier, "party": self.supplier,
"due_date": self.due_date, "due_date": self.due_date,
"against_type": "Account",
"against": self.against_expense_account, "against": self.against_expense_account,
"against_link": self.against_expense_account,
"credit": base_grand_total, "credit": base_grand_total,
"credit_in_account_currency": base_grand_total "credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency if self.party_account_currency == self.company_currency
@ -888,7 +902,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": warehouse_account[item.warehouse]["account"], "account": warehouse_account[item.warehouse]["account"],
"against_type": "Account",
"against": warehouse_account[item.from_warehouse]["account"], "against": warehouse_account[item.from_warehouse]["account"],
"against_link": warehouse_account[item.from_warehouse]["account"],
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project or self.project, "project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
@ -908,7 +924,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": warehouse_account[item.from_warehouse]["account"], "account": warehouse_account[item.from_warehouse]["account"],
"against_type": "Account",
"against": warehouse_account[item.warehouse]["account"], "against": warehouse_account[item.warehouse]["account"],
"against_link": warehouse_account[item.warehouse]["account"],
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project or self.project, "project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
@ -925,7 +943,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": item.expense_account, "account": item.expense_account,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")), "debit": flt(item.base_net_amount, item.precision("base_net_amount")),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center, "cost_center": item.cost_center,
@ -942,7 +962,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": item.expense_account, "account": item.expense_account,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"debit": warehouse_debit_amount, "debit": warehouse_debit_amount,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center, "cost_center": item.cost_center,
@ -961,7 +983,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": account, "account": account,
"against_type": "Account",
"against": item.expense_account, "against": item.expense_account,
"against_link": item.expense_account,
"cost_center": item.cost_center, "cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]), "credit": flt(amount["base_amount"]),
@ -981,7 +1005,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": supplier_warehouse_account, "account": supplier_warehouse_account,
"against_type": "Account",
"against": item.expense_account, "against": item.expense_account,
"against_link": item.expense_account,
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project or self.project, "project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
@ -1036,7 +1062,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": expense_account, "account": expense_account,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"debit": amount, "debit": amount,
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project or self.project, "project": item.project or self.project,
@ -1062,7 +1090,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": expense_account, "account": expense_account,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"debit": discrepancy_caused_by_exchange_rate_difference, "debit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project or self.project, "project": item.project or self.project,
@ -1075,7 +1105,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.get_company_default("exchange_gain_loss_account"), "account": self.get_company_default("exchange_gain_loss_account"),
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"credit": discrepancy_caused_by_exchange_rate_difference, "credit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project or self.project, "project": item.project or self.project,
@ -1084,17 +1116,6 @@ class PurchaseInvoice(BuyingController):
item=item, item=item,
) )
) )
# update gross amount of asset bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
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)
)
if ( if (
self.auto_accounting_for_stock self.auto_accounting_for_stock
and self.is_opening == "No" and self.is_opening == "No"
@ -1120,7 +1141,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": stock_rbnb, "account": stock_rbnb,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"remarks": self.remarks or _("Accounting Entry for Stock"), "remarks": self.remarks or _("Accounting Entry for Stock"),
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -1134,12 +1157,25 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount") item.item_tax_amount, item.precision("item_tax_amount")
) )
if item.is_fixed_asset and item.landed_cost_voucher_amount:
self.update_gross_purchase_amount_for_linked_assets(item)
def update_gross_purchase_amount_for_linked_assets(self, item):
assets = frappe.db.get_all( 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: for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) frappe.db.set_value(
"Asset",
asset.name,
{
"gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
},
)
def make_stock_adjustment_entry( def make_stock_adjustment_entry(
self, gl_entries, item, voucher_wise_stock_value, account_currency self, gl_entries, item, voucher_wise_stock_value, account_currency
@ -1168,7 +1204,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": cost_of_goods_sold_account, "account": cost_of_goods_sold_account,
"against_type": "Account",
"against": item.expense_account, "against": item.expense_account,
"against_link": item.expense_account,
"debit": stock_adjustment_amt, "debit": stock_adjustment_amt,
"remarks": self.get("remarks") or _("Stock Adjustment"), "remarks": self.get("remarks") or _("Stock Adjustment"),
"cost_center": item.cost_center, "cost_center": item.cost_center,
@ -1198,7 +1236,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": tax.account_head, "account": tax.account_head,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
dr_or_cr: base_amount, dr_or_cr: base_amount,
dr_or_cr + "_in_account_currency": base_amount dr_or_cr + "_in_account_currency": base_amount
if account_currency == self.company_currency if account_currency == self.company_currency
@ -1246,8 +1286,10 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": tax.account_head, "account": tax.account_head,
"against_type": "Supplier",
"cost_center": tax.cost_center, "cost_center": tax.cost_center,
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"credit": applicable_amount, "credit": applicable_amount,
"remarks": self.remarks or _("Accounting Entry for Stock"), "remarks": self.remarks or _("Accounting Entry for Stock"),
}, },
@ -1265,7 +1307,9 @@ class PurchaseInvoice(BuyingController):
{ {
"account": tax.account_head, "account": tax.account_head,
"cost_center": tax.cost_center, "cost_center": tax.cost_center,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"credit": valuation_tax[tax.name], "credit": valuation_tax[tax.name],
"remarks": self.remarks or _("Accounting Entry for Stock"), "remarks": self.remarks or _("Accounting Entry for Stock"),
}, },
@ -1280,7 +1324,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.unrealized_profit_loss_account, "account": self.unrealized_profit_loss_account,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"credit": flt(self.total_taxes_and_charges), "credit": flt(self.total_taxes_and_charges),
"credit_in_account_currency": flt(self.base_total_taxes_and_charges), "credit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -1301,7 +1347,9 @@ class PurchaseInvoice(BuyingController):
"account": self.credit_to, "account": self.credit_to,
"party_type": "Supplier", "party_type": "Supplier",
"party": self.supplier, "party": self.supplier,
"against_type": "Account",
"against": self.cash_bank_account, "against": self.cash_bank_account,
"against_link": self.cash_bank_account,
"debit": self.base_paid_amount, "debit": self.base_paid_amount,
"debit_in_account_currency": self.base_paid_amount "debit_in_account_currency": self.base_paid_amount
if self.party_account_currency == self.company_currency if self.party_account_currency == self.company_currency
@ -1322,7 +1370,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.cash_bank_account, "account": self.cash_bank_account,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"credit": self.base_paid_amount, "credit": self.base_paid_amount,
"credit_in_account_currency": self.base_paid_amount "credit_in_account_currency": self.base_paid_amount
if bank_account_currency == self.company_currency if bank_account_currency == self.company_currency
@ -1346,7 +1396,9 @@ class PurchaseInvoice(BuyingController):
"account": self.credit_to, "account": self.credit_to,
"party_type": "Supplier", "party_type": "Supplier",
"party": self.supplier, "party": self.supplier,
"against_type": "Account",
"against": self.write_off_account, "against": self.write_off_account,
"against_link": self.write_off_account,
"debit": self.base_write_off_amount, "debit": self.base_write_off_amount,
"debit_in_account_currency": self.base_write_off_amount "debit_in_account_currency": self.base_write_off_amount
if self.party_account_currency == self.company_currency if self.party_account_currency == self.company_currency
@ -1366,7 +1418,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.write_off_account, "account": self.write_off_account,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"credit": flt(self.base_write_off_amount), "credit": flt(self.base_write_off_amount),
"credit_in_account_currency": self.base_write_off_amount "credit_in_account_currency": self.base_write_off_amount
if write_off_account_currency == self.company_currency if write_off_account_currency == self.company_currency
@ -1393,7 +1447,9 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": round_off_account, "account": round_off_account,
"against_type": "Supplier",
"against": self.supplier, "against": self.supplier,
"against_link": self.supplier,
"debit_in_account_currency": self.rounding_adjustment, "debit_in_account_currency": self.rounding_adjustment,
"debit": self.base_rounding_adjustment, "debit": self.base_rounding_adjustment,
"cost_center": round_off_cost_center "cost_center": round_off_cost_center
@ -1816,10 +1872,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc) 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() @frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None): def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent): 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.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order 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.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.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
@ -51,6 +51,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
def tearDown(self): def tearDown(self):
frappe.db.rollback() 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): def test_purchase_invoice_received_qty(self):
""" """
1. Test if received qty is validated against accepted + rejected 1. Test if received qty is validated against accepted + rejected
@ -1227,11 +1237,11 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self): def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value( unlink_enabled = frappe.db.get_single_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" "Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
) )
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value( frappe.db.set_value(
@ -1422,7 +1432,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pay.cancel() pay.cancel()
frappe.db.set_single_value( frappe.db.set_single_value(
"Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled "Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled
) )
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
@ -1985,6 +1995,26 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC") 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): def set_advance_flag(company, flag, default_account):
frappe.db.set_value( frappe.db.set_value(
@ -2094,7 +2124,7 @@ def make_purchase_invoice(**args):
bundle_id = None bundle_id = None
if args.get("batch_no") or args.get("serial_no"): if args.get("batch_no") or args.get("serial_no"):
batches = {} 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" item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"): if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty}) batches = frappe._dict({args.batch_no: qty})
@ -2121,8 +2151,9 @@ def make_purchase_invoice(**args):
"items", "items",
{ {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"item_name": args.item_name,
"warehouse": args.warehouse or "_Test Warehouse - _TC", "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, "received_qty": args.received_qty or 0,
"rejected_qty": args.rejected_qty or 0, "rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50, "rate": args.rate or 50,

View File

@ -288,7 +288,6 @@
"oldfieldname": "import_rate", "oldfieldname": "import_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency", "options": "currency",
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
"reqd": 1 "reqd": 1
}, },
{ {
@ -919,7 +918,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-30 16:26:05.629780", "modified": "2023-12-25 22:00:28.043555",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -126,7 +126,7 @@ class RepostAccountingLedger(Document):
return rendered_page return rendered_page
def on_submit(self): def on_submit(self):
if len(self.vouchers) > 1: if len(self.vouchers) > 5:
job_name = "repost_accounting_ledger_" + self.name job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue( frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
@ -170,8 +170,6 @@ def start_repost(account_repost_doc=str) -> None:
doc.make_gl_entries(1) doc.make_gl_entries(1)
doc.make_gl_entries() doc.make_gl_entries()
frappe.db.commit()
def get_allowed_types_from_settings(): def get_allowed_types_from_settings():
return [ return [

View File

@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.create_company() self.create_company()
self.create_customer() self.create_customer()
self.create_item() self.create_item()
self.update_repost_settings() update_repost_settings()
def teadDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def update_repost_settings(self):
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()
def test_01_basic_functions(self): def test_01_basic_functions(self):
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item=self.item,
@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
# Submit repost document # Submit repost document
ral.save().submit() ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
res = ( res = (
qb.from_(gl) qb.from_(gl)
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit")) .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
pe = get_payment_entry(si.doctype, si.name) pe = get_payment_entry(si.doctype, si.name)
pe.save().submit() pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save()
# assert preview data is generated
preview = ral.generate_preview()
self.assertIsNotNone(preview)
ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
# with deletion flag set # with deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger") ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company ral.company = self.company
@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit() ral.save().submit()
start_repost(ral.name)
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def test_05_without_deletion_flag(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
)
pe = get_payment_entry(si.doctype, si.name)
pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit()
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def update_repost_settings():
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()

View File

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

View File

@ -37,7 +37,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload(); super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 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"]; 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries",
'Serial and Batch Bundle'
];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format
@ -197,6 +199,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
on_submit(doc, dt, dn) { on_submit(doc, dt, dn) {
var me = this; var me = this;
super.on_submit();
if (frappe.get_route()[0] != 'Form') { if (frappe.get_route()[0] != 'Form') {
return return
} }
@ -895,8 +898,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.events.append_time_log(frm, timesheet, 1.0); frm.events.append_time_log(frm, timesheet, 1.0);
} }
}); });
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals"); frm.trigger("calculate_timesheet_totals");
frm.refresh();
}, },
async get_exchange_rate(frm, from_currency, to_currency) { async get_exchange_rate(frm, from_currency, to_currency) {

View File

@ -138,6 +138,7 @@
"loyalty_amount", "loyalty_amount",
"column_break_77", "column_break_77",
"loyalty_program", "loyalty_program",
"dont_create_loyalty_points",
"loyalty_redemption_account", "loyalty_redemption_account",
"loyalty_redemption_cost_center", "loyalty_redemption_cost_center",
"contact_and_address_tab", "contact_and_address_tab",
@ -1041,8 +1042,7 @@
"label": "Loyalty Program", "label": "Loyalty Program",
"no_copy": 1, "no_copy": 1,
"options": "Loyalty Program", "options": "Loyalty Program",
"print_hide": 1, "print_hide": 1
"read_only": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@ -2162,6 +2162,14 @@
"fieldname": "update_billed_amount_in_delivery_note", "fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note" "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", "icon": "fa fa-file-text",
@ -2174,7 +2182,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-11-23 16:56:29.679499", "modified": "2024-01-02 17:25:46.027523",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "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.contacts.doctype.address.address import get_address_display
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values 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 import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.deferred_revenue import validate_service_stop_date
@ -117,6 +117,7 @@ class SalesInvoice(SellingController):
discount_amount: DF.Currency discount_amount: DF.Currency
dispatch_address: DF.SmallText | None dispatch_address: DF.SmallText | None
dispatch_address_name: DF.Link | None dispatch_address_name: DF.Link | None
dont_create_loyalty_points: DF.Check
due_date: DF.Date | None due_date: DF.Date | None
from_date: DF.Date | None from_date: DF.Date | None
grand_total: DF.Currency grand_total: DF.Currency
@ -471,7 +472,12 @@ class SalesInvoice(SellingController):
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) 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 # 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() self.make_loyalty_point_entry()
elif ( elif (
self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program 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", "Serial and Batch Bundle",
) )
self.delete_auto_created_batches()
def update_status_updater_args(self): def update_status_updater_args(self):
if cint(self.update_stock): if cint(self.update_stock):
self.status_updater.append( self.status_updater.append(
@ -1225,7 +1233,9 @@ class SalesInvoice(SellingController):
"party_type": "Customer", "party_type": "Customer",
"party": self.customer, "party": self.customer,
"due_date": self.due_date, "due_date": self.due_date,
"against_type": "Account",
"against": self.against_income_account, "against": self.against_income_account,
"against_link": self.against_income_account,
"debit": base_grand_total, "debit": base_grand_total,
"debit_in_account_currency": base_grand_total "debit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency if self.party_account_currency == self.company_currency
@ -1254,7 +1264,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": tax.account_head, "account": tax.account_head,
"against_type": "Customer",
"against": self.customer, "against": self.customer,
"against_link": self.customer,
"credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")), "credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")),
"credit_in_account_currency": ( "credit_in_account_currency": (
flt(base_amount, tax.precision("base_tax_amount_after_discount_amount")) flt(base_amount, tax.precision("base_tax_amount_after_discount_amount"))
@ -1275,7 +1287,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.unrealized_profit_loss_account, "account": self.unrealized_profit_loss_account,
"against_type": "Customer",
"against": self.customer, "against": self.customer,
"against_link": self.customer,
"debit": flt(self.total_taxes_and_charges), "debit": flt(self.total_taxes_and_charges),
"debit_in_account_currency": flt(self.base_total_taxes_and_charges), "debit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -1343,7 +1357,9 @@ class SalesInvoice(SellingController):
add_asset_activity(asset.name, _("Asset sold")) add_asset_activity(asset.name, _("Asset sold"))
for gle in fixed_asset_gl_entries: for gle in fixed_asset_gl_entries:
gle["against_type"] = "Customer"
gle["against"] = self.customer gle["against"] = self.customer
gle["against_link"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item)) gl_entries.append(self.get_gl_dict(gle, item=item))
self.set_asset_status(asset) self.set_asset_status(asset)
@ -1364,7 +1380,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": income_account, "account": income_account,
"against_type": "Customer",
"against": self.customer, "against": self.customer,
"against_link": self.customer,
"credit": flt(base_amount, item.precision("base_net_amount")), "credit": flt(base_amount, item.precision("base_net_amount")),
"credit_in_account_currency": ( "credit_in_account_currency": (
flt(base_amount, item.precision("base_net_amount")) flt(base_amount, item.precision("base_net_amount"))
@ -1418,9 +1436,9 @@ class SalesInvoice(SellingController):
"account": self.debit_to, "account": self.debit_to,
"party_type": "Customer", "party_type": "Customer",
"party": self.customer, "party": self.customer,
"against": "Expense account - " "against_type": "Account",
+ cstr(self.loyalty_redemption_account) "against": self.loyalty_redemption_account,
+ " for the Loyalty Program", "against_link": self.loyalty_redemption_account,
"credit": self.loyalty_amount, "credit": self.loyalty_amount,
"against_voucher": self.return_against if cint(self.is_return) else self.name, "against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype, "against_voucher_type": self.doctype,
@ -1434,7 +1452,9 @@ class SalesInvoice(SellingController):
{ {
"account": self.loyalty_redemption_account, "account": self.loyalty_redemption_account,
"cost_center": self.cost_center or self.loyalty_redemption_cost_center, "cost_center": self.cost_center or self.loyalty_redemption_cost_center,
"against_type": "Customer",
"against": self.customer, "against": self.customer,
"against_link": self.customer,
"debit": self.loyalty_amount, "debit": self.loyalty_amount,
"remark": "Loyalty Points redeemed by the customer", "remark": "Loyalty Points redeemed by the customer",
}, },
@ -1461,7 +1481,9 @@ class SalesInvoice(SellingController):
"account": self.debit_to, "account": self.debit_to,
"party_type": "Customer", "party_type": "Customer",
"party": self.customer, "party": self.customer,
"against_type": "Account",
"against": payment_mode.account, "against": payment_mode.account,
"against_link": payment_mode.account,
"credit": payment_mode.base_amount, "credit": payment_mode.base_amount,
"credit_in_account_currency": payment_mode.base_amount "credit_in_account_currency": payment_mode.base_amount
if self.party_account_currency == self.company_currency if self.party_account_currency == self.company_currency
@ -1482,7 +1504,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": payment_mode.account, "account": payment_mode.account,
"against_type": "Customer",
"against": self.customer, "against": self.customer,
"against_link": self.customer,
"debit": payment_mode.base_amount, "debit": payment_mode.base_amount,
"debit_in_account_currency": payment_mode.base_amount "debit_in_account_currency": payment_mode.base_amount
if payment_mode_account_currency == self.company_currency if payment_mode_account_currency == self.company_currency
@ -1506,7 +1530,9 @@ class SalesInvoice(SellingController):
"account": self.debit_to, "account": self.debit_to,
"party_type": "Customer", "party_type": "Customer",
"party": self.customer, "party": self.customer,
"against_type": "Account",
"against": self.account_for_change_amount, "against": self.account_for_change_amount,
"against_link": self.account_for_change_amount,
"debit": flt(self.base_change_amount), "debit": flt(self.base_change_amount),
"debit_in_account_currency": flt(self.base_change_amount) "debit_in_account_currency": flt(self.base_change_amount)
if self.party_account_currency == self.company_currency if self.party_account_currency == self.company_currency
@ -1527,7 +1553,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.account_for_change_amount, "account": self.account_for_change_amount,
"against_type": "Customer",
"against": self.customer, "against": self.customer,
"against_link": self.customer,
"credit": self.base_change_amount, "credit": self.base_change_amount,
"cost_center": self.cost_center, "cost_center": self.cost_center,
}, },
@ -1553,7 +1581,9 @@ class SalesInvoice(SellingController):
"account": self.debit_to, "account": self.debit_to,
"party_type": "Customer", "party_type": "Customer",
"party": self.customer, "party": self.customer,
"against_type": "Account",
"against": self.write_off_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": flt(self.base_write_off_amount, self.precision("base_write_off_amount")),
"credit_in_account_currency": ( "credit_in_account_currency": (
flt(self.base_write_off_amount, self.precision("base_write_off_amount")) flt(self.base_write_off_amount, self.precision("base_write_off_amount"))
@ -1573,7 +1603,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.write_off_account, "account": self.write_off_account,
"against_type": "Customer",
"against": self.customer, "against": self.customer,
"against_link": self.customer,
"debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), "debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")),
"debit_in_account_currency": ( "debit_in_account_currency": (
flt(self.base_write_off_amount, self.precision("base_write_off_amount")) flt(self.base_write_off_amount, self.precision("base_write_off_amount"))
@ -1601,7 +1633,9 @@ class SalesInvoice(SellingController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": round_off_account, "account": round_off_account,
"against_type": "Customer",
"against": self.customer, "against": self.customer,
"against_link": self.customer,
"credit_in_account_currency": flt( "credit_in_account_currency": flt(
self.rounding_adjustment, self.precision("rounding_adjustment") 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): 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( target_doctypes = frappe.get_all(
doctype, doctype,
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1}, filters=filters,
as_list=True, as_list=True,
) )
@ -2540,10 +2583,6 @@ def get_loyalty_programs(customer):
return lp_details return lp_details
def on_doctype_update():
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
@frappe.whitelist() @frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None): def create_invoice_discounting(source_name, target_doc=None):
invoice = frappe.get_doc("Sales Invoice", source_name) 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 ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule, 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.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.selling.doctype.customer.test_customer import get_customer_dict from erpnext.selling.doctype.customer.test_customer import get_customer_dict
@ -72,6 +72,16 @@ class TestSalesInvoice(FrappeTestCase):
def tearDownClass(self): def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0) 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): def test_timestamp_change(self):
w = frappe.copy_doc(test_records[0]) w = frappe.copy_doc(test_records[0])
w.docstatus = 0 w.docstatus = 0
@ -1414,10 +1424,11 @@ class TestSalesInvoice(FrappeTestCase):
def test_serialized_cancel(self): def test_serialized_cancel(self):
si = self.test_serialized() si = self.test_serialized()
si.cancel() si.reload()
serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle) serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle)
si.cancel()
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC" frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
) )
@ -2793,6 +2804,12 @@ class TestSalesInvoice(FrappeTestCase):
@change_settings("Selling Settings", {"enable_discount_accounting": 1}) @change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
update_repost_settings,
)
update_repost_settings()
additional_discount_account = create_account( additional_discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
parent_account="Indirect Expenses - _TC", parent_account="Indirect Expenses - _TC",
@ -3629,7 +3646,7 @@ def create_sales_invoice(**args):
bundle_id = None bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")): if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
batches = {} 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" item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"): if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty}) batches = frappe._dict({args.batch_no: qty})
@ -3661,7 +3678,7 @@ def create_sales_invoice(**args):
"description": args.description or "_Test Item", "description": args.description or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"target_warehouse": args.target_warehouse, "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", "uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos", "stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100, "rate": args.rate if args.get("rate") is not None else 100,

View File

@ -81,6 +81,7 @@
"warehouse", "warehouse",
"target_warehouse", "target_warehouse",
"quality_inspection", "quality_inspection",
"pick_serial_and_batch",
"serial_and_batch_bundle", "serial_and_batch_bundle",
"batch_no", "batch_no",
"incoming_rate", "incoming_rate",
@ -897,12 +898,18 @@
"options": "Serial and Batch Bundle", "options": "Serial and Batch Bundle",
"print_hide": 1, "print_hide": 1,
"search_index": 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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-14 18:34:10.479329", "modified": "2023-12-29 13:03:14.121298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

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

View File

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

View File

@ -280,6 +280,7 @@ def check_if_in_list(gle, gl_map, dimensions=None):
"project", "project",
"finance_book", "finance_book",
"voucher_no", "voucher_no",
"against_link",
] ]
if dimensions: if dimensions:
@ -653,10 +654,10 @@ def check_freezing_date(posting_date, adv_adj=False):
Hence stop admin to bypass if accounts are freezed Hence stop admin to bypass if accounts are freezed
""" """
if not adv_adj: 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: if acc_frozen_upto:
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", None, "frozen_accounts_modifier" "Accounts Settings", "frozen_accounts_modifier"
) )
if getdate(posting_date) <= getdate(acc_frozen_upto) and ( if getdate(posting_date) <= getdate(acc_frozen_upto) and (
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator" 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) set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)
) )
party = party_details[party_type.lower()] 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) 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) currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details( 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 return due_date
def validate_due_date( def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
):
if getdate(due_date) < getdate(posting_date): if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date")) frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else: else:

View File

@ -55,8 +55,8 @@ class ReceivablePayableReport(object):
def run(self, args): def run(self, args):
self.filters.update(args) self.filters.update(args)
self.set_defaults() self.set_defaults()
self.party_naming_by = frappe.db.get_value( self.party_naming_by = frappe.db.get_single_value(
args.get("naming_by")[0], None, args.get("naming_by")[1] args.get("naming_by")[0], args.get("naming_by")[1]
) )
self.get_columns() self.get_columns()
self.get_data() self.get_data()
@ -225,7 +225,7 @@ class ReceivablePayableReport(object):
if not row: if not row:
return 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 amount = ple.amount_in_account_currency
else: else:
amount = ple.amount amount = ple.amount
@ -244,6 +244,10 @@ class ReceivablePayableReport(object):
row.invoiced_in_account_currency += amount_in_account_currency row.invoiced_in_account_currency += amount_in_account_currency
else: else:
if self.is_invoice(ple): if self.is_invoice(ple):
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 -= amount
row.credit_note_in_account_currency -= amount_in_account_currency row.credit_note_in_account_currency -= amount_in_account_currency
else: else:
@ -451,7 +455,7 @@ class ReceivablePayableReport(object):
party_details = self.get_party_details(row.party) or {} party_details = self.get_party_details(row.party) or {}
row.update(party_details) 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 row.currency = row.account_currency
else: else:
row.currency = self.company_currency row.currency = self.company_currency

View File

@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
return credit_note 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): def test_accounts_receivable(self):
filters = { filters = {
"company": self.company, "company": self.company,
@ -544,7 +579,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters.update({"party_account": self.debtors_usd}) filters.update({"party_account": self.debtors_usd})
report = execute(filters)[1] report = execute(filters)[1]
self.assertEqual(len(report), 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] row = report[0]
self.assertEqual( self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] 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): def run(self, args):
self.account_type = args.get("account_type") self.account_type = args.get("account_type")
self.party_type = get_party_types_from_account_type(self.account_type) self.party_type = get_party_types_from_account_type(self.account_type)
self.party_naming_by = frappe.db.get_value( self.party_naming_by = frappe.db.get_single_value(
args.get("naming_by")[0], None, args.get("naming_by")[1] args.get("naming_by")[0], args.get("naming_by")[1]
) )
self.get_columns() self.get_columns()
self.get_data(args) 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], "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"reqd": 1 "reqd": 1
}, },
{
"fieldname":"group_by",
"label": __("Group By"),
"fieldtype": "Select",
"options": ["Asset Category", "Asset"],
"default": "Asset Category",
},
{ {
"fieldname":"asset_category", "fieldname":"asset_category",
"label": __("Asset Category"), "label": __("Asset Category"),
"fieldtype": "Link", "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): 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 = [] data = []
asset_categories = get_asset_categories(filters) asset_categories = get_asset_categories_for_grouped_by_category(filters)
assets = get_assets(filters) assets = get_assets_for_grouped_by_category(filters)
for asset_category in asset_categories: for asset_category in asset_categories:
row = frappe._dict() row = frappe._dict()
@ -38,6 +45,7 @@ def get_data(filters):
if asset["asset_category"] == asset_category.get("asset_category", "") if asset["asset_category"] == asset_category.get("asset_category", "")
) )
) )
row.accumulated_depreciation_as_on_to_date = ( row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date) flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period) + flt(row.depreciation_amount_during_the_period)
@ -57,7 +65,7 @@ def get_data(filters):
return data return data
def get_asset_categories(filters): def get_asset_categories_for_grouped_by_category(filters):
condition = "" condition = ""
if filters.get("asset_category"): if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s" 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 = "" condition = ""
if filters.get("asset_category"): if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(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): def get_columns(filters):
return [ columns = []
if filters.get("group_by") == "Asset Category":
columns.append(
{ {
"label": _("Asset Category"), "label": _("Asset Category"),
"fieldname": "asset_category", "fieldname": "asset_category",
"fieldtype": "Link", "fieldtype": "Link",
"options": "Asset Category", "options": "Asset Category",
"width": 120, "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), "label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "cost_as_on_from_date", "fieldname": "cost_as_on_from_date",
@ -254,3 +438,5 @@ def get_columns(filters):
"width": 200, "width": 200,
}, },
] ]
return columns

View File

@ -97,11 +97,11 @@ def execute(filters=None):
chart = get_chart_data(filters, columns, asset, liability, equity) 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 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( def get_provisional_profit_loss(
@ -217,7 +217,7 @@ def get_report_summary(
"datatype": "Currency", "datatype": "Currency",
"currency": currency, "currency": currency,
}, },
] ], (net_asset - net_liability + net_equity)
def get_chart_data(filters, columns, asset, liability, 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 // License: GNU General Public License v3. See license.txt
frappe.query_reports["Budget Variance Report"] = { 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", fieldname: "from_fiscal_year",
label: __("From Fiscal Year"), label: __("From Fiscal Year"),
@ -44,9 +81,13 @@ frappe.query_reports["Budget Variance Report"] = {
fieldname: "budget_against", fieldname: "budget_against",
label: __("Budget Against"), label: __("Budget Against"),
fieldtype: "Select", fieldtype: "Select",
options: ["Cost Center", "Project"], options: budget_against_options,
default: "Cost Center", default: "Cost Center",
reqd: 1, reqd: 1,
get_data: function() {
console.log(this.options);
return ["Emacs", "Rocks"];
},
on_change: function() { on_change: function() {
frappe.query_report.set_filter_value("budget_against_filter", []); frappe.query_report.set_filter_value("budget_against_filter", []);
frappe.query_report.refresh(); frappe.query_report.refresh();
@ -71,24 +112,8 @@ frappe.query_reports["Budget Variance Report"] = {
fieldtype: "Check", fieldtype: "Check",
default: 0, default: 0,
}, },
], ]
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes(__("variance"))) { return filters;
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;
}
}
erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
});

View File

@ -128,7 +128,7 @@ frappe.query_reports["Consolidated Financial Statement"] = {
} }
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);
if (!data.parent_account) { if (data && !data.parent_account) {
value = $(`<span>${value}</span>`); value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold"); var $value = $(value).css("font-weight", "bold");

View File

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

View File

@ -211,7 +211,13 @@ def get_data(
ignore_accumulated_values_for_fy, ignore_accumulated_values_for_fy,
) )
accumulate_values_into_parents(accounts, accounts_by_name, period_list) 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) out = filter_out_zero_value_rows(out, parent_children_map)
if out and total: 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) ) + 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 = [] data = []
year_start_date = period_list[0]["year_start_date"].strftime("%Y-%m-%d") 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") year_end_date = period_list[-1]["year_end_date"].strftime("%Y-%m-%d")
@ -310,6 +316,12 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency):
has_value = True has_value = True
total += flt(row[period.key]) total += flt(row[period.key])
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["has_value"] = has_value
row["total"] = total row["total"] = total
data.append(row) data.append(row)

View File

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

View File

@ -200,10 +200,10 @@ def get_gl_entries(filters, accounting_dimensions):
""" """
select select
name as gl_entry, posting_date, account, party_type, party, 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} cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency, against_voucher_type, against_voucher, account_currency,
against, is_opening, creation {select_fields} against_link, against, is_opening, creation {select_fields}
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s {conditions} where company=%(company)s {conditions}
{order_by_statement} {order_by_statement}
@ -238,6 +238,12 @@ def get_conditions(filters):
if filters.get("voucher_no"): if filters.get("voucher_no"):
conditions.append("voucher_no=%(voucher_no)s") 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"): if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
conditions.append("party_type in ('Customer', 'Supplier')") 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")) group_by = group_by_field(filters.get("group_by"))
for gle in gl_entries: 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=[])) gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[]))
return gle_map return gle_map
@ -605,6 +612,12 @@ def get_columns(filters):
columns += [ columns += [
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120}, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
{
"label": _("Voucher Subtype"),
"fieldname": "voucher_subtype",
"fieldtype": "Data",
"width": 180,
},
{ {
"label": _("Voucher No"), "label": _("Voucher No"),
"fieldname": "voucher_no", "fieldname": "voucher_no",

View File

@ -66,11 +66,11 @@ def execute(filters=None):
currency = filters.presentation_currency or frappe.get_cached_value( currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency" "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 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( def get_report_summary(
@ -82,6 +82,17 @@ def get_report_summary(
if filters.get("accumulated_in_group_company"): if filters.get("accumulated_in_group_company"):
period_list = get_filtered_list_for_consolidated_report(filters, period_list) period_list = get_filtered_list_for_consolidated_report(filters, period_list)
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)
if expense:
net_expense = expense[-2].get(key)
if net_profit_loss:
net_profit = net_profit_loss.get(key)
else:
for period in period_list: for period in period_list:
key = period if consolidated else period.key key = period if consolidated else period.key
if income: if income:
@ -112,7 +123,7 @@ def get_report_summary(
"datatype": "Currency", "datatype": "Currency",
"currency": currency, "currency": currency,
}, },
] ], net_profit
def get_net_profit_loss(income, expense, period_list, company, currency=None, consolidated=False): 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", "parent_field": "parent_account",
"initial_depth": 3 "initial_depth": 3
} }
erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]);
});

View File

@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
"payable_account": inv.credit_to, "payable_account": inv.credit_to,
"mode_of_payment": inv.mode_of_payment, "mode_of_payment": inv.mode_of_payment,
"project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project, "project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
"bill_no": inv.bill_no,
"bill_date": inv.bill_date,
"remarks": inv.remarks, "remarks": inv.remarks,
"purchase_order": ", ".join(purchase_order), "purchase_order": ", ".join(purchase_order),
"purchase_receipt": ", ".join(purchase_receipt), "purchase_receipt": ", ".join(purchase_receipt),

View File

@ -345,21 +345,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
if filters.get("party"): if filters.get("party"):
party = [filters.get("party")] party = [filters.get("party")]
query = query.where( jv_condition = gle.against.isin(party) | (
((gle.account.isin(tds_accounts) & gle.against.isin(party))) (gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
| gle.party.isin(party)
) )
else: else:
party = frappe.get_all(filters.get("party_type"), pluck="name") party = frappe.get_all(filters.get("party_type"), pluck="name")
query = query.where( jv_condition = gle.against.isin(party) | (
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
| (
(gle.voucher_type == "Journal Entry") (gle.voucher_type == "Journal Entry")
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == "")) & ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
) )
| gle.party.isin(party) query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
)
return query return query

View File

@ -251,6 +251,7 @@ def get_journal_entries(filters, args):
) )
.where( .where(
(je.voucher_type == "Journal Entry") (je.voucher_type == "Journal Entry")
& (je.docstatus == 1)
& (journal_account.party == filters.get(args.party)) & (journal_account.party == filters.get(args.party))
& (journal_account.account.isin(args.party_account)) & (journal_account.account.isin(args.party_account))
) )
@ -281,7 +282,9 @@ def get_payment_entries(filters, args):
pe.cost_center, pe.cost_center,
) )
.where( .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) .orderby(pe.posting_date, pe.name, order=Order.desc)
) )
@ -365,7 +368,7 @@ def filter_invoices_based_on_dimensions(filters, query, parent_doc):
dimension.document_type, filters.get(dimension.fieldname) dimension.document_type, filters.get(dimension.fieldname)
) )
fieldname = dimension.fieldname fieldname = dimension.fieldname
query = query.where(parent_doc[fieldname] == filters.fieldname) query = query.where(parent_doc[fieldname].isin(filters[fieldname]))
return query return query

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.set("reference_name", d["against_voucher"])
new_row.against_account = cstr(jv_detail.against_account) 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.is_advance = cstr(jv_detail.is_advance)
new_row.docstatus = 1 new_row.docstatus = 1
@ -662,8 +663,10 @@ def update_reference_in_payment_entry(
"total_amount": d.grand_total, "total_amount": d.grand_total,
"outstanding_amount": d.outstanding_amount, "outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount, "allocated_amount": d.allocated_amount,
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), "exchange_rate": d.exchange_rate
"exchange_gain_loss": d.exchange_gain_loss, if d.difference_amount is not None
else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.difference_amount,
"account": d.account, "account": d.account,
} }
@ -1113,12 +1116,16 @@ def get_companies():
@frappe.whitelist() @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 from erpnext.accounts.report.financial_statements import sort_accounts
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
fields = ["name as value", "is_group as expandable"] fields = ["name as value", "is_group as expandable"]
filters = [["docstatus", "<", 2]] 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]) filters.append(['ifnull(`{0}`,"")'.format(parent_fieldname), "=", "" if is_root else parent])
@ -1273,7 +1280,7 @@ def parse_naming_series_variable(doc, variable):
else: else:
date = getdate() date = getdate()
company = None company = None
return get_fiscal_year(date=date, company=company)[0] return get_fiscal_year(date=date, company=company).name
@frappe.whitelist() @frappe.whitelist()

View File

@ -5,7 +5,7 @@
"label": "Profit and Loss" "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", "creation": "2020-03-02 15:41:59.515192",
"custom_blocks": [], "custom_blocks": [],
"docstatus": 0, "docstatus": 0,
@ -652,14 +652,6 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "Bank Statement",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{ {
"dependencies": "", "dependencies": "",
"hidden": 0, "hidden": 0,
@ -1059,9 +1051,83 @@
"link_type": "Report", "link_type": "Report",
"onboard": 0, "onboard": 0,
"type": "Link" "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", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",

View File

@ -571,10 +571,16 @@ frappe.ui.form.on('Asset', {
indicator: 'red' indicator: 'red'
}); });
} }
frm.set_value('gross_purchase_amount', item.base_net_rate + item.item_tax_amount); var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
frm.set_value('purchase_receipt_amount', item.base_net_rate + item.item_tax_amount); var asset_quantity = is_grouped_asset ? item.qty : 1;
item.asset_location && frm.set_value('location', item.asset_location); var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
frm.set_value('gross_purchase_amount', purchase_amount);
frm.set_value('purchase_receipt_amount', purchase_amount);
frm.set_value('asset_quantity', asset_quantity);
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center); frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
if(item.asset_location) { frm.set_value('location', item.asset_location); }
}, },
set_depreciation_rate: function(frm, row) { set_depreciation_rate: function(frm, row) {

View File

@ -35,6 +35,8 @@
"purchase_receipt", "purchase_receipt",
"purchase_invoice", "purchase_invoice",
"available_for_use_date", "available_for_use_date",
"total_asset_cost",
"additional_asset_cost",
"column_break_23", "column_break_23",
"gross_purchase_amount", "gross_purchase_amount",
"asset_quantity", "asset_quantity",
@ -200,9 +202,9 @@
"fieldname": "purchase_date", "fieldname": "purchase_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Purchase Date", "label": "Purchase Date",
"mandatory_depends_on": "eval:!doc.is_existing_asset",
"read_only": 1, "read_only": 1,
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
"reqd": 1
}, },
{ {
"fieldname": "disposal_date", "fieldname": "disposal_date",
@ -225,15 +227,15 @@
"fieldname": "gross_purchase_amount", "fieldname": "gross_purchase_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Gross Purchase Amount", "label": "Gross Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only_depends_on": "eval:!doc.is_existing_asset", "read_only_depends_on": "eval:!doc.is_existing_asset"
"reqd": 1
}, },
{ {
"fieldname": "available_for_use_date", "fieldname": "available_for_use_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Available-for-use Date", "label": "Available-for-use Date",
"reqd": 1 "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)"
}, },
{ {
"default": "0", "default": "0",
@ -529,6 +531,22 @@
"label": "Capitalized In", "label": "Capitalized In",
"options": "Asset Capitalization", "options": "Asset Capitalization",
"read_only": 1 "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, "idx": 72,
@ -572,7 +590,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2023-11-20 20:57:37.010467", "modified": "2024-01-05 17:36:53.131512",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -50,13 +50,14 @@ class Asset(AccountsController):
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
additional_asset_cost: DF.Currency
amended_from: DF.Link | None amended_from: DF.Link | None
asset_category: DF.Link | None asset_category: DF.Link | None
asset_name: DF.Data asset_name: DF.Data
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"] asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
asset_owner_company: DF.Link | None asset_owner_company: DF.Link | None
asset_quantity: DF.Int asset_quantity: DF.Int
available_for_use_date: DF.Date available_for_use_date: DF.Date | None
booked_fixed_asset: DF.Check booked_fixed_asset: DF.Check
calculate_depreciation: DF.Check calculate_depreciation: DF.Check
capitalized_in: DF.Link | None capitalized_in: DF.Link | None
@ -91,7 +92,7 @@ class Asset(AccountsController):
number_of_depreciations_booked: DF.Int number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency opening_accumulated_depreciation: DF.Currency
policy_number: DF.Data | None policy_number: DF.Data | None
purchase_date: DF.Date purchase_date: DF.Date | None
purchase_invoice: DF.Link | None purchase_invoice: DF.Link | None
purchase_receipt: DF.Link | None purchase_receipt: DF.Link | None
purchase_receipt_amount: DF.Currency purchase_receipt_amount: DF.Currency
@ -111,6 +112,7 @@ class Asset(AccountsController):
"Decapitalized", "Decapitalized",
] ]
supplier: DF.Link | None supplier: DF.Link | None
total_asset_cost: DF.Currency
total_number_of_depreciations: DF.Int total_number_of_depreciations: DF.Int
value_after_depreciation: DF.Currency value_after_depreciation: DF.Currency
# end: auto-generated types # end: auto-generated types
@ -144,6 +146,7 @@ class Asset(AccountsController):
).format(asset_depr_schedules_links) ).format(asset_depr_schedules_links)
) )
self.total_asset_cost = self.gross_purchase_amount
self.status = self.get_status() self.status = self.get_status()
def on_submit(self): def on_submit(self):
@ -313,7 +316,12 @@ class Asset(AccountsController):
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category): 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( frappe.throw(
_("Please create purchase receipt or purchase invoice for the item {0}").format( _("Please create purchase receipt or purchase invoice for the item {0}").format(
self.item_code self.item_code
@ -326,7 +334,7 @@ class Asset(AccountsController):
and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock") and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock")
): ):
frappe.throw( 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: if not self.calculate_depreciation:
@ -689,7 +697,9 @@ class Asset(AccountsController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": cwip_account, "account": cwip_account,
"against_type": "Account",
"against": fixed_asset_account, "against": fixed_asset_account,
"against_link": fixed_asset_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date, "posting_date": self.available_for_use_date,
"credit": self.purchase_receipt_amount, "credit": self.purchase_receipt_amount,
@ -704,7 +714,9 @@ class Asset(AccountsController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": fixed_asset_account, "account": fixed_asset_account,
"against_type": "Account",
"against": cwip_account, "against": cwip_account,
"against_link": cwip_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date, "posting_date": self.available_for_use_date,
"debit": self.purchase_receipt_amount, "debit": self.purchase_receipt_amount,

View File

@ -19,6 +19,7 @@ from frappe.utils import (
) )
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, get_checks_for_pl_and_bs_accounts,
) )
@ -35,7 +36,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
def post_depreciation_entries(date=None): def post_depreciation_entries(date=None):
# Return if automatic booking of asset depreciation is disabled # Return if automatic booking of asset depreciation is disabled
if not cint( 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 return
@ -522,6 +523,13 @@ def depreciate_asset(asset_doc, date, notes):
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
cancel_depreciation_entries(asset_doc, date)
@erpnext.allow_regional
def cancel_depreciation_entries(asset_doc, date):
pass
def reset_depreciation_schedule(asset_doc, date, notes): def reset_depreciation_schedule(asset_doc, date, notes):
if not asset_doc.calculate_depreciation: if not asset_doc.calculate_depreciation:

View File

@ -251,7 +251,16 @@ class TestAsset(AssetSetup):
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")), flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0, 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", "_Test Gain/Loss on Asset Disposal - _TC",
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")), flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),

View File

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

View File

@ -485,7 +485,9 @@ class AssetCapitalization(StockController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": account, "account": account,
"against_type": "Account",
"against": target_account, "against": target_account,
"against_link": target_account,
"cost_center": item_row.cost_center, "cost_center": item_row.cost_center,
"project": item_row.get("project") or self.get("project"), "project": item_row.get("project") or self.get("project"),
"remarks": self.get("remarks") or "Accounting Entry for Stock", "remarks": self.get("remarks") or "Accounting Entry for Stock",
@ -526,7 +528,9 @@ class AssetCapitalization(StockController):
self.set_consumed_asset_status(asset) self.set_consumed_asset_status(asset)
for gle in fixed_asset_gl_entries: for gle in fixed_asset_gl_entries:
gle["against_type"] = "Account"
gle["against"] = target_account gle["against"] = target_account
gle["against_link"] = target_account
gl_entries.append(self.get_gl_dict(gle, item=item)) gl_entries.append(self.get_gl_dict(gle, item=item))
target_against.add(gle["account"]) target_against.add(gle["account"])
@ -542,7 +546,9 @@ class AssetCapitalization(StockController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": item_row.expense_account, "account": item_row.expense_account,
"against_type": "Account",
"against": target_account, "against": target_account,
"against_link": target_account,
"cost_center": item_row.cost_center, "cost_center": item_row.cost_center,
"project": item_row.get("project") or self.get("project"), "project": item_row.get("project") or self.get("project"),
"remarks": self.get("remarks") or "Accounting Entry for Stock", "remarks": self.get("remarks") or "Accounting Entry for Stock",
@ -553,15 +559,18 @@ class AssetCapitalization(StockController):
) )
def get_gl_entries_for_target_item(self, gl_entries, target_against, precision): def get_gl_entries_for_target_item(self, gl_entries, target_against, precision):
for target_account in target_against:
if self.target_is_fixed_asset: if self.target_is_fixed_asset:
# Capitalization # Capitalization
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.target_fixed_asset_account, "account": self.target_fixed_asset_account,
"against": ", ".join(target_against), "against_type": "Account",
"against": target_account,
"against_link": target_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": flt(self.total_value, precision), "debit": flt(self.total_value, precision) / len(target_against),
"cost_center": self.get("cost_center"), "cost_center": self.get("cost_center"),
}, },
item=self, item=self,
@ -578,11 +587,13 @@ class AssetCapitalization(StockController):
self.get_gl_dict( self.get_gl_dict(
{ {
"account": account, "account": account,
"against": ", ".join(target_against), "against_type": "Account",
"against": target_account,
"against_link": target_account,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"project": self.get("project"), "project": self.get("project"),
"remarks": self.get("remarks") or "Accounting Entry for Stock", "remarks": self.get("remarks") or "Accounting Entry for Stock",
"debit": stock_value_difference, "debit": stock_value_difference / len(target_against),
}, },
self.warehouse_account[sle.warehouse]["account_currency"], self.warehouse_account[sle.warehouse]["account_currency"],
item=self, item=self,

View File

@ -98,12 +98,12 @@ class TestAssetCapitalization(unittest.TestCase):
# Test General Ledger Entries # Test General Ledger Entries
expected_gle = { expected_gle = {
"_Test Fixed Asset - TCP1": 3000, "_Test Fixed Asset - TCP1": 2999.99,
"Expenses Included In Asset Valuation - TCP1": -1000, "Expenses Included In Asset Valuation - TCP1": -1000,
"_Test Warehouse - TCP1": -2000, "_Test Warehouse - TCP1": -2000,
"Round Off - TCP1": 0.01,
} }
actual_gle = get_actual_gle_dict(asset_capitalization.name) actual_gle = get_actual_gle_dict(asset_capitalization.name)
self.assertEqual(actual_gle, expected_gle) self.assertEqual(actual_gle, expected_gle)
# Test Stock Ledger Entries # Test Stock Ledger Entries
@ -189,9 +189,10 @@ class TestAssetCapitalization(unittest.TestCase):
# Test General Ledger Entries # Test General Ledger Entries
default_expense_account = frappe.db.get_value("Company", company, "default_expense_account") default_expense_account = frappe.db.get_value("Company", company, "default_expense_account")
expected_gle = { expected_gle = {
"_Test Fixed Asset - _TC": 3000, "_Test Fixed Asset - _TC": 2999.99,
"Expenses Included In Asset Valuation - _TC": -1000, "Expenses Included In Asset Valuation - _TC": -1000,
default_expense_account: -2000, default_expense_account: -2000,
"Round Off - _TC": 0.01,
} }
actual_gle = get_actual_gle_dict(asset_capitalization.name) actual_gle = get_actual_gle_dict(asset_capitalization.name)
@ -376,9 +377,10 @@ class TestAssetCapitalization(unittest.TestCase):
# Test General Ledger Entries # Test General Ledger Entries
expected_gle = { expected_gle = {
"_Test Warehouse - TCP1": consumed_asset_value_before_disposal,
"_Test Accumulated Depreciations - TCP1": accumulated_depreciation, "_Test Accumulated Depreciations - TCP1": accumulated_depreciation,
"_Test Fixed Asset - TCP1": -consumed_asset_purchase_value, "_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) actual_gle = get_actual_gle_dict(asset_capitalization.name)
self.assertEqual(actual_gle, expected_gle) self.assertEqual(actual_gle, expected_gle)

View File

@ -86,12 +86,12 @@ class AssetCategory(Document):
if selected_key_type not in expected_key_types: if selected_key_type not in expected_key_types:
frappe.throw( 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( ).format(
d.idx, d.idx,
frappe.unscrub(key_to_match), frappe.unscrub(key_to_match),
frappe.bold(selected_account), frappe.bold(selected_account),
frappe.bold(expected_key_types), frappe.bold(" or ".join(expected_key_types)),
), ),
title=_("Invalid Account"), title=_("Invalid Account"),
) )

View File

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

View File

@ -35,6 +35,7 @@ class AssetDepreciationSchedule(Document):
amended_from: DF.Link | None amended_from: DF.Link | None
asset: DF.Link asset: DF.Link
company: DF.Link | None
daily_prorata_based: DF.Check daily_prorata_based: DF.Check
depreciation_method: DF.Literal[ depreciation_method: DF.Literal[
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual" "", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"
@ -340,6 +341,7 @@ class AssetDepreciationSchedule(Document):
n == 0 n == 0
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation and not self.opening_accumulated_depreciation
and not self.flags.wdv_it_act_applied
): ):
from_date = add_days( from_date = add_days(
asset_doc.available_for_use_date, -1 asset_doc.available_for_use_date, -1
@ -591,24 +593,17 @@ def get_depreciation_amount(
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
) )
else: else:
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row
)
return get_wdv_or_dd_depr_amount( return get_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value, depreciable_value,
rate_of_depreciation,
fb_row.frequency_of_depreciation,
schedule_idx, schedule_idx,
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
) )
@erpnext.allow_regional
def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row):
return fb_row.rate_of_depreciation
def get_straight_line_or_manual_depr_amount( def get_straight_line_or_manual_depr_amount(
asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations
): ):
@ -744,30 +739,56 @@ def get_asset_shift_factors_map():
return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
@erpnext.allow_regional
def get_wdv_or_dd_depr_amount( def get_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value, depreciable_value,
rate_of_depreciation,
frequency_of_depreciation,
schedule_idx, schedule_idx,
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
): ):
if cint(frequency_of_depreciation) == 12: return get_default_wdv_or_dd_depr_amount(
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) asset,
fb_row,
depreciable_value,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
)
def get_default_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
):
if cint(fb_row.frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
else: else:
if has_wdv_or_dd_non_yearly_pro_rata: if has_wdv_or_dd_non_yearly_pro_rata:
if schedule_idx == 0: if schedule_idx == 0:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1: elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1:
return ( return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) flt(depreciable_value)
* flt(fb_row.frequency_of_depreciation)
* (flt(fb_row.rate_of_depreciation) / 1200)
) )
else: else:
return prev_depreciation_amount return prev_depreciation_amount
else: else:
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0: if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0:
return ( return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) flt(depreciable_value)
* flt(fb_row.frequency_of_depreciation)
* (flt(fb_row.rate_of_depreciation) / 1200)
) )
else: else:
return prev_depreciation_amount return prev_depreciation_amount

View File

@ -94,7 +94,6 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
"fieldname": "daily_prorata_based", "fieldname": "daily_prorata_based",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Depreciate based on daily pro-rata" "label": "Depreciate based on daily pro-rata"
@ -110,7 +109,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-29 00:57:07.579777", "modified": "2023-12-29 08:49:39.876439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Finance Book", "name": "Asset Finance Book",

View File

@ -93,6 +93,10 @@ class AssetRepair(AccountsController):
self.increase_asset_value() 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"): if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse() self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity() self.decrease_stock_quantity()
@ -128,6 +132,10 @@ class AssetRepair(AccountsController):
self.decrease_asset_value() 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"): if self.get("stock_consumption"):
self.increase_stock_quantity() self.increase_stock_quantity()
if self.get("capitalize_repair_cost"): if self.get("capitalize_repair_cost"):
@ -277,7 +285,9 @@ class AssetRepair(AccountsController):
"account": fixed_asset_account, "account": fixed_asset_account,
"debit": self.repair_cost, "debit": self.repair_cost,
"debit_in_account_currency": self.repair_cost, "debit_in_account_currency": self.repair_cost,
"against_type": "Account",
"against": pi_expense_account, "against": pi_expense_account,
"against_link": pi_expense_account,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -296,7 +306,9 @@ class AssetRepair(AccountsController):
"account": pi_expense_account, "account": pi_expense_account,
"credit": self.repair_cost, "credit": self.repair_cost,
"credit_in_account_currency": self.repair_cost, "credit_in_account_currency": self.repair_cost,
"against_type": "Account",
"against": fixed_asset_account, "against": fixed_asset_account,
"against_link": fixed_asset_account,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -330,7 +342,9 @@ class AssetRepair(AccountsController):
"account": item.expense_account or default_expense_account, "account": item.expense_account or default_expense_account,
"credit": item.amount, "credit": item.amount,
"credit_in_account_currency": item.amount, "credit_in_account_currency": item.amount,
"against_type": "Account",
"against": fixed_asset_account, "against": fixed_asset_account,
"against_link": fixed_asset_account,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -347,7 +361,9 @@ class AssetRepair(AccountsController):
"account": fixed_asset_account, "account": fixed_asset_account,
"debit": item.amount, "debit": item.amount,
"debit_in_account_currency": item.amount, "debit_in_account_currency": item.amount,
"against_type": "Account",
"against": item.expense_account or default_expense_account, "against": item.expense_account or default_expense_account,
"against_link": item.expense_account or default_expense_account,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"cost_center": self.cost_center, "cost_center": self.cost_center,

View File

@ -214,7 +214,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-11-28 13:01:18.403492", "modified": "2024-01-05 15:26:02.320942",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",
@ -238,6 +238,41 @@
"role": "Purchase Manager", "role": "Purchase Manager",
"share": 1, "share": 1,
"write": 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", "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) { if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
var row = locals[cdt][cdn]; 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) 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) { 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_requested_qty()
self.update_ordered_qty() self.update_ordered_qty()
self.update_reserved_qty_for_subcontract() self.update_reserved_qty_for_subcontract()
self.update_subcontracting_order_status()
self.notify_update() self.notify_update()
clear_doctype_notifications(self) clear_doctype_notifications(self)
@ -627,6 +628,17 @@ class PurchaseOrder(BuyingController):
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"): if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
make_subcontracting_order(self.name, save=True, notify=True) 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): def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item""" """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): class TestPurchaseOrder(FrappeTestCase):
def test_purchase_order_qty(self): def test_purchase_order_qty(self):
po = create_purchase_order(qty=1, do_not_save=True) po = create_purchase_order(qty=1, do_not_save=True)
# NonNegativeError with qty=-1
po.append( po.append(
"items", "items",
{ {
@ -39,9 +41,15 @@ class TestPurchaseOrder(FrappeTestCase):
) )
self.assertRaises(frappe.NonNegativeError, po.save) self.assertRaises(frappe.NonNegativeError, po.save)
# InvalidQtyError with qty=0
po.items[1].qty = 0 po.items[1].qty = 0
self.assertRaises(InvalidQtyError, po.save) 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): def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True) po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) 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", "item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"from_warehouse": args.from_warehouse, "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, "rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1), "schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1), "include_exploded_items": args.get("include_exploded_items", 1),

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