Merge remote-tracking branch 'upstream/develop' into move-exotel-to-separate-app
This commit is contained in:
commit
a0131a96cb
5
.github/helper/install.sh
vendored
5
.github/helper/install.sh
vendored
@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||
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
|
||||
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/redis_socketio:/# redis_socketio:/g' Procfile
|
||||
|
||||
bench get-app payments
|
||||
bench get-app payments --branch ${githubbranch%"-hotfix"}
|
||||
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
||||
|
||||
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||
|
1
.github/workflows/server-tests-mariadb.yml
vendored
1
.github/workflows/server-tests-mariadb.yml
vendored
@ -7,7 +7,6 @@ on:
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- '**.csv'
|
||||
push:
|
||||
branches: [ develop ]
|
||||
paths-ignore:
|
||||
|
22
CODEOWNERS
22
CODEOWNERS
@ -3,13 +3,13 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# 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/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
erpnext/loan_management/ @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @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/subcontracting @rohitwaghchaure @s-aga-r
|
||||
|
||||
erpnext/crm/ @NagariaHussain
|
||||
erpnext/education/ @rutwikhdev
|
||||
erpnext/projects/ @ruchamahabal
|
||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
|
||||
|
||||
.github/ @ankush
|
||||
.github/ @deepeshgarg007
|
||||
pyproject.toml @ankush
|
||||
|
@ -18,7 +18,6 @@
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_currency",
|
||||
"inter_company_account",
|
||||
"column_break1",
|
||||
"parent_account",
|
||||
"account_type",
|
||||
@ -34,15 +33,11 @@
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Section Break",
|
||||
"oldfieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break0",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@ -53,9 +48,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "account_name",
|
||||
"oldfieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account_number",
|
||||
@ -63,17 +56,13 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Number",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_group",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Group",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Is Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
@ -85,9 +74,7 @@
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "root_type",
|
||||
@ -95,9 +82,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Root Type",
|
||||
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "report_type",
|
||||
@ -105,32 +90,18 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Report Type",
|
||||
"options": "\nBalance Sheet\nProfit and Loss",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_group==0",
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "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
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@ -142,9 +113,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Setting Account Type helps in selecting this Account in transactions.",
|
||||
@ -154,9 +123,7 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"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",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"description": "Rate at which this tax is applied",
|
||||
@ -164,9 +131,7 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "tax_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
{
|
||||
"description": "If the account is frozen, entries are allowed to restricted users.",
|
||||
@ -175,17 +140,13 @@
|
||||
"label": "Frozen",
|
||||
"oldfieldname": "freeze_account",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "No\nYes",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"fieldname": "balance_must_be",
|
||||
"fieldtype": "Select",
|
||||
"label": "Balance must be",
|
||||
"options": "\nDebit\nCredit",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "\nDebit\nCredit"
|
||||
},
|
||||
{
|
||||
"fieldname": "lft",
|
||||
@ -194,9 +155,7 @@
|
||||
"label": "Lft",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rgt",
|
||||
@ -205,9 +164,7 @@
|
||||
"label": "Rgt",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "old_parent",
|
||||
@ -215,33 +172,27 @@
|
||||
"hidden": 1,
|
||||
"label": "Old Parent",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)",
|
||||
"fieldname": "include_in_gross",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include in gross",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Include in gross"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Disable"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-11 15:15:54.338622",
|
||||
"modified": "2023-04-11 16:08:46.983677",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@ -301,5 +252,6 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -394,7 +394,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
if ancestors and not allow_independent_account_creation:
|
||||
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
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
|
@ -29,6 +29,7 @@ def create_charts(
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
@ -95,7 +96,17 @@ def identify_is_group(child):
|
||||
is_group = child.get("is_group")
|
||||
elif len(
|
||||
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
|
||||
else:
|
||||
@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
|
||||
"root_type",
|
||||
"tax_rate",
|
||||
"account_number",
|
||||
"account_currency",
|
||||
],
|
||||
order_by="lft, rgt",
|
||||
)
|
||||
@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
continue
|
||||
|
||||
|
@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
@ -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": []
|
||||
}
|
@ -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
|
@ -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
|
@ -31,6 +31,7 @@
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"book_tax_discount_loss",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"column_break_12",
|
||||
@ -360,6 +361,13 @@
|
||||
"fieldname": "show_balance_in_coa",
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
@ -367,7 +375,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-02 12:07:42.434214",
|
||||
"modified": "2023-03-28 09:50:20.375233",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
@ -81,7 +81,7 @@ class BankClearance(Document):
|
||||
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
|
||||
loan_disbursements = (
|
||||
query = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
||||
@ -90,17 +90,22 @@ class BankClearance(Document):
|
||||
ConstantColumn(0).as_("debit"),
|
||||
loan_disbursement.reference_number.as_("cheque_number"),
|
||||
loan_disbursement.reference_date.as_("cheque_date"),
|
||||
loan_disbursement.clearance_date.as_("clearance_date"),
|
||||
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||
loan_disbursement.applicant.as_("against_account"),
|
||||
)
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_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]))
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.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")
|
||||
|
||||
@ -113,16 +118,19 @@ class BankClearance(Document):
|
||||
ConstantColumn(0).as_("credit"),
|
||||
loan_repayment.reference_number.as_("cheque_number"),
|
||||
loan_repayment.reference_date.as_("cheque_date"),
|
||||
loan_repayment.clearance_date.as_("clearance_date"),
|
||||
loan_repayment.applicant.as_("against_account"),
|
||||
loan_repayment.posting_date,
|
||||
)
|
||||
.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.to_date)
|
||||
.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"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
|
||||
|
@ -18,6 +18,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
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');
|
||||
},
|
||||
|
||||
@ -32,6 +36,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.disable_save();
|
||||
frappe.require("bank-reconciliation-tool.bundle.js", () =>
|
||||
frm.trigger("make_reconciliation_tool")
|
||||
);
|
||||
@ -72,10 +77,12 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
after_save: function (frm) {
|
||||
frm.trigger("make_reconciliation_tool");
|
||||
frm.add_custom_button(__('Get Unreconciled Entries'), function() {
|
||||
frm.trigger("make_reconciliation_tool");
|
||||
});
|
||||
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
|
||||
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
@ -89,7 +96,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
r.account,
|
||||
"account_currency",
|
||||
(r) => {
|
||||
frm.currency = r.account_currency;
|
||||
frm.doc.account_currency = r.account_currency;
|
||||
frm.trigger("render_chart");
|
||||
}
|
||||
);
|
||||
@ -162,9 +169,9 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
"reconciliation_tool_cards"
|
||||
).$wrapper,
|
||||
bank_statement_closing_balance:
|
||||
frm.doc.bank_statement_closing_balance,
|
||||
frm.doc.bank_statement_closing_balance,
|
||||
cleared_balance: frm.cleared_balance,
|
||||
currency: frm.currency,
|
||||
currency: frm.doc.account_currency,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
@ -14,6 +14,7 @@
|
||||
"to_reference_date",
|
||||
"filter_by_reference_date",
|
||||
"column_break_2",
|
||||
"account_currency",
|
||||
"account_opening_balance",
|
||||
"bank_statement_closing_balance",
|
||||
"section_break_1",
|
||||
@ -59,7 +60,7 @@
|
||||
"fieldname": "account_opening_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Account Opening Balance",
|
||||
"options": "Currency",
|
||||
"options": "account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -67,7 +68,7 @@
|
||||
"fieldname": "bank_statement_closing_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Closing Balance",
|
||||
"options": "Currency"
|
||||
"options": "account_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_1",
|
||||
@ -104,13 +105,20 @@
|
||||
"fieldname": "filter_by_reference_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Filter by Reference Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Account Currency",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-13 13:00:02.022919",
|
||||
"modified": "2023-03-07 11:02:24.535714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Reconciliation Tool",
|
||||
|
@ -46,7 +46,7 @@ class BankTransaction(StatusUpdater):
|
||||
def add_payment_entries(self, vouchers):
|
||||
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||
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
|
||||
for voucher in vouchers:
|
||||
@ -114,9 +114,7 @@ class BankTransaction(StatusUpdater):
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
frappe.throw(
|
||||
frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
|
||||
)
|
||||
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
self.reload()
|
||||
|
||||
@ -178,7 +176,9 @@ def get_clearance_details(transaction, payment_entry):
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
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
|
||||
|
@ -36,7 +36,7 @@ def validate_columns(data):
|
||||
|
||||
no_of_columns = max([len(d) for d in data])
|
||||
|
||||
if no_of_columns > 7:
|
||||
if no_of_columns > 8:
|
||||
frappe.throw(
|
||||
_("More columns found than expected. Please compare the uploaded file with standard template"),
|
||||
title=(_("Wrong Template")),
|
||||
@ -233,6 +233,7 @@ def build_forest(data):
|
||||
is_group,
|
||||
account_type,
|
||||
root_type,
|
||||
account_currency,
|
||||
) = i
|
||||
|
||||
if not account_name:
|
||||
@ -253,6 +254,8 @@ def build_forest(data):
|
||||
charts_map[account_name]["account_type"] = account_type
|
||||
if 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]
|
||||
paths.append(path) # List of path is created
|
||||
line_no += 1
|
||||
@ -315,20 +318,21 @@ def get_template(template_type):
|
||||
"Is Group",
|
||||
"Account Type",
|
||||
"Root Type",
|
||||
"Account Currency",
|
||||
]
|
||||
writer = UnicodeWriter()
|
||||
writer.writerow(fields)
|
||||
|
||||
if template_type == "Blank Template":
|
||||
for root_type in get_root_types():
|
||||
writer.writerow(["", "", "", 1, "", root_type])
|
||||
writer.writerow(["", "", "", "", 1, "", root_type])
|
||||
|
||||
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():
|
||||
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:
|
||||
writer = get_sample_template(writer)
|
||||
|
@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
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
|
||||
|
||||
new_balance_in_account_currency = 0 # this will be '0'
|
||||
@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
journal_entry_accounts = []
|
||||
for d in accounts:
|
||||
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
|
||||
continue
|
||||
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
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,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document):
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
journal_entry.set("accounts", journal_entry_accounts)
|
||||
journal_entry.set_amounts_in_company_currency()
|
||||
journal_entry.set_total_debit_credit()
|
||||
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.account == account)
|
||||
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:
|
||||
conditions.append(gl.party_type == party_type)
|
||||
if party:
|
||||
|
@ -137,7 +137,8 @@
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
"options": "Finance Book",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "2_add_edit_gl_entries",
|
||||
@ -538,7 +539,7 @@
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-17 12:53:53.280620",
|
||||
"modified": "2023-03-01 14:58:59.286591",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
@ -51,7 +51,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_multi_currency()
|
||||
self.set_amounts_in_company_currency()
|
||||
self.validate_debit_credit_amount()
|
||||
|
||||
self.set_total_debit_credit()
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
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))
|
||||
|
||||
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 self.difference:
|
||||
frappe.throw(
|
||||
|
@ -287,10 +287,6 @@ class TestJournalEntry(unittest.TestCase):
|
||||
jv.submit()
|
||||
|
||||
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(
|
||||
"Sales Expenses - _TC",
|
||||
"Buildings - _TC",
|
||||
|
@ -217,7 +217,6 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.toggle_display("set_exchange_gain_loss",
|
||||
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
|
||||
|
||||
frm.refresh_fields();
|
||||
},
|
||||
|
||||
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"],
|
||||
party_account_currency, "references");
|
||||
|
||||
frm.set_currency_labels(["amount"], company_currency, "deductions");
|
||||
|
||||
cur_frm.set_df_property("source_exchange_rate", "description",
|
||||
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
|
||||
|
||||
|
@ -416,7 +416,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
for ref in self.get("references"):
|
||||
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[key] += ref.allocated_amount
|
||||
|
||||
@ -424,20 +424,37 @@ class PaymentEntry(AccountsController):
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule",
|
||||
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:
|
||||
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[invoice_key]["outstanding"] = term.outstanding
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||
term.discount / 100
|
||||
)
|
||||
if not (term.discount_type and term.discount):
|
||||
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):
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
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"))
|
||||
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]),
|
||||
)
|
||||
|
||||
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):
|
||||
if self.docstatus == 2:
|
||||
self.status = "Cancelled"
|
||||
@ -1642,7 +1686,14 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
|
||||
@frappe.whitelist()
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
paid_amount, received_amount, discount_amount = apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc
|
||||
reference_date = getdate(reference_date)
|
||||
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")
|
||||
@ -1678,6 +1730,7 @@ def get_payment_entry(
|
||||
pe.company = doc.company
|
||||
pe.cost_center = doc.get("cost_center")
|
||||
pe.posting_date = nowdate()
|
||||
pe.reference_date = reference_date
|
||||
pe.mode_of_payment = doc.get("mode_of_payment")
|
||||
pe.party_type = 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(
|
||||
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)
|
||||
else:
|
||||
@ -1769,16 +1822,17 @@ def get_payment_entry(
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=reference_doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
"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": discount_amount * (-1 if payment_type == "Pay" else 1),
|
||||
}
|
||||
base_total_discount_loss = 0
|
||||
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||
base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
|
||||
|
||||
set_pending_discount_loss(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
pe.set_difference_amount()
|
||||
|
||||
pe.set_difference_amount()
|
||||
|
||||
return pe
|
||||
|
||||
@ -1889,20 +1943,28 @@ def set_paid_amount_and_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
|
||||
valid_discounts = []
|
||||
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||
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:
|
||||
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":
|
||||
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:
|
||||
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":
|
||||
paid_amount -= discount_amount
|
||||
@ -1911,23 +1973,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
received_amount -= discount_amount
|
||||
paid_amount -= discount_amount_in_foreign_currency
|
||||
|
||||
valid_discounts.append({"type": term.discount_type, "discount": term.discount})
|
||||
total_discount += discount_amount
|
||||
|
||||
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)
|
||||
|
||||
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(
|
||||
payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
||||
payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||
):
|
||||
references = []
|
||||
is_multi_currency_acc = (doc.currency != doc.company_currency) and (
|
||||
party_account_currency != doc.company_currency
|
||||
)
|
||||
|
||||
for payment_term in payment_schedule:
|
||||
payment_term_outstanding = flt(
|
||||
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:
|
||||
references.append(
|
||||
|
@ -5,7 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
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 erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
@ -256,10 +256,25 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
|
||||
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")
|
||||
|
||||
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()
|
||||
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].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):
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD",
|
||||
@ -839,24 +1038,27 @@ def create_payment_terms_template():
|
||||
).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", "Test Discount Template"):
|
||||
payment_term_template = frappe.get_doc(
|
||||
if not frappe.db.exists("Payment Terms Template", template_name):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "Test Discount Template",
|
||||
"template_name": template_name,
|
||||
"allocate_payment_based_on_payment_terms": 1,
|
||||
"terms": [
|
||||
{
|
||||
"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,
|
||||
"credit_days_based_on": "Day(s) after invoice date",
|
||||
"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": 1,
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
"creation": "2016-06-15 15:56:30.815503",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account",
|
||||
"cost_center",
|
||||
@ -17,9 +18,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
@ -28,37 +27,30 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-12 20:38:08.110674",
|
||||
"modified": "2023-03-06 07:11:57.739619",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
@ -66,5 +58,6 @@
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -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}));
|
||||
|
@ -221,15 +221,27 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||
difference_amount = 0
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
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
|
||||
if frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
) != frappe.get_cached_value("Company", self.company, "default_currency"):
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
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
|
||||
|
||||
@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()
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
|
@ -5,7 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
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 erpnext import get_default_cost_center
|
||||
@ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("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()
|
||||
|
||||
si.reload()
|
||||
@ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("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()
|
||||
|
||||
# check PR tool output
|
||||
@ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("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()
|
||||
|
||||
# assert outstanding
|
||||
@ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("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()
|
||||
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
@ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
|
||||
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):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
@ -495,26 +495,28 @@ def get_amount(ref_doc, payment_account=None):
|
||||
"""get amount based on doctype"""
|
||||
dt = ref_doc.doctype
|
||||
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"]:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
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":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
grand_total = pay.amount
|
||||
break
|
||||
|
||||
elif dt == "Fees":
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return grand_total
|
||||
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
@ -6,6 +6,7 @@ import unittest
|
||||
import frappe
|
||||
|
||||
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.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
@ -45,7 +46,10 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
|
||||
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(
|
||||
dt="Sales Order",
|
||||
dn=so_inr.name,
|
||||
@ -71,6 +75,29 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
self.assertEqual(pr.reference_name, si_usd.name)
|
||||
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):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
|
@ -4,12 +4,13 @@
|
||||
|
||||
import frappe
|
||||
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 (
|
||||
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
|
||||
|
||||
|
||||
@ -20,9 +21,17 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
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):
|
||||
self.validate_future_closing_vouchers()
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
gle_count = frappe.db.count(
|
||||
@ -42,6 +51,25 @@ class PeriodClosingVoucher(AccountsController):
|
||||
else:
|
||||
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):
|
||||
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))
|
||||
|
||||
def validate_posting_date(self):
|
||||
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
|
||||
|
||||
validate_fiscal_year(
|
||||
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
|
||||
)[1]
|
||||
|
||||
self.check_if_previous_year_closed()
|
||||
|
||||
pce = frappe.db.sql(
|
||||
"""select name from `tabPeriod Closing Voucher`
|
||||
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()
|
||||
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
||||
if gl_entries:
|
||||
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(
|
||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
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):
|
||||
gl_entries = []
|
||||
|
||||
# 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):
|
||||
gl_entries.append(self.get_gle_for_pl_account(acc))
|
||||
|
||||
# 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):
|
||||
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):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
@ -120,6 +186,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
if flt(acc.bal_in_account_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,
|
||||
)
|
||||
@ -129,6 +196,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
def get_gle_for_closing_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
@ -141,12 +210,36 @@ class PeriodClosingVoucher(AccountsController):
|
||||
if flt(acc.bal_in_account_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,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
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):
|
||||
if not self.accounting_dimensions:
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
@ -154,47 +247,88 @@ class PeriodClosingVoucher(AccountsController):
|
||||
for dimension in self.accounting_dimensions:
|
||||
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"""
|
||||
|
||||
dimension_fields = ["t1.cost_center", "t1.finance_book"]
|
||||
qb_dimension_fields = ["cost_center", "finance_book", "project"]
|
||||
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
for dimension in self.accounting_dimensions:
|
||||
dimension_fields.append("t1.{0}".format(dimension))
|
||||
qb_dimension_fields.append(dimension)
|
||||
|
||||
if group_by_account:
|
||||
dimension_fields.append("t1.account")
|
||||
qb_dimension_fields.append("account")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
t2.account_currency,
|
||||
{dimension_fields},
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
where
|
||||
t1.is_cancelled = 0
|
||||
and t1.account = t2.name
|
||||
and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2
|
||||
and t2.company = %s
|
||||
and t1.posting_date between %s and %s
|
||||
group by {dimension_fields}
|
||||
""".format(
|
||||
dimension_fields=", ".join(dimension_fields)
|
||||
),
|
||||
(self.company, self.get("year_start_date"), self.posting_date),
|
||||
as_dict=1,
|
||||
account_filters = {
|
||||
"company": self.company,
|
||||
"is_group": 0,
|
||||
}
|
||||
|
||||
if report_type:
|
||||
account_filters.update({"report_type": report_type})
|
||||
|
||||
accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
|
||||
|
||||
if not for_aggregation:
|
||||
query = query.select(
|
||||
(Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
|
||||
"bal_in_account_currency"
|
||||
),
|
||||
(Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
|
||||
)
|
||||
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
|
||||
|
||||
try:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name)
|
||||
frappe.db.set_value(
|
||||
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
|
||||
)
|
||||
|
@ -16,16 +16,17 @@ from erpnext.accounts.utils import get_fiscal_year, now
|
||||
class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
def test_closing_entry(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")
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=400,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
posting_date=now(),
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
@ -33,18 +34,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
jv1.submit()
|
||||
|
||||
jv2 = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=600,
|
||||
account1="Cost of Goods Sold - TPC",
|
||||
account2="Cash - TPC",
|
||||
cost_center=cost_center,
|
||||
posting_date=now(),
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
jv2.save()
|
||||
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
|
||||
|
||||
expected_gle = (
|
||||
@ -65,6 +66,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
def test_cost_center_wise_posting(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()
|
||||
surplus_account = create_account()
|
||||
@ -81,6 +83,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
debit_to="Debtors - TPC",
|
||||
currency="USD",
|
||||
customer="_Test Customer USD",
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
@ -91,9 +94,10 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
debit_to="Debtors - TPC",
|
||||
currency="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.submit()
|
||||
surplus_account = pcv.closing_account_head
|
||||
@ -128,12 +132,13 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
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 `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
si = create_sales_invoice(
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
@ -142,6 +147,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
debit_to="Debtors - TPC",
|
||||
currency="USD",
|
||||
customer="_Test Customer USD",
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
@ -149,14 +155,14 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
account2="Sales - TPC",
|
||||
amount=400,
|
||||
cost_center=cost_center,
|
||||
posting_date=now(),
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
jv.company = company
|
||||
jv.finance_book = create_finance_book().name
|
||||
jv.save()
|
||||
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
|
||||
|
||||
expected_gle = (
|
||||
@ -177,14 +183,130 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
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()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"posting_date": today(),
|
||||
"transaction_date": posting_date or today(),
|
||||
"posting_date": posting_date or today(),
|
||||
"company": "Test PCV Company",
|
||||
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
|
||||
"cost_center": cost_center,
|
||||
|
@ -112,7 +112,8 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
party_type: "Customer",
|
||||
account: this.frm.doc.debit_to,
|
||||
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();
|
||||
});
|
||||
|
@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
bold_item_name = frappe.bold(item.item_name)
|
||||
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)
|
||||
|
||||
@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice):
|
||||
).format(item.idx, bold_invalid_batch_no, bold_item_name),
|
||||
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(
|
||||
_(
|
||||
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
|
||||
@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
|
||||
),
|
||||
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(
|
||||
_(
|
||||
"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)
|
||||
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(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||
<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;">
|
||||
{{ _("Date: ") }}
|
||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||
|
@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
|
||||
class ProcessStatementOfAccounts(Document):
|
||||
def validate(self):
|
||||
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:
|
||||
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,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"currency": doc.currency,
|
||||
@ -156,7 +157,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
|
||||
]
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
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"):
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[["name", "in", list(sales_person_records["Customer"])]],
|
||||
)
|
||||
else:
|
||||
@ -228,7 +229,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
if customer_collection == "Sales Partner":
|
||||
customers = frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[["default_sales_partner", "=", collection_name]],
|
||||
)
|
||||
else:
|
||||
@ -245,7 +246,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
continue
|
||||
|
||||
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
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_workflow": 1,
|
||||
"creation": "2020-08-03 16:35:21.852178",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer",
|
||||
"customer_name",
|
||||
"billing_email",
|
||||
"primary_email"
|
||||
],
|
||||
@ -30,11 +30,18 @@
|
||||
"fieldtype": "Read Only",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Email"
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 22:55:38.875601",
|
||||
"modified": "2023-03-13 00:12:34.508086",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts Customer",
|
||||
@ -43,5 +50,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
||||
&& !(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'));
|
||||
}
|
||||
|
||||
|
@ -118,6 +118,7 @@
|
||||
"paid_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"only_include_allocated_payments",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"advance_tax",
|
||||
@ -1550,17 +1551,24 @@
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"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",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-28 19:18:56.586321",
|
||||
"modified": "2023-04-03 22:57:14.074982",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
@ -117,7 +117,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.validate_expense_account()
|
||||
self.set_against_expense_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.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
@ -232,7 +232,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
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_internal_supplier
|
||||
):
|
||||
@ -581,6 +581,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.make_supplier_gl_entry(gl_entries)
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
|
||||
if self.check_asset_cwip_enabled():
|
||||
self.get_asset_gl_entry(gl_entries)
|
||||
@ -975,6 +976,28 @@ class PurchaseInvoice(BuyingController):
|
||||
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):
|
||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
@ -1485,11 +1508,17 @@ class PurchaseInvoice(BuyingController):
|
||||
if po_details:
|
||||
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):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
|
||||
|
||||
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):
|
||||
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
|
||||
|
@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
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):
|
||||
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
|
@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
||||
&& !(cint(doc.is_return) && doc.return_against)) {
|
||||
cur_frm.add_custom_button(__('Payment'),
|
||||
this.make_payment_entry, __('Create'));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
__('Create')
|
||||
);
|
||||
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
if(doc.docstatus==1 && !doc.is_return) {
|
||||
|
@ -32,9 +32,6 @@
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"column_break_27",
|
||||
"campaign",
|
||||
"source",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@ -123,6 +120,7 @@
|
||||
"account_for_change_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"only_include_allocated_payments",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"write_off_section",
|
||||
@ -203,7 +201,9 @@
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"campaign",
|
||||
"represents_company",
|
||||
"source",
|
||||
"customer_group",
|
||||
"col_break23",
|
||||
"is_internal_customer",
|
||||
@ -2083,10 +2083,6 @@
|
||||
"fieldname": "company_addr_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_27",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_52",
|
||||
"fieldtype": "Column Break"
|
||||
@ -2131,6 +2127,14 @@
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"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",
|
||||
@ -2143,11 +2147,10 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-01-28 19:45:47.538163",
|
||||
"modified": "2023-04-03 22:55:14.206473",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
@ -145,7 +145,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.set_against_income_account()
|
||||
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:
|
||||
self.validate_serial_numbers()
|
||||
else:
|
||||
|
@ -266,16 +266,16 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"_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 CST - _TC": [32.5, 1652, 0.65, 33.04],
|
||||
"_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17],
|
||||
"_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55],
|
||||
"_Test Account VAT - _TC": [156.0, 1808.0, 3.12, 36.16],
|
||||
"_Test Account Discount - _TC": [-181.0, 1627.0, -3.62, 32.54],
|
||||
}
|
||||
|
||||
for d in si.get("taxes"):
|
||||
for i, k in enumerate(expected_values["keys"]):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.base_grand_total, 1627.5)
|
||||
self.assertEqual(si.grand_total, 32.55)
|
||||
self.assertEqual(si.base_grand_total, 1627.0)
|
||||
self.assertEqual(si.grand_total, 32.54)
|
||||
|
||||
def test_sales_invoice_with_discount_and_inclusive_tax(self):
|
||||
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 CST - _TC": [27.88, 25.95, 1323.62],
|
||||
"_Test Account VAT - _TC": [156.25, 145.43, 1469.05],
|
||||
"_Test Account Customs Duty - _TC": [125, 116.35, 1585.40],
|
||||
"_Test Account Shipping Charges - _TC": [100, 100, 1685.40],
|
||||
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.86],
|
||||
"_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01],
|
||||
"_Test Account Customs Duty - _TC": [125, 116.34, 1585.39],
|
||||
"_Test Account Shipping Charges - _TC": [100, 100, 1685.39],
|
||||
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.85],
|
||||
"_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.00],
|
||||
}
|
||||
|
||||
for d in si.get("taxes"):
|
||||
@ -413,7 +413,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
self.assertEqual(si.base_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):
|
||||
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"][3]["account_head"], 0.0, 25.95],
|
||||
[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"][7]["account_head"], 168.54, 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 S&H Education Cess - _TC": [0.7, 0.7, 0.7],
|
||||
"_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],
|
||||
}
|
||||
|
||||
@ -1623,9 +1623,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
if expected_values.get(d.account_head):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.total_taxes_and_charges, 234.43)
|
||||
self.assertEqual(si.base_grand_total, 859.43)
|
||||
self.assertEqual(si.grand_total, 859.43)
|
||||
self.assertEqual(si.total_taxes_and_charges, 234.42)
|
||||
self.assertEqual(si.base_grand_total, 859.42)
|
||||
self.assertEqual(si.grand_total, 859.42)
|
||||
|
||||
def test_multi_currency_gle(self):
|
||||
si = create_sales_invoice(
|
||||
@ -1985,17 +1985,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
si.save()
|
||||
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.total_taxes_and_charges, 5446.88)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.rounding_adjustment, 0.0)
|
||||
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
[si.debit_to, 24900, 0.0],
|
||||
["_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],
|
||||
]
|
||||
)
|
||||
|
@ -300,6 +300,9 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
|
||||
if gl_map:
|
||||
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:
|
||||
make_entry(entry, adv_adj, update_outstanding, from_repost)
|
||||
@ -519,6 +522,9 @@ def make_reverse_gl_entries(
|
||||
)
|
||||
validate_accounting_period(gl_entries)
|
||||
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"])
|
||||
|
||||
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):
|
||||
"""
|
||||
Set is_cancelled=1 in all original gl entries for the voucher
|
||||
|
@ -32,6 +32,16 @@ from erpnext import get_company_currency
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
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):
|
||||
pass
|
||||
@ -124,12 +134,6 @@ def _get_party_details(
|
||||
set_other_values(party_details, party, party_type)
|
||||
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(
|
||||
party.name,
|
||||
party_type,
|
||||
@ -170,6 +174,9 @@ def _get_party_details(
|
||||
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
|
||||
|
||||
|
||||
@ -211,20 +218,10 @@ def set_address_details(
|
||||
else:
|
||||
party_details.update(get_company_address(company))
|
||||
|
||||
if doctype and doctype in [
|
||||
"Delivery Note",
|
||||
"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)
|
||||
if doctype in SALES_TRANSACTION_TYPES and party_details.company_address:
|
||||
party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address))
|
||||
|
||||
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
|
||||
if doctype in PURCHASE_TRANSACTION_TYPES:
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
shipping_address=shipping_address,
|
||||
@ -250,9 +247,21 @@ def set_address_details(
|
||||
**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)
|
||||
|
||||
return party_details.get(billing_address_field), party_details.shipping_address_name
|
||||
return party_address, shipping_address
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
|
@ -859,7 +859,7 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
else:
|
||||
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):
|
||||
|
@ -25,6 +25,7 @@ def get_data(filters):
|
||||
["posting_date", "<=", filters.get("to_date")],
|
||||
["against_voucher_type", "=", "Asset"],
|
||||
["account", "in", depreciation_accounts],
|
||||
["is_cancelled", "=", 0],
|
||||
]
|
||||
|
||||
if filters.get("asset"):
|
||||
|
@ -25,6 +25,8 @@ def execute(filters=None):
|
||||
company=filters.company,
|
||||
)
|
||||
|
||||
filters.period_start_date = period_list[0]["year_start_date"]
|
||||
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_currency"
|
||||
)
|
||||
@ -96,7 +98,7 @@ def execute(filters=None):
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity)
|
||||
|
||||
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
|
||||
@ -174,7 +176,6 @@ def get_report_summary(
|
||||
liability,
|
||||
equity,
|
||||
provisional_profit_loss,
|
||||
total_credit,
|
||||
currency,
|
||||
filters,
|
||||
consolidated=False,
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import getdate, nowdate
|
||||
|
||||
|
||||
@ -91,4 +92,65 @@ def get_entries(filters):
|
||||
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()),
|
||||
)
|
||||
|
@ -118,7 +118,6 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
liability,
|
||||
equity,
|
||||
provisional_profit_loss,
|
||||
total_credit,
|
||||
company_currency,
|
||||
filters,
|
||||
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]:
|
||||
if data:
|
||||
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
|
||||
|
||||
@ -155,7 +155,7 @@ def get_opening_balance(account_name, data, company):
|
||||
|
||||
|
||||
def get_root_account_name(root_type, company):
|
||||
return frappe.get_all(
|
||||
root_account = frappe.get_all(
|
||||
"Account",
|
||||
fields=["account_name"],
|
||||
filters={
|
||||
@ -165,7 +165,10 @@ def get_root_account_name(root_type, company):
|
||||
"parent_account": ("is", "not set"),
|
||||
},
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
if root_account:
|
||||
return root_account[0][0]
|
||||
|
||||
|
||||
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
|
@ -418,46 +418,47 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries=False,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
gl_entries = []
|
||||
|
||||
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
|
||||
|
||||
accounts = frappe.db.sql_list(
|
||||
"""select name from `tabAccount`
|
||||
where lft >= %s and rgt <= %s and company = %s""",
|
||||
(root_lft, root_rgt, company),
|
||||
accounts_list = frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if accounts:
|
||||
additional_conditions += " and account in ({})".format(
|
||||
", ".join(frappe.db.escape(d) for d in accounts)
|
||||
)
|
||||
ignore_opening_entries = False
|
||||
if accounts_list:
|
||||
# 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 = {
|
||||
"company": company,
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"finance_book": cstr(filters.get("finance_book")),
|
||||
}
|
||||
|
||||
if filters.get("include_default_book_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,
|
||||
gl_entries += get_accounting_entries(
|
||||
"GL Entry",
|
||||
from_date,
|
||||
to_date,
|
||||
accounts_list,
|
||||
filters,
|
||||
ignore_closing_entries,
|
||||
ignore_opening_entries=ignore_opening_entries,
|
||||
)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
@ -469,34 +470,82 @@ def set_gl_entries_by_account(
|
||||
return gl_entries_by_account
|
||||
|
||||
|
||||
def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
||||
additional_conditions = []
|
||||
def get_accounting_entries(
|
||||
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)
|
||||
|
||||
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:
|
||||
additional_conditions.append("posting_date >= %(from_date)s")
|
||||
if from_date and doctype == "GL Entry":
|
||||
query = query.where(gl_entry.posting_date >= from_date)
|
||||
|
||||
if filters:
|
||||
if filters.get("project"):
|
||||
if not isinstance(filters.get("project"), list):
|
||||
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"):
|
||||
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"):
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
query = query.where(
|
||||
(gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
|
||||
| (gl_entry.finance_book.isnull())
|
||||
)
|
||||
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:
|
||||
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(
|
||||
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):
|
||||
|
@ -38,8 +38,11 @@
|
||||
{% if(data[i].posting_date) { %}
|
||||
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
|
||||
<td>{%= data[i].voucher_type %}
|
||||
<br>{%= data[i].voucher_no %}</td>
|
||||
<td>
|
||||
<br>{%= data[i].voucher_no %}
|
||||
</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)) { %}
|
||||
{%= data[i].party || data[i].account %}
|
||||
<br>
|
||||
@ -49,11 +52,14 @@
|
||||
{% if(data[i].bill_no) { %}
|
||||
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
{% } %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td>
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}
|
||||
</td>
|
||||
{% } else { %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Party Type",
|
||||
"default": "",
|
||||
"fieldtype": "Autocomplete",
|
||||
options: Object.keys(frappe.boot.party_account_types),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"root_type": "Asset",
|
||||
"report_type": "Balance Sheet",
|
||||
"account_currency": "USD",
|
||||
"inter_company_account": 0,
|
||||
"parent_account": "Bank Accounts - _TC",
|
||||
"account_type": "Bank",
|
||||
"doctype": "Account",
|
||||
|
@ -501,7 +501,14 @@ class GrossProfitGenerator(object):
|
||||
):
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
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.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
if flt(row.qty) or row.base_amount:
|
||||
@ -734,6 +741,8 @@ class GrossProfitGenerator(object):
|
||||
if self.filters.to_date:
|
||||
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:
|
||||
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
|
||||
|
||||
|
@ -381,3 +381,82 @@ class TestGrossProfit(FrappeTestCase):
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
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])
|
||||
|
@ -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",
|
||||
"label": __("Voucher No"),
|
||||
@ -49,6 +72,20 @@ function get_filters() {
|
||||
"fieldtype": "Data",
|
||||
"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;
|
||||
|
@ -17,34 +17,26 @@ class PaymentLedger(object):
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
def init_voucher_dict(self):
|
||||
|
||||
if self.voucher_amount:
|
||||
s = set()
|
||||
# build a set of unique vouchers
|
||||
# for each ple, using group_by_key to create a key and assign it to +/- list
|
||||
for ple in self.voucher_amount:
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
s.add(key)
|
||||
group_by_key = None
|
||||
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
|
||||
if self.voucher_dict.get(against_key):
|
||||
if ple.amount > 0:
|
||||
target = self.voucher_dict.get(against_key).increase
|
||||
else:
|
||||
target = self.voucher_dict.get(against_key).decrease
|
||||
if ple.amount > 0:
|
||||
target = self.voucher_dict.setdefault(group_by_key, {}).setdefault("increase", [])
|
||||
else:
|
||||
target = self.voucher_dict.setdefault(group_by_key, {}).setdefault("decrease", [])
|
||||
|
||||
# this if condition will lose unassigned ple entries(against_voucher doc doesn't have ple)
|
||||
# need to somehow include the stray entries as well.
|
||||
if target is not None:
|
||||
entry = frappe._dict(
|
||||
company=ple.company,
|
||||
posting_date=ple.posting_date,
|
||||
account=ple.account,
|
||||
party_type=ple.party_type,
|
||||
party=ple.party,
|
||||
@ -66,10 +58,10 @@ class PaymentLedger(object):
|
||||
|
||||
for value in self.voucher_dict.values():
|
||||
voucher_data = []
|
||||
if value.increase != []:
|
||||
voucher_data.extend(value.increase)
|
||||
if value.decrease != []:
|
||||
voucher_data.extend(value.decrease)
|
||||
if value.get("increase"):
|
||||
voucher_data.extend(value.get("increase"))
|
||||
if value.get("decrease"):
|
||||
voucher_data.extend(value.get("decrease"))
|
||||
|
||||
if voucher_data:
|
||||
# balance row
|
||||
@ -117,6 +109,12 @@ class PaymentLedger(object):
|
||||
if 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):
|
||||
ple = self.ple
|
||||
|
||||
@ -134,7 +132,13 @@ class PaymentLedger(object):
|
||||
def get_columns(self):
|
||||
options = None
|
||||
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(
|
||||
@ -160,7 +164,11 @@ class PaymentLedger(object):
|
||||
)
|
||||
self.columns.append(
|
||||
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(
|
||||
@ -176,8 +184,8 @@ class PaymentLedger(object):
|
||||
dict(
|
||||
label=_("Against Voucher No"),
|
||||
fieldname="against_voucher_no",
|
||||
fieldtype="data",
|
||||
options=options,
|
||||
fieldtype="Dynamic Link",
|
||||
options="against_voucher_type",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
@ -209,7 +217,7 @@ class PaymentLedger(object):
|
||||
self.get_columns()
|
||||
self.get_data()
|
||||
|
||||
# initialize dictionary and group using against voucher
|
||||
# initialize dictionary and group using key
|
||||
self.init_voucher_dict()
|
||||
|
||||
# convert dictionary to list and add balance rows
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
@ -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
|
||||
}
|
||||
]
|
@ -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 "))
|
@ -4,7 +4,8 @@
|
||||
|
||||
import frappe
|
||||
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
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@ -78,7 +79,6 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
|
||||
accounts = frappe.db.sql(
|
||||
"""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),
|
||||
)
|
||||
|
||||
total_row = calculate_values(
|
||||
accounts, gl_entries_by_account, opening_balances, filters, company_currency
|
||||
)
|
||||
calculate_values(accounts, gl_entries_by_account, opening_balances)
|
||||
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, 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):
|
||||
additional_conditions = ""
|
||||
if not filters.show_unclosed_fy_pl_balances:
|
||||
additional_conditions = (
|
||||
" and posting_date >= %(year_start_date)s" if report_type == "Profit and Loss" else ""
|
||||
)
|
||||
gle = []
|
||||
|
||||
if not flt(filters.with_period_closing_entry):
|
||||
additional_conditions += " and ifnull(voucher_type, '')!='Period Closing Voucher'"
|
||||
|
||||
if filters.cost_center:
|
||||
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
|
||||
additional_conditions += """ and cost_center in (select name from `tabCost Center`
|
||||
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)"
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
|
||||
fields=["posting_date", "name"],
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
query_filters = {
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"report_type": report_type,
|
||||
"year_start_date": filters.year_start_date,
|
||||
"project": filters.project,
|
||||
"finance_book": filters.finance_book,
|
||||
"company_fb": frappe.get_cached_value("Company", filters.company, "default_finance_book"),
|
||||
}
|
||||
if last_period_closing_voucher:
|
||||
gle = get_opening_balance(
|
||||
"Account Closing Balance",
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=last_period_closing_voucher[0].name,
|
||||
)
|
||||
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:
|
||||
for dimension in accounting_dimensions:
|
||||
@ -187,38 +265,20 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
filters[dimension.fieldname] = get_dimension_with_children(
|
||||
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:
|
||||
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(
|
||||
"""
|
||||
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
|
||||
return gle
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency):
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances):
|
||||
init = {
|
||||
"opening_debit": 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,
|
||||
}
|
||||
|
||||
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:
|
||||
d.update(init.copy())
|
||||
|
||||
@ -261,8 +305,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
||||
|
||||
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
|
||||
|
||||
@ -274,7 +338,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
|
||||
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 = []
|
||||
|
||||
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
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
data.extend([{}, total_row])
|
||||
|
||||
return data
|
||||
|
@ -30,10 +30,6 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
||||
("Sales Register", {}),
|
||||
("Sales Register", {"item_group": "All Item Groups"}),
|
||||
("Purchase Register", {}),
|
||||
(
|
||||
"Tax Detail",
|
||||
{"mode": "run", "report_name": "Tax Detail"},
|
||||
),
|
||||
]
|
||||
|
||||
OPTIONAL_FILTERS = {}
|
||||
|
@ -51,13 +51,25 @@ GL_REPOSTING_CHUNK = 100
|
||||
|
||||
@frappe.whitelist()
|
||||
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(
|
||||
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 []
|
||||
|
||||
@ -121,8 +133,12 @@ def get_fiscal_years(
|
||||
if company:
|
||||
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
|
||||
|
||||
if boolean:
|
||||
return False
|
||||
|
||||
if verbose == 1:
|
||||
frappe.msgprint(error_msg)
|
||||
|
||||
raise FiscalYearError(error_msg)
|
||||
|
||||
|
||||
@ -451,12 +467,6 @@ def reconcile_against_document(args): # nosemgrep
|
||||
else:
|
||||
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)
|
||||
# re-submit advance entry
|
||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||
|
@ -13,6 +13,7 @@
|
||||
"hide_custom": 0,
|
||||
"icon": "accounting",
|
||||
"idx": 0,
|
||||
"is_hidden": 0,
|
||||
"label": "Accounting",
|
||||
"links": [
|
||||
{
|
||||
@ -493,17 +494,6 @@
|
||||
"onboard": 0,
|
||||
"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",
|
||||
"hidden": 0,
|
||||
@ -516,18 +506,6 @@
|
||||
"only_for": "United Arab Emirates",
|
||||
"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,
|
||||
"is_query_report": 0,
|
||||
@ -1029,17 +1007,6 @@
|
||||
"only_for": "India",
|
||||
"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,
|
||||
"is_query_report": 0,
|
||||
@ -1093,7 +1060,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2022-06-24 05:41:09.236458",
|
||||
"modified": "2023-02-23 15:32:12.135355",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
|
@ -466,6 +466,9 @@ frappe.ui.form.on('Asset', {
|
||||
} else {
|
||||
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);
|
||||
if (!item) {
|
||||
doctype_field = frappe.scrub(doctype)
|
||||
|
@ -79,6 +79,9 @@
|
||||
"options": "ACC-ASS-.YYYY.-"
|
||||
},
|
||||
{
|
||||
"depends_on": "item_code",
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@ -517,7 +520,7 @@
|
||||
"table_fieldname": "accounts"
|
||||
}
|
||||
],
|
||||
"modified": "2023-02-02 00:03:11.706427",
|
||||
"modified": "2023-03-30 15:07:41.542374",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
@ -8,16 +8,12 @@ import math
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
get_datetime,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
is_last_day_of_the_month,
|
||||
month_diff,
|
||||
nowdate,
|
||||
today,
|
||||
)
|
||||
@ -239,30 +235,6 @@ class Asset(AccountsController):
|
||||
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):
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
|
||||
frappe.throw(
|
||||
@ -471,25 +443,6 @@ class Asset(AccountsController):
|
||||
|
||||
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):
|
||||
purchase_document = self.get_purchase_document()
|
||||
if not purchase_document:
|
||||
@ -614,23 +567,42 @@ class Asset(AccountsController):
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
|
||||
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("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")
|
||||
|
||||
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))
|
||||
if self.flags.increase_in_asset_value_due_to_repair:
|
||||
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)
|
||||
|
||||
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():
|
||||
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)
|
||||
|
||||
|
||||
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()
|
||||
def split_asset(asset_name, split_qty):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = {
|
||||
}
|
||||
},
|
||||
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();
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
|
||||
|
@ -249,10 +249,16 @@ def notify_depr_entry_posting_error(failed_asset_names):
|
||||
asset_links = get_comma_separated_asset_links(failed_asset_names)
|
||||
|
||||
message = (
|
||||
_("Hi,")
|
||||
+ "<br>"
|
||||
+ _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
|
||||
_("Hello,")
|
||||
+ "<br><br>"
|
||||
+ _("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)
|
||||
|
@ -29,8 +29,11 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
scrap_asset,
|
||||
)
|
||||
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_depr_schedule,
|
||||
get_depreciation_amount,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
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.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
|
||||
)
|
||||
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(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
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
@ -828,8 +831,8 @@ class TestDepreciationMethods(AssetSetup):
|
||||
expected_schedules = [
|
||||
["2030-12-31", 28630.14, 28630.14],
|
||||
["2031-12-31", 35684.93, 64315.07],
|
||||
["2032-12-31", 17842.47, 82157.54],
|
||||
["2033-06-06", 5342.46, 87500.0],
|
||||
["2032-12-31", 17842.46, 82157.53],
|
||||
["2033-06-06", 5342.47, 87500.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
@ -857,12 +860,12 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2022-02-28", 647.25, 647.25],
|
||||
["2022-03-31", 1210.71, 1857.96],
|
||||
["2022-04-30", 1053.99, 2911.95],
|
||||
["2022-05-31", 917.55, 3829.5],
|
||||
["2022-06-30", 798.77, 4628.27],
|
||||
["2022-07-15", 371.73, 5000.0],
|
||||
["2022-02-28", 310.89, 310.89],
|
||||
["2022-03-31", 654.45, 965.34],
|
||||
["2022-04-30", 654.45, 1619.79],
|
||||
["2022-05-31", 654.45, 2274.24],
|
||||
["2022-06-30", 654.45, 2928.69],
|
||||
["2022-07-15", 2071.31, 5000.0],
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def test_expected_value_after_useful_life_greater_than_purchase_amount(self):
|
||||
|
@ -43,9 +43,9 @@ erpnext.asset.set_accumulated_depreciation = function(frm) {
|
||||
if(frm.doc.depreciation_method != "Manual") return;
|
||||
|
||||
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);
|
||||
frappe.model.set_value(row.doctype, row.name,
|
||||
"accumulated_depreciation_amount", accumulated_depreciation);
|
||||
frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
|
||||
})
|
||||
};
|
||||
|
@ -10,7 +10,9 @@
|
||||
"asset",
|
||||
"naming_series",
|
||||
"column_break_2",
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
"finance_book",
|
||||
"finance_book_id",
|
||||
"depreciation_details_section",
|
||||
@ -148,18 +150,36 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "opening_accumulated_depreciation",
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Opening Accumulated Depreciation",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-16 21:08:21.421260",
|
||||
"modified": "2023-02-26 16:37:23.734806",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Depreciation Schedule",
|
||||
|
@ -4,7 +4,19 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
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):
|
||||
@ -83,15 +95,58 @@ class AssetDepreciationSchedule(Document):
|
||||
date_of_return=None,
|
||||
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.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):
|
||||
self.asset = asset_doc.name
|
||||
self.finance_book = row.finance_book
|
||||
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.total_number_of_depreciations = row.total_number_of_depreciations
|
||||
self.frequency_of_depreciation = row.frequency_of_depreciation
|
||||
@ -102,7 +157,7 @@ class AssetDepreciationSchedule(Document):
|
||||
def make_depr_schedule(
|
||||
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 = []
|
||||
|
||||
if not asset_doc.available_for_use_date:
|
||||
@ -141,24 +196,49 @@ class AssetDepreciationSchedule(Document):
|
||||
row.db_update()
|
||||
|
||||
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:
|
||||
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
|
||||
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):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row:
|
||||
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(
|
||||
row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
|
||||
)
|
||||
@ -176,26 +256,36 @@ class AssetDepreciationSchedule(Document):
|
||||
if self.depreciation_schedule:
|
||||
from_date = self.depreciation_schedule[-1].schedule_date
|
||||
|
||||
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
|
||||
row, depreciation_amount, from_date, date_of_disposal
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
date_of_disposal,
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self.add_depr_schedule_row(
|
||||
date_of_disposal,
|
||||
depreciation_amount,
|
||||
row.depreciation_method,
|
||||
)
|
||||
|
||||
break
|
||||
|
||||
# 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(
|
||||
asset_doc.available_for_use_date, -1
|
||||
) # needed to calc depr amount for available_for_use_date too
|
||||
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
|
||||
row, depreciation_amount, from_date, row.depreciation_start_date
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
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
|
||||
@ -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
|
||||
asset_doc.to_date = add_months(
|
||||
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, days, months = asset_doc.get_pro_rata_amt(
|
||||
row, depreciation_amount, schedule_date, asset_doc.to_date
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
depreciation_amount,
|
||||
schedule_date,
|
||||
asset_doc.to_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
|
||||
depreciation_amount = self.get_adjusted_depreciation_amount(
|
||||
@ -247,7 +341,6 @@ class AssetDepreciationSchedule(Document):
|
||||
self.add_depr_schedule_row(
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
row.depreciation_method,
|
||||
)
|
||||
|
||||
# to ensure that final accumulated depreciation amount is accurate
|
||||
@ -274,14 +367,12 @@ class AssetDepreciationSchedule(Document):
|
||||
self,
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
depreciation_method,
|
||||
):
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": depreciation_method,
|
||||
},
|
||||
)
|
||||
|
||||
@ -293,7 +384,9 @@ class AssetDepreciationSchedule(Document):
|
||||
ignore_booked_entry=False,
|
||||
):
|
||||
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)
|
||||
@ -336,6 +429,132 @@ def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
|
||||
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):
|
||||
for row in asset_doc.get("finance_books"):
|
||||
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)
|
||||
|
||||
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.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
|
||||
|
||||
|
@ -84,6 +84,8 @@ def calculate_next_due_date(
|
||||
next_due_date = add_years(start_date, 1)
|
||||
if periodicity == "2 Yearly":
|
||||
next_due_date = add_years(start_date, 2)
|
||||
if periodicity == "3 Yearly":
|
||||
next_due_date = add_years(start_date, 3)
|
||||
if periodicity == "Quarterly":
|
||||
next_due_date = add_months(start_date, 3)
|
||||
if end_date and (
|
||||
|
@ -1,664 +1,156 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "",
|
||||
"beta": 0,
|
||||
"creation": "2017-10-20 07:10:55.903571",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2017-10-20 07:10:55.903571",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"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": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "maintenance_task",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Maintenance Task",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "maintenance_task",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Maintenance Task",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "maintenance_type",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "maintenance_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Maintenance Type",
|
||||
"options": "Preventive Maintenance\nCalibration"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_2",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "maintenance_status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Maintenance Status",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Planned\nOverdue\nCancelled",
|
||||
"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
|
||||
},
|
||||
"fieldname": "maintenance_status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Maintenance Status",
|
||||
"options": "Planned\nOverdue\nCancelled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_2",
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Today",
|
||||
"fieldname": "start_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",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"default": "Today",
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Start Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "periodicity",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Periodicity",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "periodicity",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Periodicity",
|
||||
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_4",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "end_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": "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
|
||||
},
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "End Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "certificate_required",
|
||||
"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",
|
||||
"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,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"default": "0",
|
||||
"fieldname": "certificate_required",
|
||||
"fieldtype": "Check",
|
||||
"label": "Certificate Required",
|
||||
"search_index": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_9",
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "assign_to",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Assign To",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "assign_to",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Assign To",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_10",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "assign_to.full_name",
|
||||
"fieldname": "assign_to_name",
|
||||
"fieldtype": "Read Only",
|
||||
"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": "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
|
||||
},
|
||||
"fieldname": "assign_to_name",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Assign to Name"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_10",
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "next_due_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_standard_filter": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "next_due_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Next Due Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_14",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "last_completion_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_standard_filter": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "last_completion_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Last Completion Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_7",
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"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": "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
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"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,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-06-18 16:12:04.330021",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Maintenance Task",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-23 07:03:07.113452",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Maintenance Task",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
@ -43,53 +43,57 @@ class AssetRepair(AccountsController):
|
||||
def before_submit(self):
|
||||
self.check_repair_status()
|
||||
|
||||
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
||||
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()
|
||||
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
|
||||
|
||||
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()
|
||||
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
||||
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
|
||||
|
||||
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 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):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
||||
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()
|
||||
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
|
||||
|
||||
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()
|
||||
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
||||
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
|
||||
|
||||
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 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):
|
||||
frappe.get_doc("Asset", self.asset).set_status()
|
||||
|
@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
|
||||
frm.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
|
||||
args: {
|
||||
asset: frm.doc.asset,
|
||||
asset_name: frm.doc.asset,
|
||||
finance_book: frm.doc.finance_book
|
||||
},
|
||||
callback: function(r) {
|
||||
|
@ -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_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
get_depreciation_amount,
|
||||
)
|
||||
|
||||
|
||||
@ -162,7 +163,7 @@ class AssetValueAdjustment(Document):
|
||||
depreciation_amount = days * rate_per_day
|
||||
from_date = data.schedule_date
|
||||
else:
|
||||
depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d)
|
||||
depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
|
||||
|
||||
if depreciation_amount:
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
|
@ -12,8 +12,7 @@
|
||||
"column_break_3",
|
||||
"accumulated_depreciation_amount",
|
||||
"journal_entry",
|
||||
"make_depreciation_entry",
|
||||
"depreciation_method"
|
||||
"make_depreciation_entry"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -58,20 +57,11 @@
|
||||
"fieldname": "make_depreciation_entry",
|
||||
"fieldtype": "Button",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2022-12-06 20:35:50.264281",
|
||||
"modified": "2023-03-13 23:17:15.849950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Depreciation Schedule",
|
||||
|
@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
"label": __("Period Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": ["Fiscal Year"],
|
||||
"default": "Fiscal Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
fieldtype: "Link",
|
||||
options: "Asset Category"
|
||||
},
|
||||
{
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book"
|
||||
},
|
||||
{
|
||||
fieldname:"cost_center",
|
||||
label: __("Cost Center"),
|
||||
@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"is_existing_asset",
|
||||
label: __("Is Existing Asset"),
|
||||
fieldname:"finance_book",
|
||||
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"
|
||||
},
|
||||
]
|
||||
|
@ -45,8 +45,10 @@ def get_conditions(filters):
|
||||
filters.year_end_date = getdate(fiscal_year.year_end_date)
|
||||
|
||||
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
|
||||
if filters.get("is_existing_asset"):
|
||||
conditions["is_existing_asset"] = filters.get("is_existing_asset")
|
||||
if filters.get("only_depreciable_assets"):
|
||||
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"):
|
||||
conditions["asset_category"] = filters.get("asset_category")
|
||||
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_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
assets_linked_to_fb = None
|
||||
|
||||
if filters.only_depreciable_assets:
|
||||
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:
|
||||
if filters.finance_book:
|
||||
if asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
else:
|
||||
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
|
||||
row = {
|
||||
@ -172,11 +173,11 @@ def prepare_chart_data(data, filters):
|
||||
"datasets": [
|
||||
{
|
||||
"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"),
|
||||
"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 [
|
||||
{
|
||||
"label": _("Asset Id"),
|
||||
"label": _("Asset ID"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "asset_id",
|
||||
"options": "Asset",
|
||||
|
@ -16,8 +16,10 @@
|
||||
"transaction_settings_section",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"over_order_allowance",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"disable_last_purchase_rate",
|
||||
@ -147,6 +149,21 @@
|
||||
"fieldname": "show_pay_button",
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
@ -154,7 +171,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-15 14:42:10.200679",
|
||||
"modified": "2023-03-02 17:02:14.404622",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
@ -21,3 +21,10 @@ class BuyingSettings(Document):
|
||||
self.get("supp_master_name") == "Naming Series",
|
||||
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
|
||||
|
@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
this.make_purchase_invoice, __('Create'));
|
||||
|
||||
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) {
|
||||
|
@ -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.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
||||
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.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
|
||||
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_for_subcontracting()
|
||||
self.validate_minimum_order_qty()
|
||||
validate_against_blanket_order(self)
|
||||
|
||||
if self.is_old_subcontracting_flow:
|
||||
self.validate_bom_for_subcontracting_items()
|
||||
|
@ -113,7 +113,10 @@ class RequestforQuotation(BuyingController):
|
||||
|
||||
def get_link(self):
|
||||
# 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):
|
||||
self.vendor = supplier
|
||||
|
@ -64,7 +64,7 @@ frappe.ui.form.on("Supplier", {
|
||||
// custom buttons
|
||||
frm.add_custom_button(__('Accounting Ledger'), function () {
|
||||
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"));
|
||||
|
||||
frm.add_custom_button(__('Accounts Payable'), function () {
|
||||
|
@ -125,18 +125,9 @@ class Supplier(TransactionBase):
|
||||
|
||||
def on_trash(self):
|
||||
if self.supplier_primary_contact:
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabSupplier`
|
||||
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},
|
||||
)
|
||||
self.db_set("supplier_primary_contact", None)
|
||||
if self.supplier_primary_address:
|
||||
self.db_set("supplier_primary_address", None)
|
||||
|
||||
delete_contact_and_address("Supplier", self.name)
|
||||
|
||||
|
@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Item To Be Received"] = {
|
||||
fieldname:"from_date",
|
||||
label: __("From 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
|
||||
},
|
||||
{
|
||||
fieldname:"to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1
|
||||
},
|
||||
]
|
||||
|
@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
|
||||
fieldname:"from_date",
|
||||
label: __("From 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
|
||||
},
|
||||
{
|
||||
fieldname:"to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1
|
||||
},
|
||||
]
|
||||
|
@ -515,6 +515,7 @@ class AccountsController(TransactionBase):
|
||||
parent_dict.update({"customer": parent_dict.get("party_name")})
|
||||
|
||||
self.pricing_rules = []
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.get("item_code"):
|
||||
args = parent_dict.copy()
|
||||
@ -833,7 +834,9 @@ class AccountsController(TransactionBase):
|
||||
def set_advances(self):
|
||||
"""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", [])
|
||||
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
|
||||
|
||||
item_allowance = {}
|
||||
@ -1245,17 +1248,20 @@ class AccountsController(TransactionBase):
|
||||
|
||||
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"):
|
||||
if not item.get(item_ref_dn):
|
||||
continue
|
||||
|
||||
ref_amt = flt(
|
||||
frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on),
|
||||
self.precision(based_on, item),
|
||||
)
|
||||
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||
|
||||
if not ref_amt:
|
||||
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
|
||||
),
|
||||
title=_("Warning"),
|
||||
@ -1302,6 +1308,16 @@ class AccountsController(TransactionBase):
|
||||
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):
|
||||
"""
|
||||
Returns Sum of Amount of
|
||||
|
@ -265,7 +265,10 @@ class BuyingController(SubcontractingController):
|
||||
) / qty_in_stock_uom
|
||||
else:
|
||||
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
|
||||
else:
|
||||
item.valuation_rate = 0.0
|
||||
|
@ -131,7 +131,7 @@ def validate_returned_items(doc):
|
||||
)
|
||||
|
||||
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))
|
||||
else:
|
||||
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:
|
||||
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"]:
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, source_doc.name, doctype
|
||||
@ -610,7 +620,7 @@ def get_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
|
||||
|
||||
return_ref_field = frappe.scrub(child_doc.doctype)
|
||||
@ -619,7 +629,7 @@ def get_returned_serial_nos(child_doc, parent_doc):
|
||||
|
||||
serial_nos = []
|
||||
|
||||
fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)]
|
||||
fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
|
||||
|
||||
filters = [
|
||||
[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):
|
||||
serial_nos.extend(get_serial_nos(row.serial_no))
|
||||
serial_nos.extend(get_serial_nos(row.get(serial_no_field)))
|
||||
|
||||
return serial_nos
|
||||
|
@ -136,7 +136,7 @@ class SellingController(StockController):
|
||||
self.in_words = money_in_words(amount, self.currency)
|
||||
|
||||
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
|
||||
|
||||
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
|
||||
|
@ -464,7 +464,7 @@ class StatusUpdater(Document):
|
||||
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)
|
||||
/ 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
|
||||
where name='%(name)s'"""
|
||||
% args
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user