Merge remote-tracking branch 'upstream/develop' into move-exotel-to-separate-app

This commit is contained in:
Suraj Shetty 2023-04-30 12:43:18 +05:30
commit a0131a96cb
262 changed files with 9386 additions and 8395 deletions

View File

@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"} frappeuser=${FRAPPE_USER:-"frappe"}
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}} frappebranch=${FRAPPE_BRANCH:-$githubbranch}
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1 git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
@ -60,7 +61,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app payments bench get-app payments --branch ${githubbranch%"-hotfix"}
bench get-app erpnext "${GITHUB_WORKSPACE}" bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi

View File

@ -7,7 +7,6 @@ on:
- '**.css' - '**.css'
- '**.md' - '**.md'
- '**.html' - '**.html'
- '**.csv'
push: push:
branches: [ develop ] branches: [ develop ]
paths-ignore: paths-ignore:

View File

@ -3,13 +3,13 @@
# These owners will be the default owners for everything in # These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007 erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/loan_management/ @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @nextchamp-saqib @deepeshgarg007 erpnext/support/ @deepeshgarg007
pos* @nextchamp-saqib pos*
erpnext/buying/ @rohitwaghchaure @s-aga-r erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r erpnext/maintenance/ @rohitwaghchaure @s-aga-r
@ -18,12 +18,8 @@ erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/crm/ @NagariaHussain erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/education/ @rutwikhdev erpnext/patches/ @deepeshgarg007
erpnext/projects/ @ruchamahabal
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure .github/ @deepeshgarg007
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
.github/ @ankush
pyproject.toml @ankush pyproject.toml @ankush

View File

@ -18,7 +18,6 @@
"root_type", "root_type",
"report_type", "report_type",
"account_currency", "account_currency",
"inter_company_account",
"column_break1", "column_break1",
"parent_account", "parent_account",
"account_type", "account_type",
@ -34,15 +33,11 @@
{ {
"fieldname": "properties", "fieldname": "properties",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break0", "fieldname": "column_break0",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1,
"width": "50%" "width": "50%"
}, },
{ {
@ -53,9 +48,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "account_name", "oldfieldname": "account_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "account_number", "fieldname": "account_number",
@ -63,17 +56,13 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Account Number", "label": "Account Number",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "is_group", "fieldname": "is_group",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Group", "label": "Is Group"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
@ -85,9 +74,7 @@
"options": "Company", "options": "Company",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 1, "remember_last_selected_value": 1,
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "root_type", "fieldname": "root_type",
@ -95,9 +82,7 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Root Type", "label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity", "options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "report_type", "fieldname": "report_type",
@ -105,32 +90,18 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Report Type", "label": "Report Type",
"options": "\nBalance Sheet\nProfit and Loss", "options": "\nBalance Sheet\nProfit and Loss",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.is_group==0", "depends_on": "eval:doc.is_group==0",
"fieldname": "account_currency", "fieldname": "account_currency",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Currency", "label": "Currency",
"options": "Currency", "options": "Currency"
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "inter_company_account",
"fieldtype": "Check",
"label": "Inter Company Account",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break1", "fieldname": "column_break1",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1,
"width": "50%" "width": "50%"
}, },
{ {
@ -142,9 +113,7 @@
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Account", "options": "Account",
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"description": "Setting Account Type helps in selecting this Account in transactions.", "description": "Setting Account Type helps in selecting this Account in transactions.",
@ -154,9 +123,7 @@
"label": "Account Type", "label": "Account Type",
"oldfieldname": "account_type", "oldfieldname": "account_type",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary", "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"description": "Rate at which this tax is applied", "description": "Rate at which this tax is applied",
@ -164,9 +131,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Rate", "label": "Rate",
"oldfieldname": "tax_rate", "oldfieldname": "tax_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"description": "If the account is frozen, entries are allowed to restricted users.", "description": "If the account is frozen, entries are allowed to restricted users.",
@ -175,17 +140,13 @@
"label": "Frozen", "label": "Frozen",
"oldfieldname": "freeze_account", "oldfieldname": "freeze_account",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "No\nYes", "options": "No\nYes"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "balance_must_be", "fieldname": "balance_must_be",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Balance must be", "label": "Balance must be",
"options": "\nDebit\nCredit", "options": "\nDebit\nCredit"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "lft", "fieldname": "lft",
@ -194,9 +155,7 @@
"label": "Lft", "label": "Lft",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"search_index": 1, "search_index": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "rgt", "fieldname": "rgt",
@ -205,9 +164,7 @@
"label": "Rgt", "label": "Rgt",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"search_index": 1, "search_index": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "old_parent", "fieldname": "old_parent",
@ -215,33 +172,27 @@
"hidden": 1, "hidden": 1,
"label": "Old Parent", "label": "Old Parent",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)", "depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)",
"fieldname": "include_in_gross", "fieldname": "include_in_gross",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Include in gross", "label": "Include in gross"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable", "label": "Disable"
"show_days": 1,
"show_seconds": 1
} }
], ],
"icon": "fa fa-money", "icon": "fa fa-money",
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-06-11 15:15:54.338622", "modified": "2023-04-11 16:08:46.983677",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Account", "name": "Account",
@ -301,5 +252,6 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -394,7 +394,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if ancestors and not allow_independent_account_creation: if ancestors and not allow_independent_account_creation:
for ancestor in ancestors: for ancestor in ancestors:
if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"): old_name = frappe.db.get_value(
"Account",
{"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor},
"name",
)
if old_name:
# same account in parent company exists # same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company") allow_child_account_creation = _("Allow Account Creation Against Child Company")

View File

@ -29,6 +29,7 @@ def create_charts(
"root_type", "root_type",
"is_group", "is_group",
"tax_rate", "tax_rate",
"account_currency",
]: ]:
account_number = cstr(child.get("account_number")).strip() account_number = cstr(child.get("account_number")).strip()
@ -95,7 +96,17 @@ def identify_is_group(child):
is_group = child.get("is_group") is_group = child.get("is_group")
elif len( elif len(
set(child.keys()) set(child.keys())
- set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"]) - set(
[
"account_name",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_number",
"account_currency",
]
)
): ):
is_group = 1 is_group = 1
else: else:
@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
"root_type", "root_type",
"tax_rate", "tax_rate",
"account_number", "account_number",
"account_currency",
], ],
order_by="lft, rgt", order_by="lft, rgt",
) )
@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"root_type", "root_type",
"is_group", "is_group",
"tax_rate", "tax_rate",
"account_currency",
]: ]:
continue continue

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("Account Closing Balance", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,164 @@
{
"actions": [],
"creation": "2023-02-21 15:20:59.586811",
"default_view": "List",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"closing_date",
"account",
"cost_center",
"debit",
"credit",
"account_currency",
"debit_in_account_currency",
"credit_in_account_currency",
"project",
"company",
"finance_book",
"period_closing_voucher",
"is_period_closing_voucher_entry"
],
"fields": [
{
"fieldname": "closing_date",
"fieldtype": "Date",
"in_filter": 1,
"in_list_view": 1,
"label": "Closing Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
"search_index": 1
},
{
"fieldname": "account",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account",
"oldfieldname": "account",
"oldfieldtype": "Link",
"options": "Account",
"search_index": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Cost Center",
"oldfieldname": "cost_center",
"oldfieldtype": "Link",
"options": "Cost Center"
},
{
"fieldname": "debit",
"fieldtype": "Currency",
"label": "Debit Amount",
"oldfieldname": "debit",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "credit",
"fieldtype": "Currency",
"label": "Credit Amount",
"oldfieldname": "credit",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency"
},
{
"fieldname": "debit_in_account_currency",
"fieldtype": "Currency",
"label": "Debit Amount in Account Currency",
"options": "account_currency"
},
{
"fieldname": "credit_in_account_currency",
"fieldtype": "Currency",
"label": "Credit Amount in Account Currency",
"options": "account_currency"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
"search_index": 1
},
{
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
},
{
"fieldname": "period_closing_voucher",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Period Closing Voucher",
"options": "Period Closing Voucher",
"search_index": 1
},
{
"default": "0",
"fieldname": "is_period_closing_voucher_entry",
"fieldtype": "Check",
"label": "Is Period Closing Voucher Entry"
}
],
"icon": "fa fa-list",
"in_create": 1,
"links": [],
"modified": "2023-03-06 08:56:36.393237",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Closing Balance",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager"
},
{
"export": 1,
"read": 1,
"report": 1,
"role": "Auditor"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,127 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils import cint, cstr
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
class AccountClosingBalance(Document):
pass
def make_closing_entries(closing_entries, voucher_name):
accounting_dimensions = get_accounting_dimensions()
company = closing_entries[0].get("company")
closing_date = closing_entries[0].get("closing_date")
previous_closing_entries = get_previous_closing_entries(
company, closing_date, accounting_dimensions
)
combined_entries = closing_entries + previous_closing_entries
merged_entries = aggregate_with_last_account_closing_balance(
combined_entries, accounting_dimensions
)
for key, value in merged_entries.items():
cle = frappe.new_doc("Account Closing Balance")
cle.update(value)
cle.update(value["dimensions"])
cle.update(
{
"period_closing_voucher": voucher_name,
"closing_date": closing_date,
}
)
cle.submit()
def aggregate_with_last_account_closing_balance(entries, accounting_dimensions):
merged_entries = {}
for entry in entries:
key, key_values = generate_key(entry, accounting_dimensions)
merged_entries.setdefault(
key,
{
"debit": 0,
"credit": 0,
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
},
)
merged_entries[key]["dimensions"] = key_values
merged_entries[key]["debit"] += entry.get("debit")
merged_entries[key]["credit"] += entry.get("credit")
merged_entries[key]["debit_in_account_currency"] += entry.get("debit_in_account_currency")
merged_entries[key]["credit_in_account_currency"] += entry.get("credit_in_account_currency")
return merged_entries
def generate_key(entry, accounting_dimensions):
key = [
cstr(entry.get("account")),
cstr(entry.get("account_currency")),
cstr(entry.get("cost_center")),
cstr(entry.get("project")),
cstr(entry.get("finance_book")),
cint(entry.get("is_period_closing_voucher_entry")),
]
key_values = {
"company": cstr(entry.get("company")),
"account": cstr(entry.get("account")),
"account_currency": cstr(entry.get("account_currency")),
"cost_center": cstr(entry.get("cost_center")),
"project": cstr(entry.get("project")),
"finance_book": cstr(entry.get("finance_book")),
"is_period_closing_voucher_entry": cint(entry.get("is_period_closing_voucher_entry")),
}
for dimension in accounting_dimensions:
key.append(cstr(entry.get(dimension)))
key_values[dimension] = cstr(entry.get(dimension))
return tuple(key), key_values
def get_previous_closing_entries(company, closing_date, accounting_dimensions):
entries = []
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
fields=["name"],
order_by="posting_date desc",
limit=1,
)
if last_period_closing_voucher:
account_closing_balance = frappe.qb.DocType("Account Closing Balance")
query = frappe.qb.from_(account_closing_balance).select(
account_closing_balance.company,
account_closing_balance.account,
account_closing_balance.account_currency,
account_closing_balance.debit,
account_closing_balance.credit,
account_closing_balance.debit_in_account_currency,
account_closing_balance.credit_in_account_currency,
account_closing_balance.cost_center,
account_closing_balance.project,
account_closing_balance.finance_book,
account_closing_balance.is_period_closing_voucher_entry,
)
for dimension in accounting_dimensions:
query = query.select(account_closing_balance[dimension])
query = query.where(
account_closing_balance.period_closing_voucher == last_period_closing_voucher[0].name
)
entries = query.run(as_dict=1)
return entries

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 TestAccountClosingBalance(FrappeTestCase):
pass

View File

@ -31,6 +31,7 @@
"determine_address_tax_category_from", "determine_address_tax_category_from",
"column_break_19", "column_break_19",
"add_taxes_from_item_tax_template", "add_taxes_from_item_tax_template",
"book_tax_discount_loss",
"print_settings", "print_settings",
"show_inclusive_tax_in_print", "show_inclusive_tax_in_print",
"column_break_12", "column_break_12",
@ -360,6 +361,13 @@
"fieldname": "show_balance_in_coa", "fieldname": "show_balance_in_coa",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts" "label": "Show Balances in Chart Of Accounts"
},
{
"default": "0",
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -367,7 +375,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-01-02 12:07:42.434214", "modified": "2023-03-28 09:50:20.375233",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -81,7 +81,7 @@ class BankClearance(Document):
loan_disbursement = frappe.qb.DocType("Loan Disbursement") loan_disbursement = frappe.qb.DocType("Loan Disbursement")
loan_disbursements = ( query = (
frappe.qb.from_(loan_disbursement) frappe.qb.from_(loan_disbursement)
.select( .select(
ConstantColumn("Loan Disbursement").as_("payment_document"), ConstantColumn("Loan Disbursement").as_("payment_document"),
@ -90,17 +90,22 @@ class BankClearance(Document):
ConstantColumn(0).as_("debit"), ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"), loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"), loan_disbursement.reference_date.as_("cheque_date"),
loan_disbursement.clearance_date.as_("clearance_date"),
loan_disbursement.disbursement_date.as_("posting_date"), loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"), loan_disbursement.applicant.as_("against_account"),
) )
.where(loan_disbursement.docstatus == 1) .where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date) .where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date) .where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date) .orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, order=frappe.qb.desc) .orderby(loan_disbursement.name, order=frappe.qb.desc)
).run(as_dict=1) )
if not self.include_reconciled_entries:
query = query.where(loan_disbursement.clearance_date.isnull())
loan_disbursements = query.run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment") loan_repayment = frappe.qb.DocType("Loan Repayment")
@ -113,16 +118,19 @@ class BankClearance(Document):
ConstantColumn(0).as_("credit"), ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"), loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"), loan_repayment.reference_date.as_("cheque_date"),
loan_repayment.clearance_date.as_("clearance_date"),
loan_repayment.applicant.as_("against_account"), loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date, loan_repayment.posting_date,
) )
.where(loan_repayment.docstatus == 1) .where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.posting_date >= self.from_date) .where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date) .where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account])) .where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
) )
if not self.include_reconciled_entries:
query = query.where(loan_repayment.clearance_date.isnull())
if frappe.db.has_column("Loan Repayment", "repay_from_salary"): if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0)) query = query.where((loan_repayment.repay_from_salary == 0))

View File

@ -18,6 +18,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}, },
onload: function (frm) { onload: function (frm) {
// Set default filter dates
today = frappe.datetime.get_today()
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
frm.doc.bank_statement_to_date = today;
frm.trigger('bank_account'); frm.trigger('bank_account');
}, },
@ -32,6 +36,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}, },
refresh: function (frm) { refresh: function (frm) {
frm.disable_save();
frappe.require("bank-reconciliation-tool.bundle.js", () => frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool") frm.trigger("make_reconciliation_tool")
); );
@ -72,10 +77,12 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}, },
}) })
}); });
},
after_save: function (frm) { frm.add_custom_button(__('Get Unreconciled Entries'), function() {
frm.trigger("make_reconciliation_tool"); frm.trigger("make_reconciliation_tool");
});
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
}, },
bank_account: function (frm) { bank_account: function (frm) {
@ -89,7 +96,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
r.account, r.account,
"account_currency", "account_currency",
(r) => { (r) => {
frm.currency = r.account_currency; frm.doc.account_currency = r.account_currency;
frm.trigger("render_chart"); frm.trigger("render_chart");
} }
); );
@ -162,9 +169,9 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"reconciliation_tool_cards" "reconciliation_tool_cards"
).$wrapper, ).$wrapper,
bank_statement_closing_balance: bank_statement_closing_balance:
frm.doc.bank_statement_closing_balance, frm.doc.bank_statement_closing_balance,
cleared_balance: frm.cleared_balance, cleared_balance: frm.cleared_balance,
currency: frm.currency, currency: frm.doc.account_currency,
} }
); );
}, },

View File

@ -14,6 +14,7 @@
"to_reference_date", "to_reference_date",
"filter_by_reference_date", "filter_by_reference_date",
"column_break_2", "column_break_2",
"account_currency",
"account_opening_balance", "account_opening_balance",
"bank_statement_closing_balance", "bank_statement_closing_balance",
"section_break_1", "section_break_1",
@ -59,7 +60,7 @@
"fieldname": "account_opening_balance", "fieldname": "account_opening_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Account Opening Balance", "label": "Account Opening Balance",
"options": "Currency", "options": "account_currency",
"read_only": 1 "read_only": 1
}, },
{ {
@ -67,7 +68,7 @@
"fieldname": "bank_statement_closing_balance", "fieldname": "bank_statement_closing_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Closing Balance", "label": "Closing Balance",
"options": "Currency" "options": "account_currency"
}, },
{ {
"fieldname": "section_break_1", "fieldname": "section_break_1",
@ -104,13 +105,20 @@
"fieldname": "filter_by_reference_date", "fieldname": "filter_by_reference_date",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Filter by Reference Date" "label": "Filter by Reference Date"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Account Currency",
"options": "Currency"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-01-13 13:00:02.022919", "modified": "2023-03-07 11:02:24.535714",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Reconciliation Tool", "name": "Bank Reconciliation Tool",

View File

@ -46,7 +46,7 @@ class BankTransaction(StatusUpdater):
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"
if 0.0 >= self.unallocated_amount: if 0.0 >= self.unallocated_amount:
frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled")) frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
added = False added = False
for voucher in vouchers: for voucher in vouchers:
@ -114,9 +114,7 @@ class BankTransaction(StatusUpdater):
elif 0.0 > unallocated_amount: elif 0.0 > unallocated_amount:
self.db_delete_payment_entry(payment_entry) self.db_delete_payment_entry(payment_entry)
frappe.throw( frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
)
self.reload() self.reload()
@ -178,7 +176,9 @@ def get_clearance_details(transaction, payment_entry):
if gle["gl_account"] == gl_bank_account: if gle["gl_account"] == gl_bank_account:
if gle["amount"] <= 0.0: if gle["amount"] <= 0.0:
frappe.throw( frappe.throw(
frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}") frappe._("Voucher {0} value is broken: {1}").format(
payment_entry.payment_entry, gle["amount"]
)
) )
unmatched_gles -= 1 unmatched_gles -= 1

View File

@ -36,7 +36,7 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data]) no_of_columns = max([len(d) for d in data])
if no_of_columns > 7: if no_of_columns > 8:
frappe.throw( frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"), _("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")), title=(_("Wrong Template")),
@ -233,6 +233,7 @@ def build_forest(data):
is_group, is_group,
account_type, account_type,
root_type, root_type,
account_currency,
) = i ) = i
if not account_name: if not account_name:
@ -253,6 +254,8 @@ def build_forest(data):
charts_map[account_name]["account_type"] = account_type charts_map[account_name]["account_type"] = account_type
if root_type: if root_type:
charts_map[account_name]["root_type"] = root_type charts_map[account_name]["root_type"] = root_type
if account_currency:
charts_map[account_name]["account_currency"] = account_currency
path = return_parent(data, account_name)[::-1] path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created paths.append(path) # List of path is created
line_no += 1 line_no += 1
@ -315,20 +318,21 @@ def get_template(template_type):
"Is Group", "Is Group",
"Account Type", "Account Type",
"Root Type", "Root Type",
"Account Currency",
] ]
writer = UnicodeWriter() writer = UnicodeWriter()
writer.writerow(fields) writer.writerow(fields)
if template_type == "Blank Template": if template_type == "Blank Template":
for root_type in get_root_types(): for root_type in get_root_types():
writer.writerow(["", "", "", 1, "", root_type]) writer.writerow(["", "", "", "", 1, "", root_type])
for account in get_mandatory_group_accounts(): for account in get_mandatory_group_accounts():
writer.writerow(["", "", "", 1, account, "Asset"]) writer.writerow(["", "", "", "", 1, account, "Asset"])
for account_type in get_mandatory_account_types(): for account_type in get_mandatory_account_types():
writer.writerow( writer.writerow(
["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] ["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
) )
else: else:
writer = get_sample_template(writer) writer = get_sample_template(writer)

View File

@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
# Handle Accounts with '0' balance in Account/Base Currency # Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]: for d in [x for x in account_details if x.zero_balance]:
# TODO: Set new balance in Base/Account currency if d.balance != 0:
if d.balance > 0:
current_exchange_rate = new_exchange_rate = 0 current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0' new_balance_in_account_currency = 0 # this will be '0'
@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document):
journal_entry_accounts = [] journal_entry_accounts = []
for d in accounts: for d in accounts:
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
continue
dr_or_cr = ( dr_or_cr = (
"debit_in_account_currency" "debit_in_account_currency"
if d.get("balance_in_account_currency") > 0 if d.get("balance_in_account_currency") > 0
@ -448,7 +450,13 @@ class ExchangeRateRevaluation(Document):
} }
) )
journal_entry_accounts.append( journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
journal_entry.append(
"accounts",
{ {
"account": unrealized_exchange_gain_loss_account, "account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account), "balance": get_balance_on(unrealized_exchange_gain_loss_account),
@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document):
"exchange_rate": 1, "exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
} },
) )
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency() journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit() journal_entry.set_total_debit_credit()
journal_entry.save() journal_entry.save()
@ -483,6 +490,8 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
conditions.append(gl.company == company) conditions.append(gl.company == company)
conditions.append(gl.account == account) conditions.append(gl.account == account)
conditions.append(gl.is_cancelled == 0) conditions.append(gl.is_cancelled == 0)
conditions.append((gl.debit > 0) | (gl.credit > 0))
conditions.append((gl.debit_in_account_currency > 0) | (gl.credit_in_account_currency > 0))
if party_type: if party_type:
conditions.append(gl.party_type == party_type) conditions.append(gl.party_type == party_type)
if party: if party:

View File

@ -137,7 +137,8 @@
"fieldname": "finance_book", "fieldname": "finance_book",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Finance Book", "label": "Finance Book",
"options": "Finance Book" "options": "Finance Book",
"read_only": 1
}, },
{ {
"fieldname": "2_add_edit_gl_entries", "fieldname": "2_add_edit_gl_entries",
@ -538,7 +539,7 @@
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-17 12:53:53.280620", "modified": "2023-03-01 14:58:59.286591",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@ -51,7 +51,7 @@ class JournalEntry(AccountsController):
self.validate_multi_currency() self.validate_multi_currency()
self.set_amounts_in_company_currency() self.set_amounts_in_company_currency()
self.validate_debit_credit_amount() self.validate_debit_credit_amount()
self.set_total_debit_credit()
# Do not validate while importing via data import # Do not validate while importing via data import
if not frappe.flags.in_import: if not frappe.flags.in_import:
self.validate_total_debit_and_credit() self.validate_total_debit_and_credit()
@ -666,7 +666,6 @@ class JournalEntry(AccountsController):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self): def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
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):
if self.difference: if self.difference:
frappe.throw( frappe.throw(

View File

@ -287,10 +287,6 @@ class TestJournalEntry(unittest.TestCase):
jv.submit() jv.submit()
def test_inter_company_jv(self): def test_inter_company_jv(self):
frappe.db.set_value("Account", "Sales Expenses - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1)
jv = make_journal_entry( jv = make_journal_entry(
"Sales Expenses - _TC", "Sales Expenses - _TC",
"Buildings - _TC", "Buildings - _TC",

View File

@ -217,7 +217,6 @@ frappe.ui.form.on('Payment Entry', {
frm.toggle_display("set_exchange_gain_loss", frm.toggle_display("set_exchange_gain_loss",
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount); frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
frm.refresh_fields();
}, },
set_dynamic_labels: function(frm) { set_dynamic_labels: function(frm) {
@ -245,8 +244,6 @@ frappe.ui.form.on('Payment Entry', {
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"], frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
party_account_currency, "references"); party_account_currency, "references");
frm.set_currency_labels(["amount"], company_currency, "deductions");
cur_frm.set_df_property("source_exchange_rate", "description", cur_frm.set_df_property("source_exchange_rate", "description",
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency)); ("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));

View File

@ -416,7 +416,7 @@ class PaymentEntry(AccountsController):
for ref in self.get("references"): for ref in self.get("references"):
if ref.payment_term and ref.reference_name: if ref.payment_term and ref.reference_name:
key = (ref.payment_term, ref.reference_name) key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
invoice_payment_amount_map.setdefault(key, 0.0) invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += ref.allocated_amount invoice_payment_amount_map[key] += ref.allocated_amount
@ -424,20 +424,37 @@ class PaymentEntry(AccountsController):
payment_schedule = frappe.get_all( payment_schedule = frappe.get_all(
"Payment Schedule", "Payment Schedule",
filters={"parent": ref.reference_name}, filters={"parent": ref.reference_name},
fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"], fields=[
"paid_amount",
"payment_amount",
"payment_term",
"discount",
"outstanding",
"discount_type",
],
) )
for term in payment_schedule: for term in payment_schedule:
invoice_key = (term.payment_term, ref.reference_name) invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
invoice_paid_amount_map.setdefault(invoice_key, {}) invoice_paid_amount_map.setdefault(invoice_key, {})
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( if not (term.discount_type and term.discount):
term.discount / 100 continue
)
if term.discount_type == "Percentage":
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
term.discount / 100
)
else:
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1): for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
if not invoice_paid_amount_map.get(key): if not invoice_paid_amount_map.get(key):
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1])) frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
allocated_amount = self.get_allocated_amount_in_transaction_currency(
allocated_amount, key[2], key[1]
)
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding")) outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt")) discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
@ -472,6 +489,33 @@ class PaymentEntry(AccountsController):
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
) )
def get_allocated_amount_in_transaction_currency(
self, allocated_amount, reference_doctype, reference_docname
):
"""
Payment Entry could be in base currency while reference's payment schedule
is always in transaction currency.
E.g.
* SI with base=INR and currency=USD
* SI with payment schedule in USD
* PE in INR (accounting done in base currency)
"""
ref_currency, ref_exchange_rate = frappe.db.get_value(
reference_doctype, reference_docname, ["currency", "conversion_rate"]
)
is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
# PE in different currency
reference_is_multi_currency = self.paid_from_account_currency != ref_currency
if not (is_single_currency and reference_is_multi_currency):
return allocated_amount
allocated_amount = flt(
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
)
return allocated_amount
def set_status(self): def set_status(self):
if self.docstatus == 2: if self.docstatus == 2:
self.status = "Cancelled" self.status = "Cancelled"
@ -1642,7 +1686,14 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
@frappe.whitelist() @frappe.whitelist()
def get_payment_entry( def get_payment_entry(
dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None dt,
dn,
party_amount=None,
bank_account=None,
bank_amount=None,
party_type=None,
payment_type=None,
reference_date=None,
): ):
reference_doc = None reference_doc = None
doc = frappe.get_doc(dt, dn) doc = frappe.get_doc(dt, dn)
@ -1669,8 +1720,9 @@ def get_payment_entry(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
) )
paid_amount, received_amount, discount_amount = apply_early_payment_discount( reference_date = getdate(reference_date)
paid_amount, received_amount, doc paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
paid_amount, received_amount, doc, party_account_currency, reference_date
) )
pe = frappe.new_doc("Payment Entry") pe = frappe.new_doc("Payment Entry")
@ -1678,6 +1730,7 @@ def get_payment_entry(
pe.company = doc.company pe.company = doc.company
pe.cost_center = doc.get("cost_center") pe.cost_center = doc.get("cost_center")
pe.posting_date = nowdate() pe.posting_date = nowdate()
pe.reference_date = reference_date
pe.mode_of_payment = doc.get("mode_of_payment") pe.mode_of_payment = doc.get("mode_of_payment")
pe.party_type = party_type pe.party_type = party_type
pe.party = doc.get(scrub(party_type)) pe.party = doc.get(scrub(party_type))
@ -1718,7 +1771,7 @@ def get_payment_entry(
): ):
for reference in get_reference_as_per_payment_terms( for reference in get_reference_as_per_payment_terms(
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
): ):
pe.append("references", reference) pe.append("references", reference)
else: else:
@ -1769,16 +1822,17 @@ def get_payment_entry(
if party_account and bank: if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc) pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts() pe.set_amounts()
if discount_amount: if discount_amount:
pe.set_gain_or_loss( base_total_discount_loss = 0
account_details={ if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
"cost_center": pe.cost_center
or frappe.get_cached_value("Company", pe.company, "cost_center"), set_pending_discount_loss(
"amount": discount_amount * (-1 if payment_type == "Pay" else 1), pe, doc, discount_amount, base_total_discount_loss, party_account_currency
}
) )
pe.set_difference_amount()
pe.set_difference_amount()
return pe return pe
@ -1889,20 +1943,28 @@ def set_paid_amount_and_received_amount(
return paid_amount, received_amount return paid_amount, received_amount
def apply_early_payment_discount(paid_amount, received_amount, doc): def apply_early_payment_discount(
paid_amount, received_amount, doc, party_account_currency, reference_date
):
total_discount = 0 total_discount = 0
valid_discounts = []
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
is_multi_currency = party_account_currency != doc.company_currency
if doc.doctype in eligible_for_payments and has_payment_schedule: if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule: for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
if term.discount_type == "Percentage": if term.discount_type == "Percentage":
discount_amount = flt(doc.get("grand_total")) * (term.discount / 100) grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
discount_amount = flt(grand_total) * (term.discount / 100)
else: else:
discount_amount = term.discount discount_amount = term.discount
discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1) # if accounting is done in the same currency, paid_amount = received_amount
conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
discount_amount_in_foreign_currency = discount_amount * conversion_rate
if doc.doctype == "Sales Invoice": if doc.doctype == "Sales Invoice":
paid_amount -= discount_amount paid_amount -= discount_amount
@ -1911,23 +1973,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
received_amount -= discount_amount received_amount -= discount_amount
paid_amount -= discount_amount_in_foreign_currency paid_amount -= discount_amount_in_foreign_currency
valid_discounts.append({"type": term.discount_type, "discount": term.discount})
total_discount += discount_amount total_discount += discount_amount
if total_discount: if total_discount:
money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency")) currency = doc.get("currency") if is_multi_currency else doc.company_currency
money = frappe.utils.fmt_money(total_discount, currency=currency)
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
return paid_amount, received_amount, total_discount return paid_amount, received_amount, total_discount, valid_discounts
def set_pending_discount_loss(
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
):
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
if party_account_currency != doc.company_currency:
discount_amount = discount_amount * doc.get("conversion_rate", 1)
# Avoid considering miniscule losses
discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
# Set base discount amount (discount loss/pending rounding loss) in deductions
if discount_amount > 0.0:
positive_negative = -1 if pe.payment_type == "Pay" else 1
# If tax loss booking is enabled, pending loss will be rounding loss.
# Otherwise it will be the total discount loss.
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
pe.set_gain_or_loss(
account_details={
"account": frappe.get_cached_value("Company", pe.company, account_type),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": discount_amount * positive_negative,
}
)
def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
"""Split early payment discount into Income Loss & Tax Loss."""
total_discount_percent = get_total_discount_percent(doc, valid_discounts)
if not total_discount_percent:
return 0.0
base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
# Round off total loss rather than individual losses to reduce rounding error
return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
def get_total_discount_percent(doc, valid_discounts) -> float:
"""Get total percentage and amount discount applied as a percentage."""
total_discount_percent = (
sum(
discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
)
or 0.0
)
# Operate in percentages only as it makes the income & tax split easier
total_discount_amount = (
sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
or 0.0
)
if total_discount_amount:
discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
total_discount_percent += discount_percentage
return total_discount_percent
return total_discount_percent
def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
"""Add loss on income discount in base currency."""
precision = doc.precision("total")
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
pe.append(
"deductions",
{
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(base_loss_on_income, precision),
},
)
return base_loss_on_income # Return loss without rounding
def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
"""Add loss on tax discount in base currency."""
tax_discount_loss = {}
base_total_tax_loss = 0
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
# The same account head could be used more than once
for tax in doc.get("taxes", []):
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
total_discount_percentage / 100
)
account = tax.get("account_head")
if not tax_discount_loss.get(account):
tax_discount_loss[account] = base_tax_loss
else:
tax_discount_loss[account] += base_tax_loss
for account, loss in tax_discount_loss.items():
base_total_tax_loss += loss
if loss == 0.0:
continue
pe.append(
"deductions",
{
"account": account,
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(loss, precision),
},
)
return base_total_tax_loss # Return loss without rounding
def get_reference_as_per_payment_terms( def get_reference_as_per_payment_terms(
payment_schedule, dt, dn, doc, grand_total, outstanding_amount payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
): ):
references = [] references = []
is_multi_currency_acc = (doc.currency != doc.company_currency) and (
party_account_currency != doc.company_currency
)
for payment_term in payment_schedule: for payment_term in payment_schedule:
payment_term_outstanding = flt( payment_term_outstanding = flt(
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount") payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
) )
if not is_multi_currency_acc:
# If accounting is done in company currency for multi-currency transaction
payment_term_outstanding = flt(
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
)
if payment_term_outstanding: if payment_term_outstanding:
references.append( references.append(

View File

@ -5,7 +5,7 @@ import unittest
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.payment_entry.payment_entry import (
@ -256,10 +256,25 @@ class TestPaymentEntry(FrappeTestCase):
}, },
) )
si.save() si.save()
si.submit() si.submit()
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].allocated_amount, 236.0)
self.assertEqual(pe.paid_amount, 212.4)
self.assertEqual(pe.deductions[0].amount, 23.6)
pe.submit() pe.submit()
si.load_from_db() si.load_from_db()
@ -269,6 +284,190 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].outstanding, 0) self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6) self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
def test_payment_entry_against_payment_terms_with_discount_amount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
si.payment_terms_template = "Test Discount Amount Template"
create_payment_terms_template_with_discount(
name="30 Credit Days with Rs.50 Discount",
discount_type="Amount",
discount=50,
template_name="Test Discount Amount Template",
)
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 18,
},
)
si.save()
si.submit()
# Set reference date past discount cut off date
pe_1 = get_payment_entry(
"Sales Invoice",
si.name,
bank_account="_Test Cash - _TC",
reference_date=frappe.utils.add_days(si.posting_date, 2),
)
self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
# Test if tax loss is booked on enabling configuration
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].allocated_amount, 236.0)
self.assertEqual(pe.paid_amount, 186)
self.assertEqual(pe.deductions[0].amount, 50.0)
pe.submit()
si.load_from_db()
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 186)
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
@change_settings(
"Accounts Settings",
{
"allow_multi_currency_invoices_against_single_party_account": 1,
"book_tax_discount_loss": 1,
},
)
def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
self,
):
"""
1. Multi-currency SI with single currency accounting (company currency)
2. PE with early payment discount
3. Test if Paid Amount is calculated in company currency
4. Test if deductions are calculated in company currency
SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
"""
si = create_sales_invoice(
customer="_Test Customer",
currency="USD",
conversion_rate=50,
do_not_save=1,
)
create_payment_terms_template_with_discount()
si.payment_terms_template = "Test Discount Template"
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.save()
si.submit()
pe = get_payment_entry(
"Sales Invoice",
si.name,
bank_account="_Test Bank - _TC",
)
pe.reference_no = si.name
pe.reference_date = nowdate()
# Early payment discount loss on income
self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
self.assertEqual(pe.received_amount, 4500.0)
self.assertEqual(pe.deductions[0].amount, 500.0)
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
self.assertEqual(pe.difference_amount, 0.0)
pe.insert()
pe.submit()
expected_gle = dict(
(d[0], d)
for d in [
["Debtors - _TC", 0, 5000, si.name],
["_Test Bank - _TC", 4500, 0, None],
["Write Off - _TC", 500.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
"""
1. Multi-currency SI with multi-currency accounting
2. PE with early payment discount and also exchange loss
3. Test if Paid Amount is calculated in transaction currency
4. Test if deductions are calculated in base/company currency
5. Test if exchange loss is reflected in difference
"""
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_save=1,
)
create_payment_terms_template_with_discount()
si.payment_terms_template = "Test Discount Template"
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.save()
si.submit()
pe = get_payment_entry(
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
)
pe.reference_no = si.name
pe.reference_date = nowdate()
# Early payment discount loss on income
self.assertEqual(pe.paid_amount, 90.0)
self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
self.assertEqual(pe.deductions[0].amount, 500.0)
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
# Exchange loss
self.assertEqual(pe.difference_amount, 300.0)
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 300.0,
},
)
pe.insert()
pe.submit()
self.assertEqual(pe.difference_amount, 0.0)
expected_gle = dict(
(d[0], d)
for d in [
["_Test Receivable USD - _TC", 0, 5000, si.name],
["_Test Bank - _TC", 4200, 0, None],
["Write Off - _TC", 500.0, 0, None],
["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
def test_payment_against_purchase_invoice_to_check_status(self): def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice( pi = make_purchase_invoice(
supplier="_Test Supplier USD", supplier="_Test Supplier USD",
@ -839,24 +1038,27 @@ def create_payment_terms_template():
).insert() ).insert()
def create_payment_terms_template_with_discount(): def create_payment_terms_template_with_discount(
name=None, discount_type=None, discount=None, template_name=None
):
create_payment_term(name or "30 Credit Days with 10% Discount")
template_name = template_name or "Test Discount Template"
create_payment_term("30 Credit Days with 10% Discount") if not frappe.db.exists("Payment Terms Template", template_name):
frappe.get_doc(
if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
payment_term_template = frappe.get_doc(
{ {
"doctype": "Payment Terms Template", "doctype": "Payment Terms Template",
"template_name": "Test Discount Template", "template_name": template_name,
"allocate_payment_based_on_payment_terms": 1, "allocate_payment_based_on_payment_terms": 1,
"terms": [ "terms": [
{ {
"doctype": "Payment Terms Template Detail", "doctype": "Payment Terms Template Detail",
"payment_term": "30 Credit Days with 10% Discount", "payment_term": name or "30 Credit Days with 10% Discount",
"invoice_portion": 100, "invoice_portion": 100,
"credit_days_based_on": "Day(s) after invoice date", "credit_days_based_on": "Day(s) after invoice date",
"credit_days": 2, "credit_days": 2,
"discount": 10, "discount_type": discount_type or "Percentage",
"discount": discount or 10,
"discount_validity_based_on": "Day(s) after invoice date", "discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 1, "discount_validity": 1,
} }

View File

@ -3,6 +3,7 @@
"creation": "2016-06-15 15:56:30.815503", "creation": "2016-06-15 15:56:30.815503",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [ "field_order": [
"account", "account",
"cost_center", "cost_center",
@ -17,9 +18,7 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Account", "label": "Account",
"options": "Account", "options": "Account",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "cost_center", "fieldname": "cost_center",
@ -28,37 +27,30 @@
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center", "options": "Cost Center",
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "amount", "fieldname": "amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Amount", "label": "Amount (Company Currency)",
"reqd": 1, "options": "Company:company:default_currency",
"show_days": 1, "reqd": 1
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Description", "label": "Description"
"show_days": 1,
"show_seconds": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-12 20:38:08.110674", "modified": "2023-03-06 07:11:57.739619",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry Deduction", "name": "Payment Entry Deduction",
@ -66,5 +58,6 @@
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -272,4 +272,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
} }
}; };
frappe.ui.form.on('Payment Reconciliation Allocation', {
allocated_amount: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
// filter invoice
let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number));
// filter payment
let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name));
frm.call({
doc: frm.doc,
method: 'calculate_difference_on_allocation_change',
args: {
payment_entry: payment,
invoice: invoice,
allocated_amount: row.allocated_amount
},
callback: (r) => {
if (r.message) {
row.difference_amount = r.message;
frm.refresh();
}
}
});
}
});
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm})); extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));

View File

@ -221,15 +221,27 @@ class PaymentReconciliation(Document):
def get_difference_amount(self, payment_entry, invoice, allocated_amount): def get_difference_amount(self, payment_entry, invoice, allocated_amount):
difference_amount = 0 difference_amount = 0
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( if frappe.get_cached_value(
"exchange_rate", 1 "Account", self.receivable_payable_account, "account_currency"
): ) != frappe.get_cached_value("Company", self.company, "default_currency"):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount "exchange_rate", 1
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate ):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount return difference_amount
@frappe.whitelist()
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
new_difference_amount = self.get_difference_amount(
payment_entry[0], invoice[0], allocated_amount
)
return new_difference_amount
@frappe.whitelist() @frappe.whitelist()
def allocate_entries(self, args): def allocate_entries(self, args):
self.validate_entries() self.validate_entries()

View File

@ -5,7 +5,7 @@ import unittest
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate from frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center from erpnext import get_default_cost_center
@ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile() pr.reconcile()
si.reload() si.reload()
@ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile() pr.reconcile()
# check PR tool output # check PR tool output
@ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile() pr.reconcile()
# assert outstanding # assert outstanding
@ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile() pr.reconcile()
self.assertEqual(pr.get("invoices"), []) self.assertEqual(pr.get("invoices"), [])
@ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase):
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name]) self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
@change_settings(
"Accounts Settings",
{
"allow_multi_currency_invoices_against_single_party_account": 1,
},
)
def test_no_difference_amount_for_base_currency_accounts(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debit_to
si.save().submit()
# Make payment using Payment Entry
pe1 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer,
paid_from=self.debit_to,
paid_to=self.bank,
paid_amount=100,
)
pe1.save()
pe1.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer
pr.receivable_payable_account = self.debit_to
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0)
def make_customer(customer_name, currency=None): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):

View File

@ -495,26 +495,28 @@ def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype""" """get amount based on doctype"""
dt = ref_doc.doctype dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]: if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid) grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
elif dt in ["Sales Invoice", "Purchase Invoice"]: elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency: if not ref_doc.get("is_pos"):
grand_total = flt(ref_doc.outstanding_amount) if ref_doc.party_account_currency == ref_doc.currency:
else: grand_total = flt(ref_doc.outstanding_amount)
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
elif dt == "POS Invoice": elif dt == "POS Invoice":
for pay in ref_doc.payments: for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account: if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount grand_total = pay.amount
break break
elif dt == "Fees": elif dt == "Fees":
grand_total = ref_doc.outstanding_amount grand_total = ref_doc.outstanding_amount
if grand_total > 0: if grand_total > 0:
return grand_total return grand_total
else: else:
frappe.throw(_("Payment Entry is already created")) frappe.throw(_("Payment Entry is already created"))

View File

@ -6,6 +6,7 @@ import unittest
import frappe import frappe
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
@ -45,7 +46,10 @@ class TestPaymentRequest(unittest.TestCase):
frappe.get_doc(method).insert(ignore_permissions=True) frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self): def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR", do_not_save=True)
so_inr.disable_rounded_total = 1
so_inr.save()
pr = make_payment_request( pr = make_payment_request(
dt="Sales Order", dt="Sales Order",
dn=so_inr.name, dn=so_inr.name,
@ -71,6 +75,29 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD") self.assertEqual(pr.currency, "USD")
def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice(
customer="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
)
pr = make_payment_request(
dt="Purchase Invoice",
dn=si_usd.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
pe = pr.create_payment_entry()
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
def test_payment_entry(self): def test_payment_entry(self):
frappe.db.set_value( frappe.db.set_value(
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"

View File

@ -4,12 +4,13 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.query_builder.functions import Sum
from frappe.utils import add_days, flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
) )
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@ -20,9 +21,17 @@ class PeriodClosingVoucher(AccountsController):
def on_submit(self): def on_submit(self):
self.db_set("gle_processing_status", "In Progress") self.db_set("gle_processing_status", "In Progress")
self.make_gl_entries() get_opening_entries = False
if not frappe.db.exists(
"Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)}
):
get_opening_entries = True
self.make_gl_entries(get_opening_entries=get_opening_entries)
def on_cancel(self): def on_cancel(self):
self.validate_future_closing_vouchers()
self.db_set("gle_processing_status", "In Progress") self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
gle_count = frappe.db.count( gle_count = frappe.db.count(
@ -42,6 +51,25 @@ class PeriodClosingVoucher(AccountsController):
else: else:
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name) make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
self.delete_closing_entries()
def validate_future_closing_vouchers(self):
if frappe.db.exists(
"Period Closing Voucher",
{"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company},
):
frappe.throw(
_(
"You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first"
)
)
def delete_closing_entries(self):
closing_balance = frappe.qb.DocType("Account Closing Balance")
frappe.qb.from_(closing_balance).delete().where(
closing_balance.period_closing_voucher == self.name
).run()
def validate_account_head(self): def validate_account_head(self):
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type") closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
@ -56,8 +84,6 @@ class PeriodClosingVoucher(AccountsController):
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
def validate_posting_date(self): def validate_posting_date(self):
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
validate_fiscal_year( validate_fiscal_year(
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
) )
@ -66,6 +92,8 @@ class PeriodClosingVoucher(AccountsController):
self.posting_date, self.fiscal_year, company=self.company self.posting_date, self.fiscal_year, company=self.company
)[1] )[1]
self.check_if_previous_year_closed()
pce = frappe.db.sql( pce = frappe.db.sql(
"""select name from `tabPeriod Closing Voucher` """select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""", where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
@ -78,28 +106,64 @@ class PeriodClosingVoucher(AccountsController):
) )
) )
def make_gl_entries(self): def check_if_previous_year_closed(self):
last_year_closing = add_days(self.year_start_date, -1)
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
if previous_fiscal_year and not frappe.db.exists(
"GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company}
):
return
if previous_fiscal_year and not frappe.db.exists(
"Period Closing Voucher",
{"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company},
):
frappe.throw(_("Previous Year is not closed, please close it first"))
def make_gl_entries(self, get_opening_entries=False):
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
if gl_entries: if gl_entries:
if len(gl_entries) > 5000: if len(gl_entries) > 5000:
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long") frappe.enqueue(
process_gl_entries,
gl_entries=gl_entries,
closing_entries=closing_entries,
voucher_name=self.name,
queue="long",
)
frappe.msgprint( frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."), _("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True, alert=True,
) )
else: else:
process_gl_entries(gl_entries) process_gl_entries(gl_entries, closing_entries, voucher_name=self.name)
def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = []
for acc in self.get_balances_based_on_dimensions(
group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries
):
closing_entries.append(self.get_closing_entries(acc))
return closing_entries
def get_gl_entries(self): def get_gl_entries(self):
gl_entries = [] gl_entries = []
# pl account # pl account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True): for acc in self.get_balances_based_on_dimensions(
group_by_account=True, report_type="Profit and Loss"
):
if flt(acc.bal_in_company_currency): if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_pl_account(acc)) gl_entries.append(self.get_gle_for_pl_account(acc))
# closing liability account # closing liability account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False): for acc in self.get_balances_based_on_dimensions(
group_by_account=False, report_type="Profit and Loss"
):
if flt(acc.bal_in_company_currency): if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_closing_account(acc)) gl_entries.append(self.get_gle_for_closing_account(acc))
@ -108,6 +172,8 @@ class PeriodClosingVoucher(AccountsController):
def get_gle_for_pl_account(self, acc): def get_gle_for_pl_account(self, acc):
gl_entry = self.get_gl_dict( gl_entry = self.get_gl_dict(
{ {
"company": self.company,
"closing_date": self.posting_date,
"account": acc.account, "account": acc.account,
"cost_center": acc.cost_center, "cost_center": acc.cost_center,
"finance_book": acc.finance_book, "finance_book": acc.finance_book,
@ -120,6 +186,7 @@ class PeriodClosingVoucher(AccountsController):
if flt(acc.bal_in_account_currency) > 0 if flt(acc.bal_in_account_currency) > 0
else 0, else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"is_period_closing_voucher_entry": 1,
}, },
item=acc, item=acc,
) )
@ -129,6 +196,8 @@ class PeriodClosingVoucher(AccountsController):
def get_gle_for_closing_account(self, acc): def get_gle_for_closing_account(self, acc):
gl_entry = self.get_gl_dict( gl_entry = self.get_gl_dict(
{ {
"company": self.company,
"closing_date": self.posting_date,
"account": self.closing_account_head, "account": self.closing_account_head,
"cost_center": acc.cost_center, "cost_center": acc.cost_center,
"finance_book": acc.finance_book, "finance_book": acc.finance_book,
@ -141,12 +210,36 @@ class PeriodClosingVoucher(AccountsController):
if flt(acc.bal_in_account_currency) < 0 if flt(acc.bal_in_account_currency) < 0
else 0, else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
"is_period_closing_voucher_entry": 1,
}, },
item=acc, item=acc,
) )
self.update_default_dimensions(gl_entry, acc) self.update_default_dimensions(gl_entry, acc)
return gl_entry return gl_entry
def get_closing_entries(self, acc):
closing_entry = self.get_gl_dict(
{
"company": self.company,
"closing_date": self.posting_date,
"period_closing_voucher": self.name,
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": flt(acc.debit_in_account_currency),
"debit": flt(acc.debit),
"credit_in_account_currency": flt(acc.credit_in_account_currency),
"credit": flt(acc.credit),
},
item=acc,
)
for dimension in self.accounting_dimensions:
closing_entry.update({dimension: acc.get(dimension)})
return closing_entry
def update_default_dimensions(self, gl_entry, acc): def update_default_dimensions(self, gl_entry, acc):
if not self.accounting_dimensions: if not self.accounting_dimensions:
self.accounting_dimensions = get_accounting_dimensions() self.accounting_dimensions = get_accounting_dimensions()
@ -154,47 +247,88 @@ class PeriodClosingVoucher(AccountsController):
for dimension in self.accounting_dimensions: for dimension in self.accounting_dimensions:
gl_entry.update({dimension: acc.get(dimension)}) gl_entry.update({dimension: acc.get(dimension)})
def get_pl_balances_based_on_dimensions(self, group_by_account=False): def get_balances_based_on_dimensions(
self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False
):
"""Get balance for dimension-wise pl accounts""" """Get balance for dimension-wise pl accounts"""
dimension_fields = ["t1.cost_center", "t1.finance_book"] qb_dimension_fields = ["cost_center", "finance_book", "project"]
self.accounting_dimensions = get_accounting_dimensions() self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions: for dimension in self.accounting_dimensions:
dimension_fields.append("t1.{0}".format(dimension)) qb_dimension_fields.append(dimension)
if group_by_account: if group_by_account:
dimension_fields.append("t1.account") qb_dimension_fields.append("account")
return frappe.db.sql( account_filters = {
""" "company": self.company,
select "is_group": 0,
t2.account_currency, }
{dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency, if report_type:
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency account_filters.update({"report_type": report_type})
from `tabGL Entry` t1, `tabAccount` t2
where accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
t1.is_cancelled = 0
and t1.account = t2.name gl_entry = frappe.qb.DocType("GL Entry")
and t2.report_type = 'Profit and Loss' query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
and t2.docstatus < 2
and t2.company = %s if not for_aggregation:
and t1.posting_date between %s and %s query = query.select(
group by {dimension_fields} (Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
""".format( "bal_in_account_currency"
dimension_fields=", ".join(dimension_fields) ),
), (Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
(self.company, self.get("year_start_date"), self.posting_date), )
as_dict=1, else:
query = query.select(
(Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"),
(Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"),
(Sum(gl_entry.debit)).as_("debit"),
(Sum(gl_entry.credit)).as_("credit"),
)
for dimension in qb_dimension_fields:
query = query.select(gl_entry[dimension])
query = query.where(
(gl_entry.company == self.company)
& (gl_entry.is_cancelled == 0)
& (gl_entry.account.isin(accounts))
) )
if get_opening_entries:
query = query.where(
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
| gl_entry.is_opening
== "Yes"
)
else:
query = query.where(
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
& gl_entry.is_opening
== "No"
)
def process_gl_entries(gl_entries): if for_aggregation:
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
for dimension in qb_dimension_fields:
query = query.groupby(gl_entry[dimension])
return query.run(as_dict=1)
def process_gl_entries(gl_entries, closing_entries, voucher_name=None):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
try: try:
make_gl_entries(gl_entries, merge_entries=False) make_gl_entries(gl_entries, merge_entries=False)
make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name)
frappe.db.set_value( frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed" "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
) )

View File

@ -16,16 +16,17 @@ from erpnext.accounts.utils import get_fiscal_year, now
class TestPeriodClosingVoucher(unittest.TestCase): class TestPeriodClosingVoucher(unittest.TestCase):
def test_closing_entry(self): def test_closing_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company() company = create_company()
cost_center = create_cost_center("Test Cost Center 1") cost_center = create_cost_center("Test Cost Center 1")
jv1 = make_journal_entry( jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400, amount=400,
account1="Cash - TPC", account1="Cash - TPC",
account2="Sales - TPC", account2="Sales - TPC",
cost_center=cost_center, cost_center=cost_center,
posting_date=now(),
save=False, save=False,
) )
jv1.company = company jv1.company = company
@ -33,18 +34,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
jv1.submit() jv1.submit()
jv2 = make_journal_entry( jv2 = make_journal_entry(
posting_date="2021-03-15",
amount=600, amount=600,
account1="Cost of Goods Sold - TPC", account1="Cost of Goods Sold - TPC",
account2="Cash - TPC", account2="Cash - TPC",
cost_center=cost_center, cost_center=cost_center,
posting_date=now(),
save=False, save=False,
) )
jv2.company = company jv2.company = company
jv2.save() jv2.save()
jv2.submit() jv2.submit()
pcv = self.make_period_closing_voucher() pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
surplus_account = pcv.closing_account_head surplus_account = pcv.closing_account_head
expected_gle = ( expected_gle = (
@ -65,6 +66,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
def test_cost_center_wise_posting(self): def test_cost_center_wise_posting(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company() company = create_company()
surplus_account = create_account() surplus_account = create_account()
@ -81,6 +83,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC", debit_to="Debtors - TPC",
currency="USD", currency="USD",
customer="_Test Customer USD", customer="_Test Customer USD",
posting_date="2021-03-15",
) )
create_sales_invoice( create_sales_invoice(
company=company, company=company,
@ -91,9 +94,10 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC", debit_to="Debtors - TPC",
currency="USD", currency="USD",
customer="_Test Customer USD", customer="_Test Customer USD",
posting_date="2021-03-15",
) )
pcv = self.make_period_closing_voucher(submit=False) pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False)
pcv.save() pcv.save()
pcv.submit() pcv.submit()
surplus_account = pcv.closing_account_head surplus_account = pcv.closing_account_head
@ -128,12 +132,13 @@ class TestPeriodClosingVoucher(unittest.TestCase):
def test_period_closing_with_finance_book_entries(self): def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company() company = create_company()
surplus_account = create_account() surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1") cost_center = create_cost_center("Test Cost Center 1")
si = create_sales_invoice( create_sales_invoice(
company=company, company=company,
income_account="Sales - TPC", income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC", expense_account="Cost of Goods Sold - TPC",
@ -142,6 +147,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC", debit_to="Debtors - TPC",
currency="USD", currency="USD",
customer="_Test Customer USD", customer="_Test Customer USD",
posting_date="2021-03-15",
) )
jv = make_journal_entry( jv = make_journal_entry(
@ -149,14 +155,14 @@ class TestPeriodClosingVoucher(unittest.TestCase):
account2="Sales - TPC", account2="Sales - TPC",
amount=400, amount=400,
cost_center=cost_center, cost_center=cost_center,
posting_date=now(), posting_date="2021-03-15",
) )
jv.company = company jv.company = company
jv.finance_book = create_finance_book().name jv.finance_book = create_finance_book().name
jv.save() jv.save()
jv.submit() jv.submit()
pcv = self.make_period_closing_voucher() pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
surplus_account = pcv.closing_account_head surplus_account = pcv.closing_account_head
expected_gle = ( expected_gle = (
@ -177,14 +183,130 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertSequenceEqual(pcv_gle, expected_gle) self.assertSequenceEqual(pcv_gle, expected_gle)
def make_period_closing_voucher(self, submit=True): def test_gl_entries_restrictions(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
self.make_period_closing_voucher(posting_date="2021-03-31")
jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
save=False,
)
jv1.company = company
jv1.save()
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
company = create_company()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center1,
save=False,
)
jv1.company = company
jv1.save()
jv1.submit()
jv2 = make_journal_entry(
posting_date="2021-03-15",
amount=200,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
save=False,
)
jv2.company = company
jv2.save()
jv2.submit()
pcv1 = self.make_period_closing_voucher(posting_date="2021-03-31")
closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center1,
"period_closing_voucher": pcv1.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
self.assertEqual(closing_balance.credit, 400)
self.assertEqual(closing_balance.credit_in_account_currency, 400)
jv3 = make_journal_entry(
posting_date="2022-03-15",
amount=300,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
save=False,
)
jv3.company = company
jv3.save()
jv3.submit()
pcv2 = self.make_period_closing_voucher(posting_date="2022-03-31")
cc1_closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center1,
"period_closing_voucher": pcv2.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
cc2_closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center2,
"period_closing_voucher": pcv2.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
self.assertEqual(cc1_closing_balance.credit, 400)
self.assertEqual(cc1_closing_balance.credit_in_account_currency, 400)
self.assertEqual(cc2_closing_balance.credit, 500)
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
def make_period_closing_voucher(self, posting_date=None, submit=True):
surplus_account = create_account() surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1") cost_center = create_cost_center("Test Cost Center 1")
pcv = frappe.get_doc( pcv = frappe.get_doc(
{ {
"doctype": "Period Closing Voucher", "doctype": "Period Closing Voucher",
"transaction_date": today(), "transaction_date": posting_date or today(),
"posting_date": today(), "posting_date": posting_date or today(),
"company": "Test PCV Company", "company": "Test PCV Company",
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
"cost_center": cost_center, "cost_center": cost_center,

View File

@ -112,7 +112,8 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
party_type: "Customer", party_type: "Customer",
account: this.frm.doc.debit_to, account: this.frm.doc.debit_to,
price_list: this.frm.doc.selling_price_list, price_list: this.frm.doc.selling_price_list,
pos_profile: pos_profile pos_profile: pos_profile,
company_address: this.frm.doc.company_address
}, () => { }, () => {
this.apply_pricing_rule(); this.apply_pricing_rule();
}); });

View File

@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
bold_item_name = frappe.bold(item.item_name) bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold( bold_extra_batch_qty_needed = frappe.bold(
abs(available_batch_qty - reserved_batch_qty - item.qty) abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
) )
bold_invalid_batch_no = frappe.bold(item.batch_no) bold_invalid_batch_no = frappe.bold(item.batch_no)
@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice):
).format(item.idx, bold_invalid_batch_no, bold_item_name), ).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"), title=_("Item Unavailable"),
) )
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
frappe.throw( frappe.throw(
_( _(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
), ),
title=_("Item Unavailable"), title=_("Item Unavailable"),
) )
elif is_stock_item and flt(available_stock) < flt(d.qty): elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw( frappe.throw(
_( _(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@ -651,7 +651,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty max_available_bundles = available_qty / item.stock_qty
if bundle_bin_qty > max_available_bundles and frappe.get_value( if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item" "Item", item.item_code, "is_stock_item"
): ):

View File

@ -15,7 +15,7 @@
</div> </div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2> <h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div> <div>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5> <h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
<h5 style="float: right;"> <h5 style="float: right;">
{{ _("Date: ") }} {{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}} <b>{{ frappe.format(filters.from_date, 'Date')}}

View File

@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document): class ProcessStatementOfAccounts(Document):
def validate(self): def validate(self):
if not self.subject: if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.name }}" self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body: if not self.body:
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True):
"account": [doc.account] if doc.account else None, "account": [doc.account] if doc.account else None,
"party_type": "Customer", "party_type": "Customer",
"party": [entry.customer], "party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency, "presentation_currency": presentation_currency,
"group_by": doc.group_by, "group_by": doc.group_by,
"currency": doc.currency, "currency": doc.currency,
@ -156,7 +157,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
] ]
return frappe.get_list( return frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]], filters=[[fields_dict[customer_collection], "IN", selected]],
) )
@ -179,7 +180,7 @@ def get_customers_based_on_sales_person(sales_person):
if sales_person_records.get("Customer"): if sales_person_records.get("Customer"):
return frappe.get_list( return frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[["name", "in", list(sales_person_records["Customer"])]], filters=[["name", "in", list(sales_person_records["Customer"])]],
) )
else: else:
@ -228,7 +229,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == "Sales Partner": if customer_collection == "Sales Partner":
customers = frappe.get_list( customers = frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[["default_sales_partner", "=", collection_name]], filters=[["default_sales_partner", "=", collection_name]],
) )
else: else:
@ -245,7 +246,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
continue continue
customer_list.append( customer_list.append(
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email} {
"name": customer.name,
"customer_name": customer.customer_name,
"primary_email": primary_email,
"billing_email": billing_email,
}
) )
return customer_list return customer_list

View File

@ -1,12 +1,12 @@
{ {
"actions": [], "actions": [],
"allow_workflow": 1,
"creation": "2020-08-03 16:35:21.852178", "creation": "2020-08-03 16:35:21.852178",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"customer", "customer",
"customer_name",
"billing_email", "billing_email",
"primary_email" "primary_email"
], ],
@ -30,11 +30,18 @@
"fieldtype": "Read Only", "fieldtype": "Read Only",
"in_list_view": 1, "in_list_view": 1,
"label": "Billing Email" "label": "Billing Email"
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-08-03 22:55:38.875601", "modified": "2023-03-13 00:12:34.508086",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Statement Of Accounts Customer", "name": "Process Statement Of Accounts Customer",
@ -43,5 +50,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

@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
if(doc.docstatus == 1 && doc.outstanding_amount != 0 if(doc.docstatus == 1 && doc.outstanding_amount != 0
&& !(doc.is_return && doc.return_against) && !doc.on_hold) { && !(doc.is_return && doc.return_against) && !doc.on_hold) {
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create')); this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
__('Create')
);
cur_frm.page.set_inner_btn_group_as_primary(__('Create')); cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
} }

View File

@ -118,6 +118,7 @@
"paid_amount", "paid_amount",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"only_include_allocated_payments",
"get_advances", "get_advances",
"advances", "advances",
"advance_tax", "advance_tax",
@ -1550,17 +1551,24 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"default": "0",
"depends_on": "allocate_advances_automatically",
"description": "Advance payments allocated against orders will only be fetched",
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-28 19:18:56.586321", "modified": "2023-04-03 22:57:14.074982",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -117,7 +117,7 @@ class PurchaseInvoice(BuyingController):
self.validate_expense_account() self.validate_expense_account()
self.set_against_expense_account() self.set_against_expense_account()
self.validate_write_off_account() self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
self.create_remarks() self.create_remarks()
self.set_status() self.set_status()
self.validate_purchase_receipt_if_update_stock() self.validate_purchase_receipt_if_update_stock()
@ -232,7 +232,7 @@ class PurchaseInvoice(BuyingController):
) )
if ( if (
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
and not self.is_return and not self.is_return
and not self.is_internal_supplier and not self.is_internal_supplier
): ):
@ -581,6 +581,7 @@ class PurchaseInvoice(BuyingController):
self.make_supplier_gl_entry(gl_entries) self.make_supplier_gl_entry(gl_entries)
self.make_item_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries)
if self.check_asset_cwip_enabled(): if self.check_asset_cwip_enabled():
self.get_asset_gl_entry(gl_entries) self.get_asset_gl_entry(gl_entries)
@ -975,6 +976,28 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount") item.item_tax_amount, item.precision("item_tax_amount")
) )
def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
)
precision_loss = self.get("base_net_total") - flt(
self.get("net_total") * self.conversion_rate, self.precision("net_total")
)
if precision_loss:
gl_entries.append(
self.get_gl_dict(
{
"account": round_off_account,
"against": self.supplier,
"credit": precision_loss,
"cost_center": self.cost_center or round_off_cost_center,
"remarks": _("Net total calculation precision loss"),
}
)
)
def get_asset_gl_entry(self, gl_entries): def get_asset_gl_entry(self, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed") arbnb_account = self.get_company_default("asset_received_but_not_billed")
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
@ -1485,11 +1508,17 @@ class PurchaseInvoice(BuyingController):
if po_details: if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified) updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
adjust_incoming_rate = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
for pr in set(updated_pr): for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
pr_doc = frappe.get_doc("Purchase Receipt", pr) pr_doc = frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified) update_billing_percentage(
pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate
)
def get_pr_details_billed_amt(self): def get_pr_details_billed_amt(self):
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice # Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice

View File

@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
company.enable_provisional_accounting_for_non_stock_items = 0 company.enable_provisional_accounting_for_non_stock_items = 0
company.save() company.save()
def test_adjust_incoming_rate(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1
)
# Increase the cost of the item
pr = make_purchase_receipt(qty=1, rate=100)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 150
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 150)
# Reduce the cost of the item
pr = make_purchase_receipt(qty=1, rate=100)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 50
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 50)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0
)
# Don't adjust incoming rate
pr = make_purchase_receipt(qty=1, rate=100)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 50
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def test_item_less_defaults(self): def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice") pi = frappe.new_doc("Purchase Invoice")

View File

@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
if (doc.docstatus == 1 && doc.outstanding_amount!=0 if (doc.docstatus == 1 && doc.outstanding_amount!=0
&& !(cint(doc.is_return) && doc.return_against)) { && !(cint(doc.is_return) && doc.return_against)) {
cur_frm.add_custom_button(__('Payment'), this.frm.add_custom_button(
this.make_payment_entry, __('Create')); __('Payment'),
cur_frm.page.set_inner_btn_group_as_primary(__('Create')); () => this.make_payment_entry(),
__('Create')
);
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
} }
if(doc.docstatus==1 && !doc.is_return) { if(doc.docstatus==1 && !doc.is_return) {

View File

@ -32,9 +32,6 @@
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
"project", "project",
"column_break_27",
"campaign",
"source",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@ -123,6 +120,7 @@
"account_for_change_amount", "account_for_change_amount",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"only_include_allocated_payments",
"get_advances", "get_advances",
"advances", "advances",
"write_off_section", "write_off_section",
@ -203,7 +201,9 @@
"more_information", "more_information",
"status", "status",
"inter_company_invoice_reference", "inter_company_invoice_reference",
"campaign",
"represents_company", "represents_company",
"source",
"customer_group", "customer_group",
"col_break23", "col_break23",
"is_internal_customer", "is_internal_customer",
@ -2083,10 +2083,6 @@
"fieldname": "company_addr_col_break", "fieldname": "company_addr_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{ {
"fieldname": "column_break_52", "fieldname": "column_break_52",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -2131,6 +2127,14 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"default": "0",
"depends_on": "allocate_advances_automatically",
"description": "Advance payments allocated against orders will only be fetched",
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -2143,11 +2147,10 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-01-28 19:45:47.538163", "modified": "2023-04-03 22:55:14.206473",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -145,7 +145,7 @@ class SalesInvoice(SellingController):
self.set_against_income_account() self.set_against_income_account()
self.validate_time_sheets_are_submitted() self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items") self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if not self.is_return: if not self.is_return:
self.validate_serial_numbers() self.validate_serial_numbers()
else: else:

View File

@ -266,16 +266,16 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Education Cess - _TC": [3, 1618, 0.06, 32.36], "_Test Account Education Cess - _TC": [3, 1618, 0.06, 32.36],
"_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39], "_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39],
"_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04], "_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04],
"_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17], "_Test Account VAT - _TC": [156.0, 1808.0, 3.12, 36.16],
"_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55], "_Test Account Discount - _TC": [-181.0, 1627.0, -3.62, 32.54],
} }
for d in si.get("taxes"): for d in si.get("taxes"):
for i, k in enumerate(expected_values["keys"]): for i, k in enumerate(expected_values["keys"]):
self.assertEqual(d.get(k), expected_values[d.account_head][i]) self.assertEqual(d.get(k), expected_values[d.account_head][i])
self.assertEqual(si.base_grand_total, 1627.5) self.assertEqual(si.base_grand_total, 1627.0)
self.assertEqual(si.grand_total, 32.55) self.assertEqual(si.grand_total, 32.54)
def test_sales_invoice_with_discount_and_inclusive_tax(self): def test_sales_invoice_with_discount_and_inclusive_tax(self):
si = create_sales_invoice(qty=100, rate=50, do_not_save=True) si = create_sales_invoice(qty=100, rate=50, do_not_save=True)
@ -401,10 +401,10 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account S&H Education Cess - _TC": [1.4, 1.30, 1297.67], "_Test Account S&H Education Cess - _TC": [1.4, 1.30, 1297.67],
"_Test Account CST - _TC": [27.88, 25.95, 1323.62], "_Test Account CST - _TC": [27.88, 25.95, 1323.62],
"_Test Account VAT - _TC": [156.25, 145.43, 1469.05], "_Test Account VAT - _TC": [156.25, 145.43, 1469.05],
"_Test Account Customs Duty - _TC": [125, 116.35, 1585.40], "_Test Account Customs Duty - _TC": [125, 116.34, 1585.39],
"_Test Account Shipping Charges - _TC": [100, 100, 1685.40], "_Test Account Shipping Charges - _TC": [100, 100, 1685.39],
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.86], "_Test Account Discount - _TC": [-180.33, -168.54, 1516.85],
"_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01], "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.00],
} }
for d in si.get("taxes"): for d in si.get("taxes"):
@ -413,7 +413,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.base_grand_total, 1500) self.assertEqual(si.base_grand_total, 1500)
self.assertEqual(si.grand_total, 1500) self.assertEqual(si.grand_total, 1500)
self.assertEqual(si.rounding_adjustment, -0.01) self.assertEqual(si.rounding_adjustment, 0.0)
def test_discount_amount_gl_entry(self): def test_discount_amount_gl_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
@ -454,7 +454,7 @@ class TestSalesInvoice(unittest.TestCase):
[test_records[3]["taxes"][2]["account_head"], 0.0, 1.30], [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30],
[test_records[3]["taxes"][3]["account_head"], 0.0, 25.95], [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95],
[test_records[3]["taxes"][4]["account_head"], 0.0, 145.43], [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43],
[test_records[3]["taxes"][5]["account_head"], 0.0, 116.35], [test_records[3]["taxes"][5]["account_head"], 0.0, 116.34],
[test_records[3]["taxes"][6]["account_head"], 0.0, 100], [test_records[3]["taxes"][6]["account_head"], 0.0, 100],
[test_records[3]["taxes"][7]["account_head"], 168.54, 0.0], [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0],
["_Test Account Service Tax - _TC", 16.85, 0.0], ["_Test Account Service Tax - _TC", 16.85, 0.0],
@ -1614,7 +1614,7 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Education Cess - _TC": [1.4, 1.4, 1.4], "_Test Account Education Cess - _TC": [1.4, 1.4, 1.4],
"_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7], "_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7],
"_Test Account CST - _TC": [17.19, 17.19, 17.19], "_Test Account CST - _TC": [17.19, 17.19, 17.19],
"_Test Account VAT - _TC": [78.13, 78.13, 78.13], "_Test Account VAT - _TC": [78.12, 78.12, 78.12],
"_Test Account Discount - _TC": [-95.49, -95.49, -95.49], "_Test Account Discount - _TC": [-95.49, -95.49, -95.49],
} }
@ -1623,9 +1623,9 @@ class TestSalesInvoice(unittest.TestCase):
if expected_values.get(d.account_head): if expected_values.get(d.account_head):
self.assertEqual(d.get(k), expected_values[d.account_head][i]) self.assertEqual(d.get(k), expected_values[d.account_head][i])
self.assertEqual(si.total_taxes_and_charges, 234.43) self.assertEqual(si.total_taxes_and_charges, 234.42)
self.assertEqual(si.base_grand_total, 859.43) self.assertEqual(si.base_grand_total, 859.42)
self.assertEqual(si.grand_total, 859.43) self.assertEqual(si.grand_total, 859.42)
def test_multi_currency_gle(self): def test_multi_currency_gle(self):
si = create_sales_invoice( si = create_sales_invoice(
@ -1985,17 +1985,17 @@ class TestSalesInvoice(unittest.TestCase):
) )
si.save() si.save()
si.submit() si.submit()
self.assertEqual(si.net_total, 19453.13) self.assertEqual(si.net_total, 19453.12)
self.assertEqual(si.grand_total, 24900) self.assertEqual(si.grand_total, 24900)
self.assertEqual(si.total_taxes_and_charges, 5446.88) self.assertEqual(si.total_taxes_and_charges, 5446.88)
self.assertEqual(si.rounding_adjustment, -0.01) self.assertEqual(si.rounding_adjustment, 0.0)
expected_values = dict( expected_values = dict(
(d[0], d) (d[0], d)
for d in [ for d in [
[si.debit_to, 24900, 0.0], [si.debit_to, 24900, 0.0],
["_Test Account Service Tax - _TC", 0.0, 5446.88], ["_Test Account Service Tax - _TC", 0.0, 5446.88],
["Sales - _TC", 0.0, 19453.13], ["Sales - _TC", 0.0, 19453.12],
["Round Off - _TC", 0.01, 0.0], ["Round Off - _TC", 0.01, 0.0],
] ]
) )

View File

@ -300,6 +300,9 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if gl_map: if gl_map:
check_freezing_date(gl_map[0]["posting_date"], adv_adj) check_freezing_date(gl_map[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
if gl_map[0]["voucher_type"] != "Period Closing Voucher":
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
for entry in gl_map: for entry in gl_map:
make_entry(entry, adv_adj, update_outstanding, from_repost) make_entry(entry, adv_adj, update_outstanding, from_repost)
@ -519,6 +522,9 @@ def make_reverse_gl_entries(
) )
validate_accounting_period(gl_entries) validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj) check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
for entry in gl_entries: for entry in gl_entries:
@ -566,6 +572,28 @@ def check_freezing_date(posting_date, adv_adj=False):
) )
def validate_against_pcv(is_opening, posting_date, company):
if is_opening and frappe.db.exists(
"Period Closing Voucher", {"docstatus": 1, "company": company}
):
frappe.throw(
_("Opening Entry can not be created after Period Closing Voucher is created."),
title=_("Invalid Opening Entry"),
)
last_pcv_date = frappe.db.get_value(
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)"
)
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
message = _("Books have been closed till the period ending on {0}").format(
formatdate(last_pcv_date)
)
message += "</br >"
message += _("You cannot create/amend any accounting entries till this date.")
frappe.throw(message, title=_("Period Closed"))
def set_as_cancel(voucher_type, voucher_no): def set_as_cancel(voucher_type, voucher_no):
""" """
Set is_cancelled=1 in all original gl entries for the voucher Set is_cancelled=1 in all original gl entries for the voucher

View File

@ -32,6 +32,16 @@ from erpnext import get_company_currency
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
SALES_TRANSACTION_TYPES = {
"Quotation",
"Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
}
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
class DuplicatePartyAccountError(frappe.ValidationError): class DuplicatePartyAccountError(frappe.ValidationError):
pass pass
@ -124,12 +134,6 @@ def _get_party_details(
set_other_values(party_details, party, party_type) set_other_values(party_details, party, party_type)
set_price_list(party_details, party, party_type, price_list, pos_profile) set_price_list(party_details, party, party_type, price_list, pos_profile)
party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"),
party_address,
shipping_address if party_type != "Supplier" else party_address,
)
tax_template = set_taxes( tax_template = set_taxes(
party.name, party.name,
party_type, party_type,
@ -170,6 +174,9 @@ def _get_party_details(
party_type, party.name, "tax_withholding_category" party_type, party.name, "tax_withholding_category"
) )
if not party_details.get("tax_category") and pos_profile:
party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category")
return party_details return party_details
@ -211,20 +218,10 @@ def set_address_details(
else: else:
party_details.update(get_company_address(company)) party_details.update(get_company_address(company))
if doctype and doctype in [ if doctype in SALES_TRANSACTION_TYPES and party_details.company_address:
"Delivery Note", party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address))
"Sales Invoice",
"Sales Order",
"Quotation",
"POS Invoice",
]:
if party_details.company_address:
party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address)
)
get_regional_address_details(party_details, doctype, company)
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: if doctype in PURCHASE_TRANSACTION_TYPES:
if shipping_address: if shipping_address:
party_details.update( party_details.update(
shipping_address=shipping_address, shipping_address=shipping_address,
@ -250,9 +247,21 @@ def set_address_details(
**get_fetch_values(doctype, "shipping_address", party_details.billing_address) **get_fetch_values(doctype, "shipping_address", party_details.billing_address)
) )
party_address, shipping_address = (
party_details.get(billing_address_field),
party_details.shipping_address_name,
)
party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"),
party_address,
shipping_address if party_type != "Supplier" else party_address,
)
if doctype in TRANSACTION_TYPES:
get_regional_address_details(party_details, doctype, company) get_regional_address_details(party_details, doctype, company)
return party_details.get(billing_address_field), party_details.shipping_address_name return party_address, shipping_address
@erpnext.allow_regional @erpnext.allow_regional

View File

@ -859,7 +859,7 @@ class ReceivablePayableReport(object):
) )
else: else:
self.qb_selection_filter.append( self.qb_selection_filter.append(
self.ple[dimension.fieldname] == self.filters[dimension.fieldname] self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
) )
def is_invoice(self, ple): def is_invoice(self, ple):

View File

@ -25,6 +25,7 @@ def get_data(filters):
["posting_date", "<=", filters.get("to_date")], ["posting_date", "<=", filters.get("to_date")],
["against_voucher_type", "=", "Asset"], ["against_voucher_type", "=", "Asset"],
["account", "in", depreciation_accounts], ["account", "in", depreciation_accounts],
["is_cancelled", "=", 0],
] ]
if filters.get("asset"): if filters.get("asset"):

View File

@ -25,6 +25,8 @@ def execute(filters=None):
company=filters.company, company=filters.company,
) )
filters.period_start_date = period_list[0]["year_start_date"]
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"
) )
@ -96,7 +98,7 @@ 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 = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, total_credit, 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
@ -174,7 +176,6 @@ def get_report_summary(
liability, liability,
equity, equity,
provisional_profit_loss, provisional_profit_loss,
total_credit,
currency, currency,
filters, filters,
consolidated=False, consolidated=False,

View File

@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import getdate, nowdate from frappe.utils import getdate, nowdate
@ -91,4 +92,65 @@ def get_entries(filters):
as_list=1, as_list=1,
) )
return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate())) # Loan Disbursement
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
query = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document_type"),
loan_disbursement.name.as_("payment_entry"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.reference_number.as_("cheque_no"),
loan_disbursement.clearance_date.as_("clearance_date"),
loan_disbursement.applicant.as_("against"),
-loan_disbursement.disbursed_amount.as_("amount"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= filters["from_date"])
.where(loan_disbursement.disbursement_date <= filters["to_date"])
.where(loan_disbursement.disbursement_account == filters["account"])
.orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
)
if filters.get("from_date"):
query = query.where(loan_disbursement.disbursement_date >= filters["from_date"])
if filters.get("to_date"):
query = query.where(loan_disbursement.disbursement_date <= filters["to_date"])
loan_disbursements = query.run(as_list=1)
# Loan Repayment
loan_repayment = frappe.qb.DocType("Loan Repayment")
query = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document_type"),
loan_repayment.name.as_("payment_entry"),
loan_repayment.posting_date.as_("posting_date"),
loan_repayment.reference_number.as_("cheque_no"),
loan_repayment.clearance_date.as_("clearance_date"),
loan_repayment.applicant.as_("against"),
loan_repayment.amount_paid.as_("amount"),
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.posting_date >= filters["from_date"])
.where(loan_repayment.posting_date <= filters["to_date"])
.where(loan_repayment.payment_account == filters["account"])
.orderby(loan_repayment.posting_date, order=frappe.qb.desc)
.orderby(loan_repayment.name, order=frappe.qb.desc)
)
if filters.get("from_date"):
query = query.where(loan_repayment.posting_date >= filters["from_date"])
if filters.get("to_date"):
query = query.where(loan_repayment.posting_date <= filters["to_date"])
loan_repayments = query.run(as_list=1)
return sorted(
journal_entries + payment_entries + loan_disbursements + loan_repayments,
key=lambda k: k[2] or getdate(nowdate()),
)

View File

@ -118,7 +118,6 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
liability, liability,
equity, equity,
provisional_profit_loss, provisional_profit_loss,
total_credit,
company_currency, company_currency,
filters, filters,
True, True,
@ -138,7 +137,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
for data in [asset_data, liability_data, equity_data]: for data in [asset_data, liability_data, equity_data]:
if data: if data:
account_name = get_root_account_name(data[0].root_type, company) account_name = get_root_account_name(data[0].root_type, company)
opening_value += get_opening_balance(account_name, data, company) or 0.0 if account_name:
opening_value += get_opening_balance(account_name, data, company) or 0.0
opening_balance[company] = opening_value opening_balance[company] = opening_value
@ -155,7 +155,7 @@ def get_opening_balance(account_name, data, company):
def get_root_account_name(root_type, company): def get_root_account_name(root_type, company):
return frappe.get_all( root_account = frappe.get_all(
"Account", "Account",
fields=["account_name"], fields=["account_name"],
filters={ filters={
@ -165,7 +165,10 @@ def get_root_account_name(root_type, company):
"parent_account": ("is", "not set"), "parent_account": ("is", "not set"),
}, },
as_list=1, as_list=1,
)[0][0] )
if root_account:
return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters): def get_profit_loss_data(fiscal_year, companies, columns, filters):

View File

@ -418,46 +418,47 @@ def set_gl_entries_by_account(
ignore_closing_entries=False, ignore_closing_entries=False,
): ):
"""Returns a dict like { "account": [gl entries], ... }""" """Returns a dict like { "account": [gl entries], ... }"""
gl_entries = []
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) accounts_list = frappe.db.get_all(
"Account",
accounts = frappe.db.sql_list( filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
"""select name from `tabAccount` pluck="name",
where lft >= %s and rgt <= %s and company = %s""",
(root_lft, root_rgt, company),
) )
if accounts: ignore_opening_entries = False
additional_conditions += " and account in ({})".format( if accounts_list:
", ".join(frappe.db.escape(d) for d in accounts) # For balance sheet
) if not from_date:
from_date = filters["period_start_date"]
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", from_date)},
fields=["posting_date", "name"],
order_by="posting_date desc",
limit=1,
)
if last_period_closing_voucher:
gl_entries += get_accounting_entries(
"Account Closing Balance",
from_date,
to_date,
accounts_list,
filters,
ignore_closing_entries,
last_period_closing_voucher[0].name,
)
from_date = add_days(last_period_closing_voucher[0].posting_date, 1)
ignore_opening_entries = True
gl_filters = { gl_entries += get_accounting_entries(
"company": company, "GL Entry",
"from_date": from_date, from_date,
"to_date": to_date, to_date,
"finance_book": cstr(filters.get("finance_book")), accounts_list,
} filters,
ignore_closing_entries,
if filters.get("include_default_book_entries"): ignore_opening_entries=ignore_opening_entries,
gl_filters["company_fb"] = frappe.get_cached_value("Company", company, "default_finance_book")
for key, value in filters.items():
if value:
gl_filters.update({key: value})
gl_entries = frappe.db.sql(
"""
select posting_date, account, debit, credit, is_opening, fiscal_year,
debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
where company=%(company)s
{additional_conditions}
and posting_date <= %(to_date)s
and is_cancelled = 0""".format(
additional_conditions=additional_conditions
),
gl_filters,
as_dict=True,
) )
if filters and filters.get("presentation_currency"): if filters and filters.get("presentation_currency"):
@ -469,34 +470,82 @@ def set_gl_entries_by_account(
return gl_entries_by_account return gl_entries_by_account
def get_additional_conditions(from_date, ignore_closing_entries, filters): def get_accounting_entries(
additional_conditions = [] doctype,
from_date,
to_date,
accounts,
filters,
ignore_closing_entries,
period_closing_voucher=None,
ignore_opening_entries=False,
):
gl_entry = frappe.qb.DocType(doctype)
query = (
frappe.qb.from_(gl_entry)
.select(
gl_entry.account,
gl_entry.debit,
gl_entry.credit,
gl_entry.debit_in_account_currency,
gl_entry.credit_in_account_currency,
gl_entry.account_currency,
)
.where(gl_entry.company == filters.company)
)
if doctype == "GL Entry":
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
query = query.where(gl_entry.is_cancelled == 0)
query = query.where(gl_entry.posting_date <= to_date)
if ignore_opening_entries:
query = query.where(gl_entry.is_opening == "No")
else:
query = query.select(gl_entry.closing_date.as_("posting_date"))
query = query.where(gl_entry.period_closing_voucher == period_closing_voucher)
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
query = query.where(gl_entry.account.isin(accounts))
entries = query.run(as_dict=True)
return entries
def apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters):
gl_entry = frappe.qb.DocType(doctype)
accounting_dimensions = get_accounting_dimensions(as_list=False) accounting_dimensions = get_accounting_dimensions(as_list=False)
if ignore_closing_entries: if ignore_closing_entries:
additional_conditions.append("ifnull(voucher_type, '')!='Period Closing Voucher'") if doctype == "GL Entry":
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
else:
query = query.where(gl_entry.is_period_closing_voucher_entry == 0)
if from_date: if from_date and doctype == "GL Entry":
additional_conditions.append("posting_date >= %(from_date)s") query = query.where(gl_entry.posting_date >= from_date)
if filters: if filters:
if filters.get("project"): if filters.get("project"):
if not isinstance(filters.get("project"), list): if not isinstance(filters.get("project"), list):
filters.project = frappe.parse_json(filters.get("project")) filters.project = frappe.parse_json(filters.get("project"))
additional_conditions.append("project in %(project)s") query = query.where(gl_entry.project.isin(filters.project))
if filters.get("cost_center"): if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center) filters.cost_center = get_cost_centers_with_children(filters.cost_center)
additional_conditions.append("cost_center in %(cost_center)s") query = query.where(gl_entry.cost_center.isin(filters.cost_center))
if filters.get("include_default_book_entries"): if filters.get("include_default_book_entries"):
additional_conditions.append( query = query.where(
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" (gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
| (gl_entry.finance_book.isnull())
) )
else: else:
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") query = query.where(
(gl_entry.finance_book.isin([cstr(filters.company_fb), ""])) | (gl_entry.finance_book.isnull())
)
if accounting_dimensions: if accounting_dimensions:
for dimension in accounting_dimensions: for dimension in accounting_dimensions:
@ -505,11 +554,10 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
filters[dimension.fieldname] = get_dimension_with_children( filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname) dimension.document_type, filters.get(dimension.fieldname)
) )
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
else:
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" query = query.where(gl_entry[dimension.fieldname].isin(filters[dimension.fieldname]))
return query
def get_cost_centers_with_children(cost_centers): def get_cost_centers_with_children(cost_centers):

View File

@ -38,8 +38,11 @@
{% if(data[i].posting_date) { %} {% if(data[i].posting_date) { %}
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td> <td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
<td>{%= data[i].voucher_type %} <td>{%= data[i].voucher_type %}
<br>{%= data[i].voucher_no %}</td> <br>{%= data[i].voucher_no %}
<td> </td>
{% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
<td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
<span>
{% if(!(filters.party || filters.account)) { %} {% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %} {%= data[i].party || data[i].account %}
<br> <br>
@ -49,11 +52,14 @@
{% if(data[i].bill_no) { %} {% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} <br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %} {% } %}
</td> </span>
<td style="text-align: right"> </td>
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td> <td style="text-align: right">
<td style="text-align: right"> {%= format_currency(data[i].debit, filters.presentation_currency) %}
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td> </td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency) %}
</td>
{% } else { %} {% } else { %}
<td></td> <td></td>
<td></td> <td></td>

View File

@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = {
{ {
"fieldname":"party_type", "fieldname":"party_type",
"label": __("Party Type"), "label": __("Party Type"),
"fieldtype": "Link", "fieldtype": "Autocomplete",
"options": "Party Type", options: Object.keys(frappe.boot.party_account_types),
"default": "",
on_change: function() { on_change: function() {
frappe.query_report.set_filter_value('party', ""); frappe.query_report.set_filter_value('party', "");
} }

View File

@ -24,7 +24,6 @@ class TestGeneralLedger(FrappeTestCase):
"root_type": "Asset", "root_type": "Asset",
"report_type": "Balance Sheet", "report_type": "Balance Sheet",
"account_currency": "USD", "account_currency": "USD",
"inter_company_account": 0,
"parent_account": "Bank Accounts - _TC", "parent_account": "Bank Accounts - _TC",
"account_type": "Bank", "account_type": "Bank",
"doctype": "Account", "doctype": "Account",

View File

@ -501,7 +501,14 @@ class GrossProfitGenerator(object):
): ):
returned_item_rows = self.returned_invoices[row.parent][row.item_code] returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows: for returned_item_row in returned_item_rows:
row.qty += flt(returned_item_row.qty) # returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
returned_item_row.qty = 0
else:
row.qty = 0
returned_item_row.qty += row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
if flt(row.qty) or row.base_amount: if flt(row.qty) or row.base_amount:
@ -734,6 +741,8 @@ class GrossProfitGenerator(object):
if self.filters.to_date: if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s" conditions += " and posting_date <= %(to_date)s"
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
if self.filters.item_group: if self.filters.item_group:
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))

View File

@ -381,3 +381,82 @@ class TestGrossProfit(FrappeTestCase):
} }
gp_entry = [x for x in data if x.parent_invoice == sinv.name] gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0]) self.assertDictContainsSubset(expected_entry, gp_entry[0])
def test_crnote_against_invoice_with_multiple_instances_of_same_item(self):
"""
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
"""
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
# Invoice with an item added twice
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False))
sinv = sinv.save().submit()
# Create Credit Note for Invoice
cr_note = make_sales_return(sinv.name)
cr_note = cr_note.save().submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 0.0,
"avg._selling_rate": 0.0,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
# Both items of Invoice should have '0' qty
self.assertEqual(len(gp_entry), 2)
self.assertDictContainsSubset(expected_entry, gp_entry[0])
self.assertDictContainsSubset(expected_entry, gp_entry[1])
def test_standalone_cr_notes(self):
"""
Standalone cr notes will be reported as usual
"""
# Make Cr Note
sinv = self.create_sales_invoice(
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
sinv.is_return = 1
sinv = sinv.save().submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": -1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])

View File

@ -37,6 +37,29 @@ function get_filters() {
}); });
} }
}, },
{
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Link",
"options": "Party Type",
"default": "",
on_change: function() {
frappe.query_report.set_filter_value('party', "");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
},
{ {
"fieldname":"voucher_no", "fieldname":"voucher_no",
"label": __("Voucher No"), "label": __("Voucher No"),
@ -49,6 +72,20 @@ function get_filters() {
"fieldtype": "Data", "fieldtype": "Data",
"width": 100, "width": 100,
}, },
{
"fieldname":"include_account_currency",
"label": __("Include Account Currency"),
"fieldtype": "Check",
"width": 100,
},
{
"fieldname":"group_party",
"label": __("Group by Party"),
"fieldtype": "Check",
"width": 100,
},
] ]
return filters; return filters;

View File

@ -17,34 +17,26 @@ class PaymentLedger(object):
self.ple = qb.DocType("Payment Ledger Entry") self.ple = qb.DocType("Payment Ledger Entry")
def init_voucher_dict(self): def init_voucher_dict(self):
if self.voucher_amount: if self.voucher_amount:
s = set() # for each ple, using group_by_key to create a key and assign it to +/- list
# build a set of unique vouchers
for ple in self.voucher_amount: for ple in self.voucher_amount:
key = (ple.voucher_type, ple.voucher_no, ple.party) group_by_key = None
s.add(key) if not self.filters.group_party:
group_by_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
else:
group_by_key = (ple.party_type, ple.party)
# for each unique vouchers, initialize +/- list
for key in s:
self.voucher_dict[key] = frappe._dict(increase=list(), decrease=list())
# for each ple, using against voucher and amount, assign it to +/- list
# group by against voucher
for ple in self.voucher_amount:
against_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
target = None target = None
if self.voucher_dict.get(against_key): if ple.amount > 0:
if ple.amount > 0: target = self.voucher_dict.setdefault(group_by_key, {}).setdefault("increase", [])
target = self.voucher_dict.get(against_key).increase else:
else: target = self.voucher_dict.setdefault(group_by_key, {}).setdefault("decrease", [])
target = self.voucher_dict.get(against_key).decrease
# this if condition will lose unassigned ple entries(against_voucher doc doesn't have ple) # this if condition will lose unassigned ple entries(against_voucher doc doesn't have ple)
# need to somehow include the stray entries as well. # need to somehow include the stray entries as well.
if target is not None: if target is not None:
entry = frappe._dict( entry = frappe._dict(
company=ple.company, posting_date=ple.posting_date,
account=ple.account, account=ple.account,
party_type=ple.party_type, party_type=ple.party_type,
party=ple.party, party=ple.party,
@ -66,10 +58,10 @@ class PaymentLedger(object):
for value in self.voucher_dict.values(): for value in self.voucher_dict.values():
voucher_data = [] voucher_data = []
if value.increase != []: if value.get("increase"):
voucher_data.extend(value.increase) voucher_data.extend(value.get("increase"))
if value.decrease != []: if value.get("decrease"):
voucher_data.extend(value.decrease) voucher_data.extend(value.get("decrease"))
if voucher_data: if voucher_data:
# balance row # balance row
@ -117,6 +109,12 @@ class PaymentLedger(object):
if self.filters.against_voucher_no: if self.filters.against_voucher_no:
self.conditions.append(self.ple.against_voucher_no == self.filters.against_voucher_no) self.conditions.append(self.ple.against_voucher_no == self.filters.against_voucher_no)
if self.filters.party_type:
self.conditions.append(self.ple.party_type == self.filters.party_type)
if self.filters.party:
self.conditions.append(self.ple.party.isin(self.filters.party))
def get_data(self): def get_data(self):
ple = self.ple ple = self.ple
@ -134,7 +132,13 @@ class PaymentLedger(object):
def get_columns(self): def get_columns(self):
options = None options = None
self.columns.append( self.columns.append(
dict(label=_("Company"), fieldname="company", fieldtype="data", options=options, width="100") dict(
label=_("Posting Date"),
fieldname="posting_date",
fieldtype="Date",
options=options,
width="100",
)
) )
self.columns.append( self.columns.append(
@ -160,7 +164,11 @@ class PaymentLedger(object):
) )
self.columns.append( self.columns.append(
dict( dict(
label=_("Voucher No"), fieldname="voucher_no", fieldtype="data", options=options, width="100" label=_("Voucher No"),
fieldname="voucher_no",
fieldtype="Dynamic Link",
options="voucher_type",
width="100",
) )
) )
self.columns.append( self.columns.append(
@ -176,8 +184,8 @@ class PaymentLedger(object):
dict( dict(
label=_("Against Voucher No"), label=_("Against Voucher No"),
fieldname="against_voucher_no", fieldname="against_voucher_no",
fieldtype="data", fieldtype="Dynamic Link",
options=options, options="against_voucher_type",
width="100", width="100",
) )
) )
@ -209,7 +217,7 @@ class PaymentLedger(object):
self.get_columns() self.get_columns()
self.get_data() self.get_data()
# initialize dictionary and group using against voucher # initialize dictionary and group using key
self.init_voucher_dict() self.init_voucher_dict()
# convert dictionary to list and add balance rows # convert dictionary to list and add balance rows

View File

@ -1,451 +0,0 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// Contributed by Case Solved and sponsored by Nulight Studios
/* eslint-disable */
frappe.provide('frappe.query_reports');
frappe.query_reports["Tax Detail"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("company"),
reqd: 1
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.month_start(frappe.datetime.get_today()),
reqd: 1,
width: "60px"
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.month_end(frappe.datetime.get_today()),
reqd: 1,
width: "60px"
},
{
fieldname: "report_name",
label: __("Report Name"),
fieldtype: "Read Only",
default: frappe.query_report.report_name,
hidden: 1,
reqd: 1
},
{
fieldname: "mode",
label: __("Mode"),
fieldtype: "Read Only",
default: "edit",
hidden: 1,
reqd: 1
}
],
onload: function onload(report) {
// Remove Add Column and Save from menu
report.page.add_inner_button(__("New Report"), () => new_report(), __("Custom Report"));
report.page.add_inner_button(__("Load Report"), () => load_report(), __("Custom Report"));
hide_filters(report);
}
};
function hide_filters(report) {
report.page.page_form[0].querySelectorAll('.form-group.frappe-control').forEach(function setHidden(field) {
if (field.dataset.fieldtype == "Read Only") {
field.classList.add("hidden");
}
});
}
erpnext.TaxDetail = class TaxDetail {
constructor() {
this.patch();
this.load_report();
}
// Monkey patch the QueryReport class
patch() {
this.qr = frappe.query_report;
this.super = {
refresh_report: this.qr.refresh_report,
show_footer_message: this.qr.show_footer_message
}
this.qr.refresh_report = () => this.refresh_report();
this.qr.show_footer_message = () => this.show_footer_message();
}
show_footer_message() {
// The last thing to run after datatable_render in refresh()
this.super.show_footer_message.apply(this.qr);
if (this.qr.report_name !== 'Tax Detail') {
this.show_help();
if (this.loading) {
this.set_section('');
} else {
this.reload_component('');
}
}
this.loading = false;
}
refresh_report() {
// Infrequent report build (onload), load filters & data
// super function runs a refresh() serially
// already run within frappe.run_serially
this.loading = true;
this.super.refresh_report.apply(this.qr);
if (this.qr.report_name !== 'Tax Detail') {
frappe.call({
method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports',
args: {name: this.qr.report_name}
}).then((r) => {
const data = JSON.parse(r.message[this.qr.report_name]['json']);
this.create_controls();
this.sections = data.sections || {};
this.controls['show_detail'].set_input(data.show_detail);
});
}
}
load_report() {
// One-off report build like titles, menu, etc
// Run when this object is created which happens in qr.load_report
this.qr.menu_items = this.get_menu_items();
}
get_menu_items() {
// Replace Save action
let new_items = [];
const save = __('Save');
for (let item of this.qr.menu_items) {
if (item.label === save) {
new_items.push({
label: save,
action: () => this.save_report(),
standard: false
});
} else {
new_items.push(item);
}
}
return new_items;
}
save_report() {
this.check_datatable();
if (this.qr.report_name !== 'Tax Detail') {
frappe.call({
method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report',
args: {
reference_report: 'Tax Detail',
report_name: this.qr.report_name,
data: {
columns: this.qr.get_visible_columns(),
sections: this.sections,
show_detail: this.controls['show_detail'].get_input_value()
}
},
freeze: true
}).then((r) => {
this.set_section('');
});
}
}
check_datatable() {
if (!this.qr.datatable) {
frappe.throw(__('Please change the date range to load data first'));
}
}
set_section(name) {
// Sets the given section name and then reloads the data
if (name && !this.sections[name]) {
this.sections[name] = {};
}
let options = Object.keys(this.sections);
options.unshift('');
this.controls['section_name'].$wrapper.find("select").empty().add_options(options);
const org_mode = this.qr.get_filter_value('mode');
let refresh = false;
if (name) {
this.controls['section_name'].set_input(name);
this.qr.set_filter_value('mode', 'edit');
if (org_mode === 'run') {
refresh = true;
}
} else {
this.controls['section_name'].set_input('');
this.qr.set_filter_value('mode', 'run');
if (org_mode === 'edit') {
refresh = true;
}
}
if (refresh) {
this.qr.refresh();
}
this.reload_component('');
}
reload_component(component_name) {
const section_name = this.controls['section_name'].get_input_value();
if (section_name) {
const section = this.sections[section_name];
const component_names = Object.keys(section);
component_names.unshift('');
this.controls['component'].$wrapper.find("select").empty().add_options(component_names);
this.controls['component'].set_input(component_name);
if (component_name) {
this.controls['component_type'].set_input(section[component_name].type);
}
} else {
this.controls['component'].$wrapper.find("select").empty();
this.controls['component'].set_input('');
}
this.set_table_filters();
}
set_table_filters() {
let filters = {};
const section_name = this.controls['section_name'].get_input_value();
const component_name = this.controls['component'].get_input_value();
if (section_name && component_name) {
const component_type = this.sections[section_name][component_name].type;
if (component_type === 'filter') {
filters = this.sections[section_name][component_name]['filters'];
}
}
this.setAppliedFilters(filters);
}
setAppliedFilters(filters) {
if (this.qr.datatable) {
Array.from(this.qr.datatable.header.querySelectorAll('.dt-filter')).map(function setFilters(input) {
let idx = input.dataset.colIndex;
if (filters[idx]) {
input.value = filters[idx];
} else {
input.value = null;
}
});
this.qr.datatable.columnmanager.applyFilter(filters);
}
}
delete(name, type) {
if (type === 'section') {
delete this.sections[name];
const new_section = Object.keys(this.sections)[0] || '';
this.set_section(new_section);
}
if (type === 'component') {
const cur_section = this.controls['section_name'].get_input_value();
delete this.sections[cur_section][name];
this.reload_component('');
}
}
create_controls() {
let controls = {};
// SELECT in data.js
controls['section_name'] = this.qr.page.add_field({
label: __('Section'),
fieldtype: 'Select',
fieldname: 'section_name',
change: (e) => {
this.set_section(this.controls['section_name'].get_input_value());
}
});
// BUTTON in button.js
controls['new_section'] = this.qr.page.add_field({
label: __('New Section'),
fieldtype: 'Button',
fieldname: 'new_section',
click: () => {
frappe.prompt({
label: __('Section Name'),
fieldname: 'name',
fieldtype: 'Data'
}, (values) => {
this.set_section(values.name);
});
}
});
controls['delete_section'] = this.qr.page.add_field({
label: __('Delete Section'),
fieldtype: 'Button',
fieldname: 'delete_section',
click: () => {
let cur_section = this.controls['section_name'].get_input_value();
if (cur_section) {
frappe.confirm(__('Are you sure you want to delete section') + ' ' + cur_section + '?',
() => {this.delete(cur_section, 'section')});
}
}
});
controls['component'] = this.qr.page.add_field({
label: __('Component'),
fieldtype: 'Select',
fieldname: 'component',
change: (e) => {
this.reload_component(this.controls['component'].get_input_value());
}
});
controls['component_type'] = this.qr.page.add_field({
label: __('Component Type'),
fieldtype: 'Select',
fieldname: 'component_type',
default: 'filter',
options: [
{label: __('Filtered Row Subtotal'), value: 'filter'},
{label: __('Section Subtotal'), value: 'section'}
]
});
controls['add_component'] = this.qr.page.add_field({
label: __('Add Component'),
fieldtype: 'Button',
fieldname: 'add_component',
click: () => {
this.check_datatable();
let section_name = this.controls['section_name'].get_input_value();
if (section_name) {
const component_type = this.controls['component_type'].get_input_value();
let idx = 0;
const names = Object.keys(this.sections[section_name]);
if (names.length > 0) {
const idxs = names.map((key) => parseInt(key.match(/\d+$/)) || 0);
idx = Math.max(...idxs) + 1;
}
const filters = this.qr.datatable.columnmanager.getAppliedFilters();
if (component_type === 'filter') {
const name = 'Filter' + idx.toString();
let data = {
type: component_type,
filters: filters
}
this.sections[section_name][name] = data;
this.reload_component(name);
} else if (component_type === 'section') {
if (filters && Object.keys(filters).length !== 0) {
frappe.show_alert({
message: __('Column filters ignored'),
indicator: 'yellow'
});
}
let data = {
type: component_type
}
frappe.prompt({
label: __('Section'),
fieldname: 'section',
fieldtype: 'Select',
options: Object.keys(this.sections)
}, (values) => {
this.sections[section_name][values.section] = data;
this.reload_component(values.section);
});
} else {
frappe.throw(__('Please select the Component Type first'));
}
} else {
frappe.throw(__('Please select the Section first'));
}
}
});
controls['delete_component'] = this.qr.page.add_field({
label: __('Delete Component'),
fieldtype: 'Button',
fieldname: 'delete_component',
click: () => {
const component = this.controls['component'].get_input_value();
if (component) {
frappe.confirm(__('Are you sure you want to delete component') + ' ' + component + '?',
() => {this.delete(component, 'component')});
}
}
});
controls['save'] = this.qr.page.add_field({
label: __('Save & Run'),
fieldtype: 'Button',
fieldname: 'save',
click: () => {
this.save_report();
}
});
controls['show_detail'] = this.qr.page.add_field({
label: __('Show Detail'),
fieldtype: 'Check',
fieldname: 'show_detail',
default: 1
});
this.controls = controls;
}
show_help() {
const help = __('Your custom report is built from General Ledger Entries within the date range. You can add multiple sections to the report using the New Section button. Each component added to a section adds a subset of the data into the specified section. Beware of duplicated data rows. The Filtered Row component type saves the datatable column filters to specify the added data. The Section component type refers to the data in a previously defined section, but it cannot refer to its parent section. The Amount column is summed to give the section subtotal. Use the Show Detail box to see the data rows included in each section in the final report. Once finished, hit Save & Run. Report contributed by');
this.qr.$report_footer.append('<div class="col-md-12"><strong>' + __('Help') + `: </strong>${help}<a href="https://www.casesolved.co.uk"> Case Solved</a></div>`);
}
}
if (!window.taxdetail) {
window.taxdetail = new erpnext.TaxDetail();
}
function get_reports(cb) {
frappe.call({
method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports',
freeze: true
}).then((r) => {
cb(r.message);
})
}
function new_report() {
const dialog = new frappe.ui.Dialog({
title: __('New Report'),
fields: [
{
fieldname: 'report_name',
label: __('Report Name'),
fieldtype: 'Data',
default: 'VAT Return'
}
],
primary_action_label: __('Create'),
primary_action: function new_report_pa(values) {
frappe.call({
method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report',
args: {
reference_report: 'Tax Detail',
report_name: values.report_name,
data: {
columns: [],
sections: {},
show_detail: 1
}
},
freeze: true
}).then((r) => {
frappe.set_route('query-report', values.report_name);
});
dialog.hide();
}
});
dialog.show();
}
function load_report() {
get_reports(function load_report_cb(reports) {
const dialog = new frappe.ui.Dialog({
title: __('Load Report'),
fields: [
{
fieldname: 'report_name',
label: __('Report Name'),
fieldtype: 'Select',
options: Object.keys(reports)
}
],
primary_action_label: __('Load'),
primary_action: function load_report_pa(values) {
dialog.hide();
frappe.set_route('query-report', values.report_name);
}
});
dialog.show();
});
}

View File

@ -1,32 +0,0 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-02-19 16:44:21.175113",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-02-19 16:44:21.175113",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Detail",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Tax Detail",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
}

View File

@ -1,325 +0,0 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# Contributed by Case Solved and sponsored by Nulight Studios
import json
import frappe
from frappe import _
# NOTE: Payroll is implemented using Journal Entries which are included as GL Entries
# field lists in multiple doctypes will be coalesced
required_sql_fields = {
("GL Entry", 1): ["posting_date"],
("Account",): ["root_type", "account_type"],
("GL Entry", 2): ["account", "voucher_type", "voucher_no", "debit", "credit"],
("Purchase Invoice Item", "Sales Invoice Item"): [
"base_net_amount",
"item_tax_rate",
"item_tax_template",
"item_group",
"item_name",
],
("Purchase Invoice", "Sales Invoice"): ["taxes_and_charges", "tax_category"],
}
def execute(filters=None):
if not filters:
return [], []
fieldlist = required_sql_fields
fieldstr = get_fieldstr(fieldlist)
gl_entries = frappe.db.sql(
"""
select {fieldstr}
from `tabGL Entry` ge
inner join `tabAccount` a on
ge.account=a.name and ge.company=a.company
left join `tabSales Invoice` si on
ge.company=si.company and ge.voucher_type='Sales Invoice' and ge.voucher_no=si.name
left join `tabSales Invoice Item` sii on
a.root_type='Income' and si.name=sii.parent
left join `tabPurchase Invoice` pi on
ge.company=pi.company and ge.voucher_type='Purchase Invoice' and ge.voucher_no=pi.name
left join `tabPurchase Invoice Item` pii on
a.root_type='Expense' and pi.name=pii.parent
where
ge.company=%(company)s and
ge.posting_date>=%(from_date)s and
ge.posting_date<=%(to_date)s
order by ge.posting_date, ge.voucher_no
""".format(
fieldstr=fieldstr
),
filters,
as_dict=1,
)
report_data = modify_report_data(gl_entries)
summary = None
if filters["mode"] == "run" and filters["report_name"] != "Tax Detail":
report_data, summary = run_report(filters["report_name"], report_data)
# return columns, data, message, chart, report_summary
return get_columns(fieldlist), report_data, None, None, summary
def run_report(report_name, data):
"Applies the sections and filters saved in the custom report"
report_config = json.loads(frappe.get_doc("Report", report_name).json)
# Columns indexed from 1 wrt colno
columns = report_config.get("columns")
sections = report_config.get("sections", {})
show_detail = report_config.get("show_detail", 1)
report = {}
new_data = []
summary = []
for section_name, section in sections.items():
report[section_name] = {"rows": [], "subtotal": 0.0}
for component_name, component in section.items():
if component["type"] == "filter":
for row in data:
matched = True
for colno, filter_string in component["filters"].items():
filter_field = columns[int(colno) - 1]["fieldname"]
if not filter_match(row[filter_field], filter_string):
matched = False
break
if matched:
report[section_name]["rows"] += [row]
report[section_name]["subtotal"] += row["amount"]
if component["type"] == "section":
if component_name == section_name:
frappe.throw(_("A report component cannot refer to its parent section") + ": " + section_name)
try:
report[section_name]["rows"] += report[component_name]["rows"]
report[section_name]["subtotal"] += report[component_name]["subtotal"]
except KeyError:
frappe.throw(
_("A report component can only refer to an earlier section") + ": " + section_name
)
if show_detail:
new_data += report[section_name]["rows"]
new_data += [{"voucher_no": section_name, "amount": report[section_name]["subtotal"]}]
summary += [
{"label": section_name, "datatype": "Currency", "value": report[section_name]["subtotal"]}
]
if show_detail:
new_data += [{}]
return new_data or data, summary or None
def filter_match(value, string):
"Approximation to datatable filters"
import datetime
if string == "":
return True
if value is None:
value = -999999999999999
elif isinstance(value, datetime.date):
return True
if isinstance(value, str):
value = value.lower()
string = string.lower()
if string[0] == "<":
return True if string[1:].strip() else False
elif string[0] == ">":
return False if string[1:].strip() else True
elif string[0] == "=":
return string[1:] in value if string[1:] else False
elif string[0:2] == "!=":
return string[2:] not in value
elif len(string.split(":")) == 2:
pre, post = string.split(":")
return True if not pre.strip() and post.strip() in value else False
else:
return string in value
else:
if string[0] in ["<", ">", "="]:
operator = string[0]
if operator == "=":
operator = "=="
string = string[1:].strip()
elif string[0:2] == "!=":
operator = "!="
string = string[2:].strip()
elif len(string.split(":")) == 2:
pre, post = string.split(":")
try:
return True if float(pre) <= value and float(post) >= value else False
except ValueError:
return False if pre.strip() else True
else:
return string in str(value)
try:
num = float(string) if string.strip() else 0
return frappe.safe_eval(f"{value} {operator} {num}")
except ValueError:
if operator == "<":
return True
return False
def abbrev(dt):
return "".join(l[0].lower() for l in dt.split(" ")) + "."
def doclist(dt, dfs):
return [abbrev(dt) + f for f in dfs]
def as_split(fields):
for field in fields:
split = field.split(" as ")
yield (split[0], split[1] if len(split) > 1 else split[0])
def coalesce(doctypes, fields):
coalesce = []
for name, new_name in as_split(fields):
sharedfields = ", ".join(abbrev(dt) + name for dt in doctypes)
coalesce += [f"coalesce({sharedfields}) as {new_name}"]
return coalesce
def get_fieldstr(fieldlist):
fields = []
for doctypes, docfields in fieldlist.items():
if len(doctypes) == 1 or isinstance(doctypes[1], int):
fields += doclist(doctypes[0], docfields)
else:
fields += coalesce(doctypes, docfields)
return ", ".join(fields)
def get_columns(fieldlist):
columns = {}
for doctypes, docfields in fieldlist.items():
fieldmap = {name: new_name for name, new_name in as_split(docfields)}
for doctype in doctypes:
if isinstance(doctype, int):
break
meta = frappe.get_meta(doctype)
# get column field metadata from the db
fieldmeta = {}
for field in meta.get("fields"):
if field.fieldname in fieldmap.keys():
new_name = fieldmap[field.fieldname]
fieldmeta[new_name] = {
"label": _(field.label),
"fieldname": new_name,
"fieldtype": field.fieldtype,
"options": field.options,
}
# edit the columns to match the modified data
for field in fieldmap.values():
col = modify_report_columns(doctype, field, fieldmeta[field])
if col:
columns[col["fieldname"]] = col
# use of a dict ensures duplicate columns are removed
return list(columns.values())
def modify_report_columns(doctype, field, column):
"Because data is rearranged into other columns"
if doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
if field in ["item_tax_rate", "base_net_amount"]:
return None
if doctype == "GL Entry":
if field in ["debit", "credit"]:
column.update({"label": _("Amount"), "fieldname": "amount"})
elif field == "voucher_type":
column.update({"fieldtype": "Data", "options": ""})
if field == "taxes_and_charges":
column.update({"label": _("Taxes and Charges Template")})
return column
def modify_report_data(data):
import json
new_data = []
for line in data:
if line.debit:
line.amount = -line.debit
else:
line.amount = line.credit
# Remove Invoice GL Tax Entries and generate Tax entries from the invoice lines
if "Invoice" in line.voucher_type:
if line.account_type not in ("Tax", "Round Off"):
new_data += [line]
if line.item_tax_rate:
tax_rates = json.loads(line.item_tax_rate)
for account, rate in tax_rates.items():
tax_line = line.copy()
tax_line.account_type = "Tax"
tax_line.account = account
if line.voucher_type == "Sales Invoice":
line.amount = line.base_net_amount
tax_line.amount = line.base_net_amount * (rate / 100)
if line.voucher_type == "Purchase Invoice":
line.amount = -line.base_net_amount
tax_line.amount = -line.base_net_amount * (rate / 100)
new_data += [tax_line]
else:
new_data += [line]
return new_data
# JS client utilities
custom_report_dict = {
"ref_doctype": "GL Entry",
"report_type": "Custom Report",
"reference_report": "Tax Detail",
}
@frappe.whitelist()
def get_custom_reports(name=None):
filters = custom_report_dict.copy()
if name:
filters["name"] = name
reports = frappe.get_list("Report", filters=filters, fields=["name", "json"], as_list=False)
reports_dict = {rep.pop("name"): rep for rep in reports}
# Prevent custom reports with the same name
reports_dict["Tax Detail"] = {"json": None}
return reports_dict
@frappe.whitelist()
def save_custom_report(reference_report, report_name, data):
if reference_report != "Tax Detail":
frappe.throw(_("The wrong report is referenced."))
if report_name == "Tax Detail":
frappe.throw(_("The parent report cannot be overwritten."))
doc = {
"doctype": "Report",
"report_name": report_name,
"is_standard": "No",
"module": "Accounts",
"json": data,
}
doc.update(custom_report_dict)
try:
newdoc = frappe.get_doc(doc)
newdoc.insert()
frappe.msgprint(_("Report created successfully"))
except frappe.exceptions.DuplicateEntryError:
dbdoc = frappe.get_doc("Report", report_name)
dbdoc.update(doc)
dbdoc.save()
frappe.msgprint(_("Report updated successfully"))
return report_name

View File

@ -1,840 +0,0 @@
[
{
"account_manager": null,
"accounts": [],
"companies": [],
"credit_limits": [],
"customer_details": null,
"customer_group": "All Customer Groups",
"customer_name": "_Test Customer",
"customer_pos_id": null,
"customer_primary_address": null,
"customer_primary_contact": null,
"customer_type": "Company",
"default_bank_account": null,
"default_commission_rate": 0.0,
"default_currency": null,
"default_price_list": null,
"default_sales_partner": null,
"disabled": 0,
"dn_required": 0,
"docstatus": 0,
"doctype": "Customer",
"email_id": null,
"gender": null,
"image": null,
"industry": null,
"is_frozen": 0,
"is_internal_customer": 0,
"language": "en",
"lead_name": null,
"loyalty_program": null,
"loyalty_program_tier": null,
"market_segment": null,
"mobile_no": null,
"modified": "2021-02-15 05:18:03.624724",
"name": "_Test Customer",
"naming_series": "CUST-.YYYY.-",
"pan": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"payment_terms": null,
"primary_address": null,
"represents_company": "",
"sales_team": [],
"salutation": null,
"so_required": 0,
"tax_category": null,
"tax_id": null,
"tax_withholding_category": null,
"territory": "All Territories",
"website": null
},{
"accounts": [],
"allow_purchase_invoice_creation_without_purchase_order": 0,
"allow_purchase_invoice_creation_without_purchase_receipt": 0,
"companies": [],
"country": "United Kingdom",
"default_bank_account": null,
"default_currency": null,
"default_price_list": null,
"disabled": 0,
"docstatus": 0,
"doctype": "Supplier",
"hold_type": "",
"image": null,
"is_frozen": 0,
"is_internal_supplier": 0,
"is_transporter": 0,
"language": "en",
"modified": "2021-03-31 16:47:10.109316",
"name": "_Test Supplier",
"naming_series": "SUP-.YYYY.-",
"on_hold": 0,
"pan": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"payment_terms": null,
"prevent_pos": 0,
"prevent_rfqs": 0,
"release_date": null,
"represents_company": null,
"supplier_details": null,
"supplier_group": "Raw Material",
"supplier_name": "_Test Supplier",
"supplier_type": "Company",
"tax_category": null,
"tax_id": null,
"tax_withholding_category": null,
"warn_pos": 0,
"warn_rfqs": 0,
"website": null
},{
"account_currency": "GBP",
"account_name": "Debtors",
"account_number": "",
"account_type": "Receivable",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 58,
"modified": "2021-03-26 04:44:19.955468",
"name": "Debtors - _T",
"old_parent": null,
"parent": null,
"parent_account": "Application of Funds (Assets) - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Balance Sheet",
"rgt": 59,
"root_type": "Asset",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "Sales",
"account_number": "",
"account_type": "Income Account",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 291,
"modified": "2021-03-26 04:50:21.697703",
"name": "Sales - _T",
"old_parent": null,
"parent": null,
"parent_account": "Income - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Profit and Loss",
"rgt": 292,
"root_type": "Income",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "VAT on Sales",
"account_number": "",
"account_type": "Tax",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 317,
"modified": "2021-03-26 04:50:21.697703",
"name": "VAT on Sales - _T",
"old_parent": null,
"parent": null,
"parent_account": "Source of Funds (Liabilities) - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Balance Sheet",
"rgt": 318,
"root_type": "Liability",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "Cost of Goods Sold",
"account_number": "",
"account_type": "Cost of Goods Sold",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 171,
"modified": "2021-03-26 04:44:19.994857",
"name": "Cost of Goods Sold - _T",
"old_parent": null,
"parent": null,
"parent_account": "Expenses - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Profit and Loss",
"rgt": 172,
"root_type": "Expense",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "VAT on Purchases",
"account_number": "",
"account_type": "Tax",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 80,
"modified": "2021-03-26 04:44:19.961983",
"name": "VAT on Purchases - _T",
"old_parent": null,
"parent": null,
"parent_account": "Application of Funds (Assets) - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Balance Sheet",
"rgt": 81,
"root_type": "Asset",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "Creditors",
"account_number": "",
"account_type": "Payable",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 302,
"modified": "2021-03-26 04:50:21.697703",
"name": "Creditors - _T",
"old_parent": null,
"parent": null,
"parent_account": "Source of Funds (Liabilities) - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Balance Sheet",
"rgt": 303,
"root_type": "Liability",
"tax_rate": 0.0
},{
"additional_discount_percentage": 0.0,
"address_display": null,
"adjust_advance_taxes": 0,
"advances": [],
"against_expense_account": "Cost of Goods Sold - _T",
"allocate_advances_automatically": 0,
"amended_from": null,
"apply_discount_on": "Grand Total",
"apply_tds": 0,
"auto_repeat": null,
"base_discount_amount": 0.0,
"base_grand_total": 511.68,
"base_in_words": "GBP Five Hundred And Eleven and Sixty Eight Pence only.",
"base_net_total": 426.4,
"base_paid_amount": 0.0,
"base_rounded_total": 511.68,
"base_rounding_adjustment": 0.0,
"base_taxes_and_charges_added": 85.28,
"base_taxes_and_charges_deducted": 0.0,
"base_total": 426.4,
"base_total_taxes_and_charges": 85.28,
"base_write_off_amount": 0.0,
"bill_date": null,
"bill_no": null,
"billing_address": null,
"billing_address_display": null,
"buying_price_list": "Standard Buying",
"cash_bank_account": null,
"clearance_date": null,
"company": "_T",
"contact_display": null,
"contact_email": null,
"contact_mobile": null,
"contact_person": null,
"conversion_rate": 1.0,
"cost_center": null,
"credit_to": "Creditors - _T",
"currency": "GBP",
"disable_rounded_total": 0,
"discount_amount": 0.0,
"docstatus": 0,
"doctype": "Purchase Invoice",
"due_date": null,
"from_date": null,
"grand_total": 511.68,
"group_same_items": 0,
"hold_comment": null,
"ignore_pricing_rule": 0,
"in_words": "GBP Five Hundred And Eleven and Sixty Eight Pence only.",
"inter_company_invoice_reference": null,
"is_internal_supplier": 0,
"is_opening": "No",
"is_paid": 0,
"is_return": 0,
"is_subcontracted": 0,
"items": [
{
"allow_zero_valuation_rate": 0,
"amount": 426.4,
"asset_category": null,
"asset_location": null,
"base_amount": 426.4,
"base_net_amount": 426.4,
"base_net_rate": 5.33,
"base_price_list_rate": 5.33,
"base_rate": 5.33,
"base_rate_with_margin": 0.0,
"batch_no": null,
"bom": null,
"brand": null,
"conversion_factor": 0.0,
"cost_center": "Main - _T",
"deferred_expense_account": null,
"description": "<div class=\"ql-editor read-mode\"><p>Fluid to make widgets</p></div>",
"discount_amount": 0.0,
"discount_percentage": 0.0,
"enable_deferred_expense": 0,
"expense_account": "Cost of Goods Sold - _T",
"from_warehouse": null,
"image": null,
"include_exploded_items": 0,
"is_fixed_asset": 0,
"is_free_item": 0,
"item_code": null,
"item_group": null,
"item_name": "Widget Fluid 1Litre",
"item_tax_amount": 0.0,
"item_tax_rate": "{\"VAT on Purchases - _T\": 20.0}",
"item_tax_template": null,
"landed_cost_voucher_amount": 0.0,
"manufacturer": null,
"manufacturer_part_no": null,
"margin_rate_or_amount": 0.0,
"margin_type": "",
"net_amount": 426.4,
"net_rate": 5.33,
"page_break": 0,
"parent": null,
"parentfield": "items",
"parenttype": "Purchase Invoice",
"po_detail": null,
"pr_detail": null,
"price_list_rate": 5.33,
"pricing_rules": null,
"project": null,
"purchase_invoice_item": null,
"purchase_order": null,
"purchase_receipt": null,
"qty": 80.0,
"quality_inspection": null,
"rate": 5.33,
"rate_with_margin": 0.0,
"received_qty": 0.0,
"rejected_qty": 0.0,
"rejected_serial_no": null,
"rejected_warehouse": null,
"rm_supp_cost": 0.0,
"sales_invoice_item": null,
"serial_no": null,
"service_end_date": null,
"service_start_date": null,
"service_stop_date": null,
"stock_qty": 0.0,
"stock_uom": "Nos",
"stock_uom_rate": 0.0,
"total_weight": 0.0,
"uom": "Nos",
"valuation_rate": 0.0,
"warehouse": null,
"weight_per_unit": 0.0,
"weight_uom": null
}
],
"language": "en",
"letter_head": null,
"mode_of_payment": null,
"modified": "2021-04-03 03:33:09.180453",
"name": null,
"naming_series": "ACC-PINV-.YYYY.-",
"net_total": 426.4,
"on_hold": 0,
"other_charges_calculation": "<div class=\"tax-break-up\" style=\"overflow-x: auto;\">\n\t<table class=\"table table-bordered table-hover\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-left\">Item</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">Taxable Amount</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">VAT on Purchases</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Widget Fluid 1Litre</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 426.40\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(20.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 85.28\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t</tbody>\n\t</table>\n</div>",
"outstanding_amount": 511.68,
"paid_amount": 0.0,
"parent": null,
"parentfield": null,
"parenttype": null,
"party_account_currency": "GBP",
"payment_schedule": [],
"payment_terms_template": null,
"plc_conversion_rate": 1.0,
"posting_date": null,
"posting_time": "16:59:56.789522",
"price_list_currency": "GBP",
"pricing_rules": [],
"project": null,
"rejected_warehouse": null,
"release_date": null,
"remarks": "No Remarks",
"represents_company": null,
"return_against": null,
"rounded_total": 511.68,
"rounding_adjustment": 0.0,
"scan_barcode": null,
"select_print_heading": null,
"set_from_warehouse": null,
"set_posting_time": 0,
"set_warehouse": null,
"shipping_address": null,
"shipping_address_display": "",
"shipping_rule": null,
"status": "Unpaid",
"supplied_items": [],
"supplier": "_Test Supplier",
"supplier_address": null,
"supplier_name": "_Test Supplier",
"supplier_warehouse": "Stores - _T",
"tax_category": null,
"tax_id": null,
"tax_withholding_category": null,
"taxes": [
{
"account_head": "VAT on Purchases - _T",
"add_deduct_tax": "Add",
"base_tax_amount": 85.28,
"base_tax_amount_after_discount_amount": 85.28,
"base_total": 511.68,
"category": "Total",
"charge_type": "On Net Total",
"cost_center": "Main - _T",
"description": "VAT on Purchases",
"included_in_print_rate": 0,
"item_wise_tax_detail": "{\"Widget Fluid 1Litre\":[20.0,85.28]}",
"parent": null,
"parentfield": "taxes",
"parenttype": "Purchase Invoice",
"rate": 0.0,
"row_id": null,
"tax_amount": 85.28,
"tax_amount_after_discount_amount": 85.28,
"total": 511.68
}
],
"taxes_and_charges": null,
"taxes_and_charges_added": 85.28,
"taxes_and_charges_deducted": 0.0,
"tc_name": null,
"terms": null,
"title": "_Purchase Invoice",
"to_date": null,
"total": 426.4,
"total_advance": 0.0,
"total_net_weight": 0.0,
"total_qty": 80.0,
"total_taxes_and_charges": 85.28,
"unrealized_profit_loss_account": null,
"update_stock": 0,
"write_off_account": null,
"write_off_amount": 0.0,
"write_off_cost_center": null
},{
"account_for_change_amount": null,
"additional_discount_percentage": 0.0,
"address_display": null,
"advances": [],
"against_income_account": "Sales - _T",
"allocate_advances_automatically": 0,
"amended_from": null,
"apply_discount_on": "Grand Total",
"auto_repeat": null,
"base_change_amount": 0.0,
"base_discount_amount": 0.0,
"base_grand_total": 868.25,
"base_in_words": "GBP Eight Hundred And Sixty Eight and Twenty Five Pence only.",
"base_net_total": 825.0,
"base_paid_amount": 0.0,
"base_rounded_total": 868.25,
"base_rounding_adjustment": 0.0,
"base_total": 825.0,
"base_total_taxes_and_charges": 43.25,
"base_write_off_amount": 0.0,
"c_form_applicable": "No",
"c_form_no": null,
"campaign": null,
"cash_bank_account": null,
"change_amount": 0.0,
"commission_rate": 0.0,
"company": "_T",
"company_address": null,
"company_address_display": null,
"company_tax_id": null,
"contact_display": null,
"contact_email": null,
"contact_mobile": null,
"contact_person": null,
"conversion_rate": 1.0,
"cost_center": null,
"currency": "GBP",
"customer": "_Test Customer",
"customer_address": null,
"customer_group": "All Customer Groups",
"customer_name": "_Test Customer",
"debit_to": "Debtors - _T",
"discount_amount": 0.0,
"docstatus": 0,
"doctype": "Sales Invoice",
"due_date": null,
"from_date": null,
"grand_total": 868.25,
"group_same_items": 0,
"ignore_pricing_rule": 0,
"in_words": "GBP Eight Hundred And Sixty Eight and Twenty Five Pence only.",
"inter_company_invoice_reference": null,
"is_consolidated": 0,
"is_discounted": 0,
"is_internal_customer": 0,
"is_opening": "No",
"is_pos": 0,
"is_return": 0,
"items": [
{
"actual_batch_qty": 0.0,
"actual_qty": 0.0,
"allow_zero_valuation_rate": 0,
"amount": 200.0,
"asset": null,
"barcode": null,
"base_amount": 200.0,
"base_net_amount": 200.0,
"base_net_rate": 50.0,
"base_price_list_rate": 0.0,
"base_rate": 50.0,
"base_rate_with_margin": 0.0,
"batch_no": null,
"brand": null,
"conversion_factor": 1.0,
"cost_center": "Main - _T",
"customer_item_code": null,
"deferred_revenue_account": null,
"delivered_by_supplier": 0,
"delivered_qty": 0.0,
"delivery_note": null,
"description": "<div class=\"ql-editor read-mode\"><p>Used</p></div>",
"discount_amount": 0.0,
"discount_percentage": 0.0,
"dn_detail": null,
"enable_deferred_revenue": 0,
"expense_account": null,
"finance_book": null,
"image": null,
"income_account": "Sales - _T",
"incoming_rate": 0.0,
"is_fixed_asset": 0,
"is_free_item": 0,
"item_code": null,
"item_group": null,
"item_name": "Dunlop tyres",
"item_tax_rate": "{\"VAT on Sales - _T\": 20.0}",
"item_tax_template": null,
"margin_rate_or_amount": 0.0,
"margin_type": "",
"net_amount": 200.0,
"net_rate": 50.0,
"page_break": 0,
"parent": null,
"parentfield": "items",
"parenttype": "Sales Invoice",
"price_list_rate": 0.0,
"pricing_rules": null,
"project": null,
"qty": 4.0,
"quality_inspection": null,
"rate": 50.0,
"rate_with_margin": 0.0,
"sales_invoice_item": null,
"sales_order": null,
"serial_no": null,
"service_end_date": null,
"service_start_date": null,
"service_stop_date": null,
"so_detail": null,
"stock_qty": 4.0,
"stock_uom": "Nos",
"stock_uom_rate": 50.0,
"target_warehouse": null,
"total_weight": 0.0,
"uom": "Nos",
"warehouse": null,
"weight_per_unit": 0.0,
"weight_uom": null
},
{
"actual_batch_qty": 0.0,
"actual_qty": 0.0,
"allow_zero_valuation_rate": 0,
"amount": 65.0,
"asset": null,
"barcode": null,
"base_amount": 65.0,
"base_net_amount": 65.0,
"base_net_rate": 65.0,
"base_price_list_rate": 0.0,
"base_rate": 65.0,
"base_rate_with_margin": 0.0,
"batch_no": null,
"brand": null,
"conversion_factor": 1.0,
"cost_center": "Main - _T",
"customer_item_code": null,
"deferred_revenue_account": null,
"delivered_by_supplier": 0,
"delivered_qty": 0.0,
"delivery_note": null,
"description": "<div class=\"ql-editor read-mode\"><p>Used</p></div>",
"discount_amount": 0.0,
"discount_percentage": 0.0,
"dn_detail": null,
"enable_deferred_revenue": 0,
"expense_account": null,
"finance_book": null,
"image": null,
"income_account": "Sales - _T",
"incoming_rate": 0.0,
"is_fixed_asset": 0,
"is_free_item": 0,
"item_code": "",
"item_group": null,
"item_name": "Continental tyres",
"item_tax_rate": "{\"VAT on Sales - _T\": 5.0}",
"item_tax_template": null,
"margin_rate_or_amount": 0.0,
"margin_type": "",
"net_amount": 65.0,
"net_rate": 65.0,
"page_break": 0,
"parent": null,
"parentfield": "items",
"parenttype": "Sales Invoice",
"price_list_rate": 0.0,
"pricing_rules": null,
"project": null,
"qty": 1.0,
"quality_inspection": null,
"rate": 65.0,
"rate_with_margin": 0.0,
"sales_invoice_item": null,
"sales_order": null,
"serial_no": null,
"service_end_date": null,
"service_start_date": null,
"service_stop_date": null,
"so_detail": null,
"stock_qty": 1.0,
"stock_uom": null,
"stock_uom_rate": 65.0,
"target_warehouse": null,
"total_weight": 0.0,
"uom": "Nos",
"warehouse": null,
"weight_per_unit": 0.0,
"weight_uom": null
},
{
"actual_batch_qty": 0.0,
"actual_qty": 0.0,
"allow_zero_valuation_rate": 0,
"amount": 560.0,
"asset": null,
"barcode": null,
"base_amount": 560.0,
"base_net_amount": 560.0,
"base_net_rate": 70.0,
"base_price_list_rate": 0.0,
"base_rate": 70.0,
"base_rate_with_margin": 0.0,
"batch_no": null,
"brand": null,
"conversion_factor": 1.0,
"cost_center": "Main - _T",
"customer_item_code": null,
"deferred_revenue_account": null,
"delivered_by_supplier": 0,
"delivered_qty": 0.0,
"delivery_note": null,
"description": "<div class=\"ql-editor read-mode\"><p>New</p></div>",
"discount_amount": 0.0,
"discount_percentage": 0.0,
"dn_detail": null,
"enable_deferred_revenue": 0,
"expense_account": null,
"finance_book": null,
"image": null,
"income_account": "Sales - _T",
"incoming_rate": 0.0,
"is_fixed_asset": 0,
"is_free_item": 0,
"item_code": null,
"item_group": null,
"item_name": "Toyo tyres",
"item_tax_rate": "{\"VAT on Sales - _T\": 0.0}",
"item_tax_template": null,
"margin_rate_or_amount": 0.0,
"margin_type": "",
"net_amount": 560.0,
"net_rate": 70.0,
"page_break": 0,
"parent": null,
"parentfield": "items",
"parenttype": "Sales Invoice",
"price_list_rate": 0.0,
"pricing_rules": null,
"project": null,
"qty": 8.0,
"quality_inspection": null,
"rate": 70.0,
"rate_with_margin": 0.0,
"sales_invoice_item": null,
"sales_order": null,
"serial_no": null,
"service_end_date": null,
"service_start_date": null,
"service_stop_date": null,
"so_detail": null,
"stock_qty": 8.0,
"stock_uom": null,
"stock_uom_rate": 70.0,
"target_warehouse": null,
"total_weight": 0.0,
"uom": "Nos",
"warehouse": null,
"weight_per_unit": 0.0,
"weight_uom": null
}
],
"language": "en",
"letter_head": null,
"loyalty_amount": 0.0,
"loyalty_points": 0,
"loyalty_program": null,
"loyalty_redemption_account": null,
"loyalty_redemption_cost_center": null,
"modified": "2021-02-16 05:18:59.755144",
"name": null,
"naming_series": "ACC-SINV-.YYYY.-",
"net_total": 825.0,
"other_charges_calculation": "<div class=\"tax-break-up\" style=\"overflow-x: auto;\">\n\t<table class=\"table table-bordered table-hover\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-left\">Item</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">Taxable Amount</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">VAT on Sales</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Dunlop tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 200.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(20.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 40.00\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Continental tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 65.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(5.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 3.25\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Toyo tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 560.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(0.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 0.00\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t</tbody>\n\t</table>\n</div>",
"outstanding_amount": 868.25,
"packed_items": [],
"paid_amount": 0.0,
"parent": null,
"parentfield": null,
"parenttype": null,
"party_account_currency": "GBP",
"payment_schedule": [],
"payment_terms_template": null,
"payments": [],
"plc_conversion_rate": 1.0,
"po_date": null,
"po_no": "",
"pos_profile": null,
"posting_date": null,
"posting_time": "5:19:02.994077",
"price_list_currency": "GBP",
"pricing_rules": [],
"project": null,
"redeem_loyalty_points": 0,
"remarks": "No Remarks",
"represents_company": "",
"return_against": null,
"rounded_total": 868.25,
"rounding_adjustment": 0.0,
"sales_partner": null,
"sales_team": [],
"scan_barcode": null,
"select_print_heading": null,
"selling_price_list": "Standard Selling",
"set_posting_time": 0,
"set_target_warehouse": null,
"set_warehouse": null,
"shipping_address": null,
"shipping_address_name": "",
"shipping_rule": null,
"source": null,
"status": "Overdue",
"tax_category": "",
"tax_id": null,
"taxes": [
{
"account_head": "VAT on Sales - _T",
"base_tax_amount": 43.25,
"base_tax_amount_after_discount_amount": 43.25,
"base_total": 868.25,
"charge_type": "On Net Total",
"cost_center": "Main - _T",
"description": "VAT on Sales",
"included_in_print_rate": 0,
"item_wise_tax_detail": "{\"Dunlop tyres\":[20.0,40.0],\"Continental tyres\":[5.0,3.25],\"Toyo tyres\":[0.0,0.0]}",
"parent": null,
"parentfield": "taxes",
"parenttype": "Sales Invoice",
"rate": 0.0,
"row_id": null,
"tax_amount": 43.25,
"tax_amount_after_discount_amount": 43.25,
"total": 868.25
}
],
"taxes_and_charges": null,
"tc_name": null,
"terms": null,
"territory": "All Territories",
"timesheets": [],
"title": "_Sales Invoice",
"to_date": null,
"total": 825.0,
"total_advance": 0.0,
"total_billing_amount": 0.0,
"total_commission": 0.0,
"total_net_weight": 0.0,
"total_qty": 13.0,
"total_taxes_and_charges": 43.25,
"unrealized_profit_loss_account": null,
"update_billed_amount_in_sales_order": 0,
"update_stock": 0,
"write_off_account": null,
"write_off_amount": 0.0,
"write_off_cost_center": null,
"write_off_outstanding_amount_automatically": 0
}
]

View File

@ -1,213 +0,0 @@
import datetime
import json
import os
import unittest
import frappe
from frappe.utils import (
add_to_date,
get_first_day,
get_last_day,
get_year_ending,
get_year_start,
getdate,
)
from .tax_detail import filter_match, save_custom_report
class TestTaxDetail(unittest.TestCase):
def load_testdocs(self):
from erpnext.accounts.utils import FiscalYearError, get_fiscal_year
datapath, _ = os.path.splitext(os.path.realpath(__file__))
with open(datapath + ".json", "r") as fp:
docs = json.load(fp)
now = getdate()
self.from_date = get_first_day(now)
self.to_date = get_last_day(now)
try:
get_fiscal_year(now, company="_T")
except FiscalYearError:
docs = [
{
"companies": [
{
"company": "_T",
"parent": "_Test Fiscal",
"parentfield": "companies",
"parenttype": "Fiscal Year",
}
],
"doctype": "Fiscal Year",
"year": "_Test Fiscal",
"year_end_date": get_year_ending(now),
"year_start_date": get_year_start(now),
}
] + docs
docs = [
{
"abbr": "_T",
"company_name": "_T",
"country": "United Kingdom",
"default_currency": "GBP",
"doctype": "Company",
"name": "_T",
}
] + docs
for doc in docs:
try:
db_doc = frappe.get_doc(doc)
if "Invoice" in db_doc.doctype:
db_doc.due_date = add_to_date(now, days=1)
db_doc.insert()
# Create GL Entries:
db_doc.submit()
else:
db_doc.insert(ignore_if_duplicate=True)
except frappe.exceptions.DuplicateEntryError:
pass
def load_defcols(self):
self.company = frappe.get_doc("Company", "_T")
custom_report = frappe.get_doc("Report", "Tax Detail")
self.default_columns, _ = custom_report.run_query_report(
filters={
"from_date": "2021-03-01",
"to_date": "2021-03-31",
"company": self.company.name,
"mode": "run",
"report_name": "Tax Detail",
},
user=frappe.session.user,
)
def rm_testdocs(self):
"Remove the Company and all data"
from erpnext.setup.doctype.company.company import create_transaction_deletion_request
create_transaction_deletion_request(self.company.name)
def test_report(self):
self.load_testdocs()
self.load_defcols()
report_name = save_custom_report(
"Tax Detail",
"_Test Tax Detail",
json.dumps(
{
"columns": self.default_columns,
"sections": {
"Box1": {"Filter0": {"type": "filter", "filters": {"4": "VAT on Sales"}}},
"Box2": {"Filter0": {"type": "filter", "filters": {"4": "Acquisition"}}},
"Box3": {"Box1": {"type": "section"}, "Box2": {"type": "section"}},
"Box4": {"Filter0": {"type": "filter", "filters": {"4": "VAT on Purchases"}}},
"Box5": {"Box3": {"type": "section"}, "Box4": {"type": "section"}},
"Box6": {"Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales"}}},
"Box7": {"Filter0": {"type": "filter", "filters": {"2": "Expense", "3": "!=Tax"}}},
"Box8": {"Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales", "12": "EU"}}},
"Box9": {
"Filter0": {"type": "filter", "filters": {"2": "Expense", "3": "!=Tax", "12": "EU"}}
},
},
"show_detail": 1,
}
),
)
data = frappe.desk.query_report.run(
report_name,
filters={
"from_date": self.from_date,
"to_date": self.to_date,
"company": self.company.name,
"mode": "run",
"report_name": report_name,
},
user=frappe.session.user,
)
self.assertListEqual(data.get("columns"), self.default_columns)
expected = (
("Box1", 43.25),
("Box2", 0.0),
("Box3", 43.25),
("Box4", -85.28),
("Box5", -42.03),
("Box6", 825.0),
("Box7", -426.40),
("Box8", 0.0),
("Box9", 0.0),
)
exrow = iter(expected)
for row in data.get("result"):
if row.get("voucher_no") and not row.get("posting_date"):
label, value = next(exrow)
self.assertDictEqual(row, {"voucher_no": label, "amount": value})
self.assertListEqual(
data.get("report_summary"),
[{"label": label, "datatype": "Currency", "value": value} for label, value in expected],
)
self.rm_testdocs()
def test_filter_match(self):
# None - treated as -inf number except range
self.assertTrue(filter_match(None, "!="))
self.assertTrue(filter_match(None, "<"))
self.assertTrue(filter_match(None, "<jjj"))
self.assertTrue(filter_match(None, " : "))
self.assertTrue(filter_match(None, ":56"))
self.assertTrue(filter_match(None, ":de"))
self.assertFalse(filter_match(None, "3.4"))
self.assertFalse(filter_match(None, "="))
self.assertFalse(filter_match(None, "=3.4"))
self.assertFalse(filter_match(None, ">3.4"))
self.assertFalse(filter_match(None, " <"))
self.assertFalse(filter_match(None, "ew"))
self.assertFalse(filter_match(None, " "))
self.assertFalse(filter_match(None, " f :"))
# Numbers
self.assertTrue(filter_match(3.4, "3.4"))
self.assertTrue(filter_match(3.4, ".4"))
self.assertTrue(filter_match(3.4, "3"))
self.assertTrue(filter_match(-3.4, "< -3"))
self.assertTrue(filter_match(-3.4, "> -4"))
self.assertTrue(filter_match(3.4, "= 3.4 "))
self.assertTrue(filter_match(3.4, "!=4.5"))
self.assertTrue(filter_match(3.4, " 3 : 4 "))
self.assertTrue(filter_match(0.0, " : "))
self.assertFalse(filter_match(3.4, "=4.5"))
self.assertFalse(filter_match(3.4, " = 3.4 "))
self.assertFalse(filter_match(3.4, "!=3.4"))
self.assertFalse(filter_match(3.4, ">6"))
self.assertFalse(filter_match(3.4, "<-4.5"))
self.assertFalse(filter_match(3.4, "4.5"))
self.assertFalse(filter_match(3.4, "5:9"))
# Strings
self.assertTrue(filter_match("ACC-SINV-2021-00001", "SINV"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "sinv"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "-2021"))
self.assertTrue(filter_match(" ACC-SINV-2021-00001", " acc"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "=2021"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "!=zz"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "< zzz "))
self.assertTrue(filter_match("ACC-SINV-2021-00001", " : sinv "))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " sinv :"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " acc"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", "= 2021 "))
self.assertFalse(filter_match("ACC-SINV-2021-00001", "!=sinv"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " >"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", ">aa"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " <"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", "< "))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " ="))
self.assertFalse(filter_match("ACC-SINV-2021-00001", "="))
# Date - always match
self.assertTrue(filter_match(datetime.date(2021, 3, 19), " kdsjkldfs "))

View File

@ -4,7 +4,8 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr, flt, formatdate, getdate from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cstr, flt, formatdate, getdate
import erpnext import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@ -78,7 +79,6 @@ def validate_filters(filters):
def get_data(filters): def get_data(filters):
accounts = frappe.db.sql( accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt """select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
@ -118,12 +118,10 @@ def get_data(filters):
ignore_closing_entries=not flt(filters.with_period_closing_entry), ignore_closing_entries=not flt(filters.with_period_closing_entry),
) )
total_row = calculate_values( calculate_values(accounts, gl_entries_by_account, opening_balances)
accounts, gl_entries_by_account, opening_balances, filters, company_currency
)
accumulate_values_into_parents(accounts, accounts_by_name) accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency) data = prepare_data(accounts, filters, parent_children_map, company_currency)
data = filter_out_zero_value_rows( data = filter_out_zero_value_rows(
data, parent_children_map, show_zero_values=filters.get("show_zero_values") data, parent_children_map, show_zero_values=filters.get("show_zero_values")
) )
@ -140,45 +138,125 @@ def get_opening_balances(filters):
def get_rootwise_opening_balances(filters, report_type): def get_rootwise_opening_balances(filters, report_type):
additional_conditions = "" gle = []
if not filters.show_unclosed_fy_pl_balances:
additional_conditions = (
" and posting_date >= %(year_start_date)s" if report_type == "Profit and Loss" else ""
)
if not flt(filters.with_period_closing_entry): last_period_closing_voucher = frappe.db.get_all(
additional_conditions += " and ifnull(voucher_type, '')!='Period Closing Voucher'" "Period Closing Voucher",
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
if filters.cost_center: fields=["posting_date", "name"],
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) order_by="posting_date desc",
additional_conditions += """ and cost_center in (select name from `tabCost Center` limit=1,
where lft >= %s and rgt <= %s)""" % ( )
lft,
rgt,
)
if filters.project:
additional_conditions += " and project = %(project)s"
if filters.get("include_default_book_entries"):
additional_conditions += (
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
else:
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
accounting_dimensions = get_accounting_dimensions(as_list=False) accounting_dimensions = get_accounting_dimensions(as_list=False)
query_filters = { if last_period_closing_voucher:
"company": filters.company, gle = get_opening_balance(
"from_date": filters.from_date, "Account Closing Balance",
"to_date": filters.to_date, filters,
"report_type": report_type, report_type,
"year_start_date": filters.year_start_date, accounting_dimensions,
"project": filters.project, period_closing_voucher=last_period_closing_voucher[0].name,
"finance_book": filters.finance_book, )
"company_fb": frappe.get_cached_value("Company", filters.company, "default_finance_book"), if getdate(last_period_closing_voucher[0].posting_date) < getdate(
} add_days(filters.from_date, -1)
):
start_date = add_days(last_period_closing_voucher[0].posting_date, 1)
gle += get_opening_balance(
"GL Entry", filters, report_type, accounting_dimensions, start_date=start_date
)
else:
gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions)
opening = frappe._dict()
for d in gle:
opening.setdefault(
d.account,
{
"account": d.account,
"opening_debit": 0.0,
"opening_credit": 0.0,
},
)
opening[d.account]["opening_debit"] += flt(d.opening_debit)
opening[d.account]["opening_credit"] += flt(d.opening_credit)
return opening
def get_opening_balance(
doctype, filters, report_type, accounting_dimensions, period_closing_voucher=None, start_date=None
):
closing_balance = frappe.qb.DocType(doctype)
account = frappe.qb.DocType("Account")
opening_balance = (
frappe.qb.from_(closing_balance)
.select(
closing_balance.account,
Sum(closing_balance.debit).as_("opening_debit"),
Sum(closing_balance.credit).as_("opening_credit"),
)
.where(
(closing_balance.company == filters.company)
& (
closing_balance.account.isin(
frappe.qb.from_(account).select("name").where(account.report_type == report_type)
)
)
)
.groupby(closing_balance.account)
)
if period_closing_voucher:
opening_balance = opening_balance.where(
closing_balance.period_closing_voucher == period_closing_voucher
)
else:
if start_date:
opening_balance = opening_balance.where(closing_balance.posting_date >= start_date)
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date)
if (
not filters.show_unclosed_fy_pl_balances
and report_type == "Profit and Loss"
and doctype == "GL Entry"
):
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
if not flt(filters.with_period_closing_entry):
if doctype == "Account Closing Balance":
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
else:
opening_balance = opening_balance.where(
closing_balance.voucher_type != "Period Closing Voucher"
)
if filters.cost_center:
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
cost_center = frappe.qb.DocType("Cost Center")
opening_balance = opening_balance.where(
closing_balance.cost_center.in_(
frappe.qb.from_(cost_center)
.select("name")
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
)
)
if filters.project:
opening_balance = opening_balance.where(closing_balance.project == filters.project)
if filters.get("include_default_book_entries"):
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
| (closing_balance.finance_book.isnull())
)
else:
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
| (closing_balance.finance_book.isnull())
)
if accounting_dimensions: if accounting_dimensions:
for dimension in accounting_dimensions: for dimension in accounting_dimensions:
@ -187,38 +265,20 @@ def get_rootwise_opening_balances(filters, report_type):
filters[dimension.fieldname] = get_dimension_with_children( filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname) dimension.document_type, filters.get(dimension.fieldname)
) )
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname) opening_balance = opening_balance.where(
closing_balance[dimension.fieldname].isin(filters[dimension.fieldname])
)
else: else:
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname) opening_balance = opening_balance.where(
closing_balance[dimension.fieldname].isin(filters[dimension.fieldname])
)
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)}) gle = opening_balance.run(as_dict=1)
gle = frappe.db.sql( return gle
"""
select
account, sum(debit) as opening_debit, sum(credit) as opening_credit
from `tabGL Entry`
where
company=%(company)s
{additional_conditions}
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
and account in (select name from `tabAccount` where report_type=%(report_type)s)
and is_cancelled = 0
group by account""".format(
additional_conditions=additional_conditions
),
query_filters,
as_dict=True,
)
opening = frappe._dict()
for d in gle:
opening.setdefault(d.account, d)
return opening
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency): def calculate_values(accounts, gl_entries_by_account, opening_balances):
init = { init = {
"opening_debit": 0.0, "opening_debit": 0.0,
"opening_credit": 0.0, "opening_credit": 0.0,
@ -228,22 +288,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
"closing_credit": 0.0, "closing_credit": 0.0,
} }
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
"warn_if_negative": True,
"opening_debit": 0.0,
"opening_credit": 0.0,
"debit": 0.0,
"credit": 0.0,
"closing_debit": 0.0,
"closing_credit": 0.0,
"parent_account": None,
"indent": 0,
"has_value": True,
"currency": company_currency,
}
for d in accounts: for d in accounts:
d.update(init.copy()) d.update(init.copy())
@ -261,8 +305,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
prepare_opening_closing(d) prepare_opening_closing(d)
for field in value_fields:
total_row[field] += d[field] def calculate_total_row(accounts, company_currency):
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
"warn_if_negative": True,
"opening_debit": 0.0,
"opening_credit": 0.0,
"debit": 0.0,
"credit": 0.0,
"closing_debit": 0.0,
"closing_credit": 0.0,
"parent_account": None,
"indent": 0,
"has_value": True,
"currency": company_currency,
}
for d in accounts:
if not d.parent_account:
for field in value_fields:
total_row[field] += d[field]
return total_row return total_row
@ -274,7 +338,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
accounts_by_name[d.parent_account][key] += d[key] accounts_by_name[d.parent_account][key] += d[key]
def prepare_data(accounts, filters, total_row, parent_children_map, company_currency): def prepare_data(accounts, filters, parent_children_map, company_currency):
data = [] data = []
for d in accounts: for d in accounts:
@ -305,6 +369,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
row["has_value"] = has_value row["has_value"] = has_value
data.append(row) data.append(row)
total_row = calculate_total_row(accounts, company_currency)
data.extend([{}, total_row]) data.extend([{}, total_row])
return data return data

View File

@ -30,10 +30,6 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Sales Register", {}), ("Sales Register", {}),
("Sales Register", {"item_group": "All Item Groups"}), ("Sales Register", {"item_group": "All Item Groups"}),
("Purchase Register", {}), ("Purchase Register", {}),
(
"Tax Detail",
{"mode": "run", "report_name": "Tax Detail"},
),
] ]
OPTIONAL_FILTERS = {} OPTIONAL_FILTERS = {}

View File

@ -51,13 +51,25 @@ GL_REPOSTING_CHUNK = 100
@frappe.whitelist() @frappe.whitelist()
def get_fiscal_year( def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
): ):
return get_fiscal_years(date, fiscal_year, label, verbose, company, as_dict=as_dict)[0] fiscal_years = get_fiscal_years(
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
)
if boolean:
return fiscal_years
else:
return fiscal_years[0]
def get_fiscal_years( def get_fiscal_years(
transaction_date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False transaction_date=None,
fiscal_year=None,
label="Date",
verbose=1,
company=None,
as_dict=False,
boolean=False,
): ):
fiscal_years = frappe.cache().hget("fiscal_years", company) or [] fiscal_years = frappe.cache().hget("fiscal_years", company) or []
@ -121,8 +133,12 @@ def get_fiscal_years(
if company: if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
if boolean:
return False
if verbose == 1: if verbose == 1:
frappe.msgprint(error_msg) frappe.msgprint(error_msg)
raise FiscalYearError(error_msg) raise FiscalYearError(error_msg)
@ -451,12 +467,6 @@ def reconcile_against_document(args): # nosemgrep
else: else:
update_reference_in_payment_entry(entry, doc, do_not_save=True) update_reference_in_payment_entry(entry, doc, do_not_save=True)
if doc.doctype == "Journal Entry":
try:
doc.validate_total_debit_and_credit()
except Exception as validation_exception:
raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
# re-submit advance entry # re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)

View File

@ -13,6 +13,7 @@
"hide_custom": 0, "hide_custom": 0,
"icon": "accounting", "icon": "accounting",
"idx": 0, "idx": 0,
"is_hidden": 0,
"label": "Accounting", "label": "Accounting",
"links": [ "links": [
{ {
@ -493,17 +494,6 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Tax Detail",
"link_count": 0,
"link_to": "Tax Detail",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{ {
"dependencies": "GL Entry", "dependencies": "GL Entry",
"hidden": 0, "hidden": 0,
@ -516,18 +506,6 @@
"only_for": "United Arab Emirates", "only_for": "United Arab Emirates",
"type": "Link" "type": "Link"
}, },
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "KSA VAT Report",
"link_count": 0,
"link_to": "KSA VAT",
"link_type": "Report",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@ -1029,17 +1007,6 @@
"only_for": "India", "only_for": "India",
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "KSA VAT Setting",
"link_count": 0,
"link_to": "KSA VAT Setting",
"link_type": "DocType",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@ -1093,7 +1060,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2022-06-24 05:41:09.236458", "modified": "2023-02-23 15:32:12.135355",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",

View File

@ -466,6 +466,9 @@ frappe.ui.form.on('Asset', {
} else { } else {
frm.set_value('purchase_date', purchase_doc.posting_date); frm.set_value('purchase_date', purchase_doc.posting_date);
} }
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
frm.set_value('available_for_use_date', frm.doc.purchase_date);
}
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code); const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) { if (!item) {
doctype_field = frappe.scrub(doctype) doctype_field = frappe.scrub(doctype)

View File

@ -79,6 +79,9 @@
"options": "ACC-ASS-.YYYY.-" "options": "ACC-ASS-.YYYY.-"
}, },
{ {
"depends_on": "item_code",
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
"fieldname": "asset_name", "fieldname": "asset_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
@ -517,7 +520,7 @@
"table_fieldname": "accounts" "table_fieldname": "accounts"
} }
], ],
"modified": "2023-02-02 00:03:11.706427", "modified": "2023-03-30 15:07:41.542374",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -8,16 +8,12 @@ import math
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import ( from frappe.utils import (
add_months,
cint, cint,
date_diff,
flt, flt,
get_datetime, get_datetime,
get_last_day, get_last_day,
get_link_to_form, get_link_to_form,
getdate, getdate,
is_last_day_of_the_month,
month_diff,
nowdate, nowdate,
today, today,
) )
@ -239,30 +235,6 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
) )
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = self.get_modified_available_for_use_date(row)
days = date_diff(row.depreciation_start_date, from_date) + 1
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
def get_modified_available_for_use_date(self, row):
return add_months(
self.available_for_use_date,
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
)
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw( frappe.throw(
@ -471,25 +443,6 @@ class Asset(AccountsController):
return records return records
@erpnext.allow_regional
def get_depreciation_amount(self, depreciable_value, fb_row):
if fb_row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not self.flags.increase_in_asset_life:
depreciation_amount = (
flt(self.gross_purchase_amount) - flt(fb_row.expected_value_after_useful_life)
) / flt(fb_row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (
flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life)
) / (date_diff(self.to_date, self.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(fb_row.rate_of_depreciation) / 100))
return depreciation_amount
def validate_make_gl_entry(self): def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
if not purchase_document: if not purchase_document:
@ -614,23 +567,42 @@ class Asset(AccountsController):
float_precision = cint(frappe.db.get_default("float_precision")) or 2 float_precision = cint(frappe.db.get_default("float_precision")) or 2
if args.get("depreciation_method") == "Double Declining Balance": if args.get("depreciation_method") == "Double Declining Balance":
return 200.0 / args.get("total_number_of_depreciations") return 200.0 / (
(
flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation"))
)
/ 12
)
if args.get("depreciation_method") == "Written Down Value": if args.get("depreciation_method") == "Written Down Value":
if args.get("rate_of_depreciation") and on_validate: if (
args.get("rate_of_depreciation")
and on_validate
and not self.flags.increase_in_asset_value_due_to_repair
):
return args.get("rate_of_depreciation") return args.get("rate_of_depreciation")
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) if self.flags.increase_in_asset_value_due_to_repair:
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2)) value = flt(args.get("expected_value_after_useful_life")) / flt(
args.get("value_after_depreciation")
)
else:
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
depreciation_rate = math.pow(
value,
1.0
/ (
(
flt(args.get("total_number_of_depreciations"), 2)
* flt(args.get("frequency_of_depreciation"))
)
/ 12
),
)
return flt((100 * (1 - depreciation_rate)), float_precision) return flt((100 * (1 - depreciation_rate)), float_precision)
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
def update_maintenance_status(): def update_maintenance_status():
assets = frappe.get_all( assets = frappe.get_all(
@ -874,15 +846,6 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
return asset.get_value_after_depreciation(finance_book) return asset.get_value_after_depreciation(finance_book)
def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1)
if is_last_day_of_the_month(date):
period_start_date = get_last_day(period_start_date)
return date_diff(date, period_start_date)
@frappe.whitelist() @frappe.whitelist()
def split_asset(asset_name, split_qty): def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)

View File

@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = {
} }
}, },
onload: function(me) { onload: function(me) {
me.page.add_action_item('Make Asset Movement', function() { me.page.add_action_item(__("Make Asset Movement"), function() {
const assets = me.get_checked_items(); const assets = me.get_checked_items();
frappe.call({ frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement", method: "erpnext.assets.doctype.asset.asset.make_asset_movement",

View File

@ -249,10 +249,16 @@ def notify_depr_entry_posting_error(failed_asset_names):
asset_links = get_comma_separated_asset_links(failed_asset_names) asset_links = get_comma_separated_asset_links(failed_asset_names)
message = ( message = (
_("Hi,") _("Hello,")
+ "<br>" + "<br><br>"
+ _("The following assets have failed to post depreciation entries: {0}").format(asset_links) + _("The following assets have failed to automatically post depreciation entries: {0}").format(
asset_links
)
+ "." + "."
+ "<br><br>"
+ _(
"Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table."
)
) )
frappe.sendmail(recipients=recipients, subject=subject, message=message) frappe.sendmail(recipients=recipients, subject=subject, message=message)

View File

@ -29,8 +29,11 @@ from erpnext.assets.doctype.asset.depreciation import (
scrap_asset, scrap_asset,
) )
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
_check_is_pro_rata,
_get_pro_rata_amt,
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depr_schedule, get_depr_schedule,
get_depreciation_amount,
) )
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_invoice, make_purchase_invoice as make_invoice,
@ -234,7 +237,7 @@ class TestAsset(AssetSetup):
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"), asset.precision("gross_purchase_amount"),
) )
pro_rata_amount, _, _ = asset.get_pro_rata_amt( pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
) )
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
@ -321,7 +324,7 @@ class TestAsset(AssetSetup):
self.assertEquals(second_asset_depr_schedule.status, "Active") self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled") self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
pro_rata_amount, _, _ = asset.get_pro_rata_amt( pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
) )
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
@ -828,8 +831,8 @@ class TestDepreciationMethods(AssetSetup):
expected_schedules = [ expected_schedules = [
["2030-12-31", 28630.14, 28630.14], ["2030-12-31", 28630.14, 28630.14],
["2031-12-31", 35684.93, 64315.07], ["2031-12-31", 35684.93, 64315.07],
["2032-12-31", 17842.47, 82157.54], ["2032-12-31", 17842.46, 82157.53],
["2033-06-06", 5342.46, 87500.0], ["2033-06-06", 5342.47, 87500.0],
] ]
schedules = [ schedules = [
@ -857,12 +860,12 @@ class TestDepreciationMethods(AssetSetup):
) )
expected_schedules = [ expected_schedules = [
["2022-02-28", 647.25, 647.25], ["2022-02-28", 310.89, 310.89],
["2022-03-31", 1210.71, 1857.96], ["2022-03-31", 654.45, 965.34],
["2022-04-30", 1053.99, 2911.95], ["2022-04-30", 654.45, 1619.79],
["2022-05-31", 917.55, 3829.5], ["2022-05-31", 654.45, 2274.24],
["2022-06-30", 798.77, 4628.27], ["2022-06-30", 654.45, 2928.69],
["2022-07-15", 371.73, 5000.0], ["2022-07-15", 2071.31, 5000.0],
] ]
schedules = [ schedules = [
@ -938,7 +941,7 @@ class TestDepreciationBasics(AssetSetup):
}, },
) )
depreciation_amount = asset.get_depreciation_amount(100000, asset.finance_books[0]) depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
self.assertEqual(depreciation_amount, 30000) self.assertEqual(depreciation_amount, 30000)
def test_make_depr_schedule(self): def test_make_depr_schedule(self):
@ -997,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup):
}, },
) )
has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
self.assertFalse(has_pro_rata) self.assertFalse(has_pro_rata)
asset.finance_books = [] asset.finance_books = []
@ -1012,7 +1015,7 @@ class TestDepreciationBasics(AssetSetup):
}, },
) )
has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
self.assertTrue(has_pro_rata) self.assertTrue(has_pro_rata)
def test_expected_value_after_useful_life_greater_than_purchase_amount(self): def test_expected_value_after_useful_life_greater_than_purchase_amount(self):

View File

@ -43,9 +43,9 @@ erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return; if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) {
$.each(frm.doc.depreciation_schedule || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount); accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name, frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
"accumulated_depreciation_amount", accumulated_depreciation);
}) })
}; };

View File

@ -10,7 +10,9 @@
"asset", "asset",
"naming_series", "naming_series",
"column_break_2", "column_break_2",
"gross_purchase_amount",
"opening_accumulated_depreciation", "opening_accumulated_depreciation",
"number_of_depreciations_booked",
"finance_book", "finance_book",
"finance_book_id", "finance_book_id",
"depreciation_details_section", "depreciation_details_section",
@ -148,18 +150,36 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation", "fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1,
"label": "Opening Accumulated Depreciation", "label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Gross Purchase Amount",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "number_of_depreciations_booked",
"fieldtype": "Int",
"hidden": 1,
"label": "Number of Depreciations Booked",
"print_hide": 1,
"read_only": 1 "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-01-16 21:08:21.421260", "modified": "2023-02-26 16:37:23.734806",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Depreciation Schedule", "name": "Asset Depreciation Schedule",

View File

@ -4,7 +4,19 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month from frappe.utils import (
add_days,
add_months,
cint,
date_diff,
flt,
get_last_day,
getdate,
is_last_day_of_the_month,
month_diff,
)
import erpnext
class AssetDepreciationSchedule(Document): class AssetDepreciationSchedule(Document):
@ -83,15 +95,58 @@ class AssetDepreciationSchedule(Document):
date_of_return=None, date_of_return=None,
update_asset_finance_book_row=True, update_asset_finance_book_row=True,
): ):
have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc)
not_manual_depr_or_have_manual_depr_details_been_modified = (
self.not_manual_depr_or_have_manual_depr_details_been_modified(row)
)
self.set_draft_asset_depr_schedule_details(asset_doc, row) self.set_draft_asset_depr_schedule_details(asset_doc, row)
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
self.set_accumulated_depreciation(row, date_of_disposal, date_of_return) if self.should_prepare_depreciation_schedule(
have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
def have_asset_details_been_modified(self, asset_doc):
return (
asset_doc.gross_purchase_amount != self.gross_purchase_amount
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
)
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
return (
self.depreciation_method != "Manual"
or row.total_number_of_depreciations != self.total_number_of_depreciations
or row.frequency_of_depreciation != self.frequency_of_depreciation
or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date
or row.expected_value_after_useful_life != self.expected_value_after_useful_life
)
def should_prepare_depreciation_schedule(
self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
if not self.get("depreciation_schedule"):
return True
old_asset_depr_schedule_doc = self.get_doc_before_save()
if self.docstatus != 0 and not old_asset_depr_schedule_doc:
return True
if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified:
return True
return False
def set_draft_asset_depr_schedule_details(self, asset_doc, row): def set_draft_asset_depr_schedule_details(self, asset_doc, row):
self.asset = asset_doc.name self.asset = asset_doc.name
self.finance_book = row.finance_book self.finance_book = row.finance_book
self.finance_book_id = row.idx self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations self.total_number_of_depreciations = row.total_number_of_depreciations
self.frequency_of_depreciation = row.frequency_of_depreciation self.frequency_of_depreciation = row.frequency_of_depreciation
@ -102,7 +157,7 @@ class AssetDepreciationSchedule(Document):
def make_depr_schedule( def make_depr_schedule(
self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
): ):
if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"): if not self.get("depreciation_schedule"):
self.depreciation_schedule = [] self.depreciation_schedule = []
if not asset_doc.available_for_use_date: if not asset_doc.available_for_use_date:
@ -141,24 +196,49 @@ class AssetDepreciationSchedule(Document):
row.db_update() row.db_update()
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
asset_doc.number_of_depreciations_booked self.number_of_depreciations_booked
) )
has_pro_rata = asset_doc.check_is_pro_rata(row) has_pro_rata = _check_is_pro_rata(asset_doc, row)
if has_pro_rata: if has_pro_rata:
number_of_pending_depreciations += 1 number_of_pending_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False
if (
row.depreciation_method in ("Written Down Value", "Double Declining Balance")
and cint(row.frequency_of_depreciation) != 12
):
has_wdv_or_dd_non_yearly_pro_rata = _check_is_pro_rata(
asset_doc, row, wdv_or_dd_non_yearly=True
)
skip_row = False skip_row = False
should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date) should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date)
depreciation_amount = 0
for n in range(start, number_of_pending_depreciations): for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance) # If depreciation is already completed (for double declining balance)
if skip_row: if skip_row:
continue continue
depreciation_amount = asset_doc.get_depreciation_amount(value_after_depreciation, row) if n > 0 and len(self.get("depreciation_schedule")) > n - 1:
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
else:
prev_depreciation_amount = 0
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: depreciation_amount = get_depreciation_amount(
asset_doc,
value_after_depreciation,
row,
n,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
)
if not has_pro_rata or (
n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
):
schedule_date = add_months( schedule_date = add_months(
row.depreciation_start_date, n * cint(row.frequency_of_depreciation) row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
) )
@ -176,26 +256,36 @@ class AssetDepreciationSchedule(Document):
if self.depreciation_schedule: if self.depreciation_schedule:
from_date = self.depreciation_schedule[-1].schedule_date from_date = self.depreciation_schedule[-1].schedule_date
depreciation_amount, days, months = asset_doc.get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, depreciation_amount, from_date, date_of_disposal row,
depreciation_amount,
from_date,
date_of_disposal,
) )
if depreciation_amount > 0: if depreciation_amount > 0:
self.add_depr_schedule_row( self.add_depr_schedule_row(
date_of_disposal, date_of_disposal,
depreciation_amount, depreciation_amount,
row.depreciation_method,
) )
break break
# For first row # For first row
if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0: if (
(has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation
and n == 0
):
from_date = add_days( from_date = add_days(
asset_doc.available_for_use_date, -1 asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too ) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = asset_doc.get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, depreciation_amount, from_date, row.depreciation_start_date row,
depreciation_amount,
from_date,
row.depreciation_start_date,
has_wdv_or_dd_non_yearly_pro_rata,
) )
# For first depr schedule date will be the start date # For first depr schedule date will be the start date
@ -209,13 +299,17 @@ class AssetDepreciationSchedule(Document):
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months( asset_doc.to_date = add_months(
asset_doc.available_for_use_date, asset_doc.available_for_use_date,
(n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), (n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
) )
depreciation_amount_without_pro_rata = depreciation_amount depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = asset_doc.get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, depreciation_amount, schedule_date, asset_doc.to_date row,
depreciation_amount,
schedule_date,
asset_doc.to_date,
has_wdv_or_dd_non_yearly_pro_rata,
) )
depreciation_amount = self.get_adjusted_depreciation_amount( depreciation_amount = self.get_adjusted_depreciation_amount(
@ -247,7 +341,6 @@ class AssetDepreciationSchedule(Document):
self.add_depr_schedule_row( self.add_depr_schedule_row(
schedule_date, schedule_date,
depreciation_amount, depreciation_amount,
row.depreciation_method,
) )
# to ensure that final accumulated depreciation amount is accurate # to ensure that final accumulated depreciation amount is accurate
@ -274,14 +367,12 @@ class AssetDepreciationSchedule(Document):
self, self,
schedule_date, schedule_date,
depreciation_amount, depreciation_amount,
depreciation_method,
): ):
self.append( self.append(
"depreciation_schedule", "depreciation_schedule",
{ {
"schedule_date": schedule_date, "schedule_date": schedule_date,
"depreciation_amount": depreciation_amount, "depreciation_amount": depreciation_amount,
"depreciation_method": depreciation_method,
}, },
) )
@ -293,7 +384,9 @@ class AssetDepreciationSchedule(Document):
ignore_booked_entry=False, ignore_booked_entry=False,
): ):
straight_line_idx = [ straight_line_idx = [
d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line" d.idx
for d in self.get("depreciation_schedule")
if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual"
] ]
accumulated_depreciation = flt(self.opening_accumulated_depreciation) accumulated_depreciation = flt(self.opening_accumulated_depreciation)
@ -336,6 +429,132 @@ def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
return value_after_depreciation return value_after_depreciation
# if it returns True, depreciation_amount will not be equal for the first and last rows
def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
has_pro_rata = False
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
days = date_diff(row.depreciation_start_date, from_date) + 1
if wdv_or_dd_non_yearly:
total_days = get_total_days(row.depreciation_start_date, 12)
else:
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False):
if wdv_or_dd_non_yearly:
return add_months(
asset_doc.available_for_use_date,
(asset_doc.number_of_depreciations_booked * 12),
)
else:
return add_months(
asset_doc.available_for_use_date,
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
)
def _get_pro_rata_amt(
row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False
):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
if has_wdv_or_dd_non_yearly_pro_rata:
total_days = get_total_days(to_date, 12)
else:
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1)
if is_last_day_of_the_month(date):
period_start_date = get_last_day(period_start_date)
return date_diff(date, period_start_date)
@erpnext.allow_regional
def get_depreciation_amount(
asset,
depreciable_value,
row,
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
):
if row.depreciation_method in ("Straight Line", "Manual"):
return get_straight_line_or_manual_depr_amount(asset, row)
else:
return get_wdv_or_dd_depr_amount(
depreciable_value,
row.rate_of_depreciation,
row.frequency_of_depreciation,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
)
def get_straight_line_or_manual_depr_amount(asset, row):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
date_diff(asset.to_date, asset.available_for_use_date) / 365
)
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
elif asset.flags.increase_in_asset_value_due_to_repair:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
# if the Depreciation Schedule is being prepared for the first time
else:
return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
def get_wdv_or_dd_depr_amount(
depreciable_value,
rate_of_depreciation,
frequency_of_depreciation,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
):
if cint(frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
else:
if has_wdv_or_dd_non_yearly_pro_rata:
if schedule_idx == 0:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
)
else:
return prev_depreciation_amount
else:
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
)
else:
return prev_depreciation_amount
def make_draft_asset_depr_schedules_if_not_present(asset_doc): def make_draft_asset_depr_schedules_if_not_present(asset_doc):
for row in asset_doc.get("finance_books"): for row in asset_doc.get("finance_books"):
draft_asset_depr_schedule_name = get_asset_depr_schedule_name( draft_asset_depr_schedule_name = get_asset_depr_schedule_name(
@ -412,6 +631,16 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
"Written Down Value",
"Double Declining Balance",
):
new_rate_of_depreciation = flt(
asset_doc.get_depreciation_rate(row), row.precision("rate_of_depreciation")
)
row.rate_of_depreciation = new_rate_of_depreciation
new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation
new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal) new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal)
new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return) new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return)

View File

@ -84,6 +84,8 @@ def calculate_next_due_date(
next_due_date = add_years(start_date, 1) next_due_date = add_years(start_date, 1)
if periodicity == "2 Yearly": if periodicity == "2 Yearly":
next_due_date = add_years(start_date, 2) next_due_date = add_years(start_date, 2)
if periodicity == "3 Yearly":
next_due_date = add_years(start_date, 3)
if periodicity == "Quarterly": if periodicity == "Quarterly":
next_due_date = add_months(start_date, 3) next_due_date = add_months(start_date, 3)
if end_date and ( if end_date and (

View File

@ -1,664 +1,156 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2017-10-20 07:10:55.903571", "creation": "2017-10-20 07:10:55.903571",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"maintenance_task",
"maintenance_type",
"column_break_2",
"maintenance_status",
"section_break_2",
"start_date",
"periodicity",
"column_break_4",
"end_date",
"certificate_required",
"section_break_9",
"assign_to",
"column_break_10",
"assign_to_name",
"section_break_10",
"next_due_date",
"column_break_14",
"last_completion_date",
"section_break_7",
"description"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maintenance_task", "fieldname": "maintenance_task",
"fieldtype": "Data", "fieldtype": "Data",
"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": "Maintenance Task", "label": "Maintenance Task",
"length": 0, "reqd": 1
"no_copy": 0,
"options": "",
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maintenance_type", "fieldname": "maintenance_type",
"fieldtype": "Select", "fieldtype": "Select",
"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": "Maintenance Type", "label": "Maintenance Type",
"length": 0, "options": "Preventive Maintenance\nCalibration"
"no_copy": 0,
"options": "Preventive Maintenance\nCalibration",
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"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,
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "maintenance_status", "fieldname": "maintenance_status",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Maintenance Status", "label": "Maintenance Status",
"length": 0,
"no_copy": 0,
"options": "Planned\nOverdue\nCancelled", "options": "Planned\nOverdue\nCancelled",
"permlevel": 0, "reqd": 1
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_2", "fieldname": "section_break_2",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"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,
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today", "default": "Today",
"fieldname": "start_date", "fieldname": "start_date",
"fieldtype": "Date", "fieldtype": "Date",
"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": "Start Date", "label": "Start Date",
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "periodicity", "fieldname": "periodicity",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Periodicity", "label": "Periodicity",
"length": 0, "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
"no_copy": 0, "reqd": 1
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly",
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"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,
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "end_date", "fieldname": "end_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0, "label": "End Date"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "End Date",
"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, "default": "0",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "certificate_required", "fieldname": "certificate_required",
"fieldtype": "Check", "fieldtype": "Check",
"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": "Certificate Required", "label": "Certificate Required",
"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": 1, "search_index": 1,
"set_only_once": 1, "set_only_once": 1
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9", "fieldname": "section_break_9",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"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,
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "assign_to", "fieldname": "assign_to",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Assign To", "label": "Assign To",
"length": 0, "options": "User"
"no_copy": 0,
"options": "User",
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"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,
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "assign_to.full_name", "fetch_from": "assign_to.full_name",
"fieldname": "assign_to_name", "fieldname": "assign_to_name",
"fieldtype": "Read Only", "fieldtype": "Read Only",
"hidden": 0, "label": "Assign to Name"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Assign to Name",
"length": 0,
"no_copy": 0,
"options": "",
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_10", "fieldname": "section_break_10",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"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,
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "next_due_date", "fieldname": "next_due_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Next Due Date"
"label": "Next Due Date",
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_14", "fieldname": "column_break_14",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"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,
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "last_completion_date", "fieldname": "last_completion_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Last Completion Date"
"label": "Last Completion Date",
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7", "fieldname": "section_break_7",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"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,
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"hidden": 0, "label": "Description"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"options": "",
"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
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2018-06-18 16:12:04.330021", "modified": "2023-03-23 07:03:07.113452",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Maintenance Task", "name": "Asset Maintenance Task",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"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": 0,
"track_seen": 0
} }

View File

@ -43,53 +43,57 @@ class AssetRepair(AccountsController):
def before_submit(self): def before_submit(self):
self.check_repair_status() self.check_repair_status()
if self.get("stock_consumption") or self.get("capitalize_repair_cost"): self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
self.increase_asset_value()
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
if self.get("capitalize_repair_cost"):
self.make_gl_entries()
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
self.modify_depreciation_schedule()
notes = _( if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
"This schedule was created when Asset {0} was repaired through Asset Repair {1}." self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
).format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), self.increase_asset_value()
get_link_to_form(self.doctype, self.name),
) if self.get("stock_consumption"):
self.asset_doc.flags.ignore_validate_update_after_submit = True self.check_for_stock_items_and_warehouse()
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.decrease_stock_quantity()
self.asset_doc.save() if self.get("capitalize_repair_cost"):
self.make_gl_entries()
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
self.modify_depreciation_schedule()
notes = _(
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
).format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
def before_cancel(self): def before_cancel(self):
self.asset_doc = frappe.get_doc("Asset", self.asset) self.asset_doc = frappe.get_doc("Asset", self.asset)
if self.get("stock_consumption") or self.get("capitalize_repair_cost"): self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
self.decrease_asset_value()
if self.get("stock_consumption"):
self.increase_stock_quantity()
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True)
self.db_set("stock_entry", None)
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
self.revert_depreciation_schedule_on_cancellation()
notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format( if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
get_link_to_form(self.doctype, self.name),
) self.decrease_asset_value()
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) if self.get("stock_consumption"):
self.asset_doc.save() self.increase_stock_quantity()
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True)
self.db_set("stock_entry", None)
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
self.revert_depreciation_schedule_on_cancellation()
notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
def after_delete(self): def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status() frappe.get_doc("Asset", self.asset).set_status()

View File

@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
frm.call({ frm.call({
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation", method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
args: { args: {
asset: frm.doc.asset, asset_name: frm.doc.asset,
finance_book: frm.doc.finance_book finance_book: frm.doc.finance_book
}, },
callback: function(r) { callback: function(r) {

View File

@ -14,6 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depreciation_amount,
) )
@ -162,7 +163,7 @@ class AssetValueAdjustment(Document):
depreciation_amount = days * rate_per_day depreciation_amount = days * rate_per_day
from_date = data.schedule_date from_date = data.schedule_date
else: else:
depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d) depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
if depreciation_amount: if depreciation_amount:
value_after_depreciation -= flt(depreciation_amount) value_after_depreciation -= flt(depreciation_amount)

View File

@ -12,8 +12,7 @@
"column_break_3", "column_break_3",
"accumulated_depreciation_amount", "accumulated_depreciation_amount",
"journal_entry", "journal_entry",
"make_depreciation_entry", "make_depreciation_entry"
"depreciation_method"
], ],
"fields": [ "fields": [
{ {
@ -58,20 +57,11 @@
"fieldname": "make_depreciation_entry", "fieldname": "make_depreciation_entry",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Make Depreciation Entry" "label": "Make Depreciation Entry"
},
{
"fieldname": "depreciation_method",
"fieldtype": "Select",
"hidden": 1,
"label": "Depreciation Method",
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
"print_hide": 1,
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-12-06 20:35:50.264281", "modified": "2023-03-13 23:17:15.849950",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Depreciation Schedule", "name": "Depreciation Schedule",

View File

@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = {
"label": __("Period Based On"), "label": __("Period Based On"),
"fieldtype": "Select", "fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"], "options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"], "default": "Fiscal Year",
"reqd": 1 "reqd": 1
}, },
{ {
@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldtype: "Link", fieldtype: "Link",
options: "Asset Category" options: "Asset Category"
}, },
{
fieldname:"finance_book",
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book"
},
{ {
fieldname:"cost_center", fieldname:"cost_center",
label: __("Cost Center"), label: __("Cost Center"),
@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = {
reqd: 1 reqd: 1
}, },
{ {
fieldname:"is_existing_asset", fieldname:"finance_book",
label: __("Is Existing Asset"), label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book",
depends_on: "eval: doc.only_depreciable_assets == 1",
},
{
fieldname:"only_depreciable_assets",
label: __("Only depreciable assets"),
fieldtype: "Check"
},
{
fieldname:"only_existing_assets",
label: __("Only existing assets"),
fieldtype: "Check" fieldtype: "Check"
}, },
] ]

View File

@ -45,8 +45,10 @@ def get_conditions(filters):
filters.year_end_date = getdate(fiscal_year.year_end_date) filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]] conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("is_existing_asset"): if filters.get("only_depreciable_assets"):
conditions["is_existing_asset"] = filters.get("is_existing_asset") conditions["calculate_depreciation"] = filters.get("only_depreciable_assets")
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"): if filters.get("asset_category"):
conditions["asset_category"] = filters.get("asset_category") conditions["asset_category"] = filters.get("asset_category")
if filters.get("cost_center"): if filters.get("cost_center"):
@ -102,19 +104,18 @@ def get_data(filters):
] ]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
assets_linked_to_fb = frappe.db.get_all( assets_linked_to_fb = None
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")}, if filters.only_depreciable_assets:
pluck="parent", assets_linked_to_fb = frappe.db.get_all(
) doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},
pluck="parent",
)
for asset in assets_record: for asset in assets_record:
if filters.finance_book: if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
if asset.asset_id not in assets_linked_to_fb: continue
continue
else:
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
continue
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book) asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
row = { row = {
@ -172,11 +173,11 @@ def prepare_chart_data(data, filters):
"datasets": [ "datasets": [
{ {
"name": _("Asset Value"), "name": _("Asset Value"),
"values": [d.get("asset_value") for d in labels_values_map.values()], "values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
}, },
{ {
"name": _("Depreciatied Amount"), "name": _("Depreciatied Amount"),
"values": [d.get("depreciated_amount") for d in labels_values_map.values()], "values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
}, },
], ],
}, },
@ -312,7 +313,7 @@ def get_columns(filters):
return [ return [
{ {
"label": _("Asset Id"), "label": _("Asset ID"),
"fieldtype": "Link", "fieldtype": "Link",
"fieldname": "asset_id", "fieldname": "asset_id",
"options": "Asset", "options": "Asset",

View File

@ -16,8 +16,10 @@
"transaction_settings_section", "transaction_settings_section",
"po_required", "po_required",
"pr_required", "pr_required",
"over_order_allowance",
"column_break_12", "column_break_12",
"maintain_same_rate", "maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice", "bill_for_rejected_quantity_in_purchase_invoice",
"disable_last_purchase_rate", "disable_last_purchase_rate",
@ -147,6 +149,21 @@
"fieldname": "show_pay_button", "fieldname": "show_pay_button",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal" "label": "Show Pay Button in Purchase Order Portal"
},
{
"default": "0",
"depends_on": "eval: !doc.maintain_same_rate",
"description": "Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.",
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
"fieldtype": "Check",
"label": "Set Landed Cost Based on Purchase Invoice Rate"
},
{
"default": "0",
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
"fieldname": "over_order_allowance",
"fieldtype": "Float",
"label": "Over Order Allowance (%)"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -154,7 +171,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-02-15 14:42:10.200679", "modified": "2023-03-02 17:02:14.404622",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -21,3 +21,10 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series", self.get("supp_master_name") == "Naming Series",
hide_name_field=False, hide_name_field=False,
) )
def before_save(self):
self.check_maintain_same_rate()
def check_maintain_same_rate(self):
if self.maintain_same_rate:
self.set_landed_cost_based_on_purchase_invoice_rate = 0

View File

@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
this.make_purchase_invoice, __('Create')); this.make_purchase_invoice, __('Create'));
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
__('Create')
);
} }
if(flt(doc.per_billed) < 100) { if(flt(doc.per_billed) < 100) {

View File

@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
from erpnext.accounts.party import get_party_account, get_party_account_currency from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController from erpnext.controllers.buying_controller import BuyingController
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
validate_against_blanket_order,
)
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
@ -69,6 +72,7 @@ class PurchaseOrder(BuyingController):
self.validate_with_previous_doc() self.validate_with_previous_doc()
self.validate_for_subcontracting() self.validate_for_subcontracting()
self.validate_minimum_order_qty() self.validate_minimum_order_qty()
validate_against_blanket_order(self)
if self.is_old_subcontracting_flow: if self.is_old_subcontracting_flow:
self.validate_bom_for_subcontracting_items() self.validate_bom_for_subcontracting_items()

View File

@ -113,7 +113,10 @@ class RequestforQuotation(BuyingController):
def get_link(self): def get_link(self):
# RFQ link for supplier portal # RFQ link for supplier portal
return get_url("/app/request-for-quotation/" + self.name) route = frappe.db.get_value(
"Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
)
return get_url("/app/{0}/".format(route) + self.name)
def update_supplier_part_no(self, supplier): def update_supplier_part_no(self, supplier):
self.vendor = supplier self.vendor = supplier

View File

@ -64,7 +64,7 @@ frappe.ui.form.on("Supplier", {
// custom buttons // custom buttons
frm.add_custom_button(__('Accounting Ledger'), function () { frm.add_custom_button(__('Accounting Ledger'), function () {
frappe.set_route('query-report', 'General Ledger', frappe.set_route('query-report', 'General Ledger',
{ party_type: 'Supplier', party: frm.doc.name }); { party_type: 'Supplier', party: frm.doc.name, party_name: frm.doc.supplier_name });
}, __("View")); }, __("View"));
frm.add_custom_button(__('Accounts Payable'), function () { frm.add_custom_button(__('Accounts Payable'), function () {

View File

@ -125,18 +125,9 @@ class Supplier(TransactionBase):
def on_trash(self): def on_trash(self):
if self.supplier_primary_contact: if self.supplier_primary_contact:
frappe.db.sql( self.db_set("supplier_primary_contact", None)
""" if self.supplier_primary_address:
UPDATE `tabSupplier` self.db_set("supplier_primary_address", None)
SET
supplier_primary_contact=null,
supplier_primary_address=null,
mobile_no=null,
email_id=null,
primary_address=null
WHERE name=%(name)s""",
{"name": self.name},
)
delete_contact_and_address("Supplier", self.name) delete_contact_and_address("Supplier", self.name)

View File

@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Item To Be Received"] = {
fieldname:"from_date", fieldname:"from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1 reqd: 1
}, },
{ {
fieldname:"to_date", fieldname:"to_date",
label: __("To Date"), label: __("To Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.get_today(),
reqd: 1 reqd: 1
}, },
] ]

View File

@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
fieldname:"from_date", fieldname:"from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1 reqd: 1
}, },
{ {
fieldname:"to_date", fieldname:"to_date",
label: __("To Date"), label: __("To Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.get_today(),
reqd: 1 reqd: 1
}, },
] ]

View File

@ -515,6 +515,7 @@ class AccountsController(TransactionBase):
parent_dict.update({"customer": parent_dict.get("party_name")}) parent_dict.update({"customer": parent_dict.get("party_name")})
self.pricing_rules = [] self.pricing_rules = []
for item in self.get("items"): for item in self.get("items"):
if item.get("item_code"): if item.get("item_code"):
args = parent_dict.copy() args = parent_dict.copy()
@ -833,7 +834,9 @@ class AccountsController(TransactionBase):
def set_advances(self): def set_advances(self):
"""Returns list of advances against Account, Party, Reference""" """Returns list of advances against Account, Party, Reference"""
res = self.get_advance_entries() res = self.get_advance_entries(
include_unallocated=not cint(self.get("only_include_allocated_payments"))
)
self.set("advances", []) self.set("advances", [])
advance_allocated = 0 advance_allocated = 0
@ -1232,7 +1235,7 @@ class AccountsController(TransactionBase):
) )
) )
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
from erpnext.controllers.status_updater import get_allowance_for from erpnext.controllers.status_updater import get_allowance_for
item_allowance = {} item_allowance = {}
@ -1245,17 +1248,20 @@ class AccountsController(TransactionBase):
total_overbilled_amt = 0.0 total_overbilled_amt = 0.0
reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
reference_details = self.get_billing_reference_details(
reference_names, ref_dt + " Item", based_on
)
for item in self.get("items"): for item in self.get("items"):
if not item.get(item_ref_dn): if not item.get(item_ref_dn):
continue continue
ref_amt = flt( ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on),
self.precision(based_on, item),
)
if not ref_amt: if not ref_amt:
frappe.msgprint( frappe.msgprint(
_("System will not check overbilling since amount for Item {0} in {1} is zero").format( _("System will not check over billing since amount for Item {0} in {1} is zero").format(
item.item_code, ref_dt item.item_code, ref_dt
), ),
title=_("Warning"), title=_("Warning"),
@ -1302,6 +1308,16 @@ class AccountsController(TransactionBase):
alert=True, alert=True,
) )
def get_billing_reference_details(self, reference_names, reference_doctype, based_on):
return frappe._dict(
frappe.get_all(
reference_doctype,
filters={"name": ("in", reference_names)},
fields=["name", based_on],
as_list=1,
)
)
def get_billed_amount_for_item(self, item, item_ref_dn, based_on): def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
""" """
Returns Sum of Amount of Returns Sum of Amount of

View File

@ -265,7 +265,10 @@ class BuyingController(SubcontractingController):
) / qty_in_stock_uom ) / qty_in_stock_uom
else: else:
item.valuation_rate = ( item.valuation_rate = (
item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount) item.base_net_amount
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
+ flt(item.get("rate_difference_with_purchase_invoice"))
) / qty_in_stock_uom ) / qty_in_stock_uom
else: else:
item.valuation_rate = 0.0 item.valuation_rate = 0.0

View File

@ -131,7 +131,7 @@ def validate_returned_items(doc):
) )
elif ref.serial_no: elif ref.serial_no:
if not d.serial_no: if d.qty and not d.serial_no:
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx)) frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
else: else:
serial_nos = get_serial_nos(d.serial_no) serial_nos = get_serial_nos(d.serial_no)
@ -400,6 +400,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
if serial_nos: if serial_nos:
target_doc.serial_no = "\n".join(serial_nos) target_doc.serial_no = "\n".join(serial_nos)
if source_doc.get("rejected_serial_no"):
returned_serial_nos = get_returned_serial_nos(
source_doc, source_parent, serial_no_field="rejected_serial_no"
)
rejected_serial_nos = list(
set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos)
)
if rejected_serial_nos:
target_doc.rejected_serial_no = "\n".join(rejected_serial_nos)
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row( returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype source_parent.name, source_parent.supplier, source_doc.name, doctype
@ -610,7 +620,7 @@ def get_filters(
return filters return filters
def get_returned_serial_nos(child_doc, parent_doc): def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
return_ref_field = frappe.scrub(child_doc.doctype) return_ref_field = frappe.scrub(child_doc.doctype)
@ -619,7 +629,7 @@ def get_returned_serial_nos(child_doc, parent_doc):
serial_nos = [] serial_nos = []
fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)] fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
filters = [ filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "return_against", "=", parent_doc.name],
@ -629,6 +639,6 @@ def get_returned_serial_nos(child_doc, parent_doc):
] ]
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
serial_nos.extend(get_serial_nos(row.serial_no)) serial_nos.extend(get_serial_nos(row.get(serial_no_field)))
return serial_nos return serial_nos

View File

@ -136,7 +136,7 @@ class SellingController(StockController):
self.in_words = money_in_words(amount, self.currency) self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self): def calculate_commission(self):
if not self.meta.get_field("commission_rate"): if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted():
return return
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate")) self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))

View File

@ -464,7 +464,7 @@ class StatusUpdater(Document):
ifnull((select ifnull((select
ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0) ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0)
/ sum(abs(%(target_ref_field)s)) * 100 / sum(abs(%(target_ref_field)s)) * 100
from `tab%(target_dt)s` where parent='%(name)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6) from `tab%(target_dt)s` where parent='%(name)s' and parenttype='%(target_parent_dt)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
%(update_modified)s %(update_modified)s
where name='%(name)s'""" where name='%(name)s'"""
% args % args

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