mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-02 21:26:55 +00:00
Compare commits
1 Commits
hotfix-14
...
ankush-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f784b17564 |
@@ -15,8 +15,6 @@ pull_request_rules:
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
- base=version-14
|
||||
- base=version-15
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
|
||||
@@ -341,7 +341,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
|
||||
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
|
||||
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item,
|
||||
|
||||
@@ -1,83 +1,67 @@
|
||||
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.ui.form.on("Account", {
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("parent_account", "report_type", "report_type");
|
||||
frm.add_fetch("parent_account", "root_type", "root_type");
|
||||
frappe.ui.form.on('Account', {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch('parent_account', 'report_type', 'report_type');
|
||||
frm.add_fetch('parent_account', 'root_type', 'root_type');
|
||||
},
|
||||
onload: function (frm) {
|
||||
frm.set_query("parent_account", function (doc) {
|
||||
onload: function(frm) {
|
||||
frm.set_query('parent_account', function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 1,
|
||||
company: doc.company,
|
||||
},
|
||||
"is_group": 1,
|
||||
"company": doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
frm.toggle_display("account_name", frm.is_new());
|
||||
refresh: function(frm) {
|
||||
frm.toggle_display('account_name', frm.is_new());
|
||||
|
||||
// hide fields if group
|
||||
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
||||
frm.toggle_display(['account_type', 'tax_rate'], cint(frm.doc.is_group) == 0);
|
||||
|
||||
// disable fields
|
||||
frm.toggle_enable(["is_group", "company"], false);
|
||||
frm.toggle_enable(['is_group', 'company'], false);
|
||||
|
||||
if (cint(frm.doc.is_group) == 0) {
|
||||
frm.toggle_display(
|
||||
"freeze_account",
|
||||
frm.doc.__onload && frm.doc.__onload.can_freeze_account
|
||||
);
|
||||
frm.toggle_display('freeze_account', frm.doc.__onload
|
||||
&& frm.doc.__onload.can_freeze_account);
|
||||
}
|
||||
|
||||
// read-only for root accounts
|
||||
if (!frm.is_new()) {
|
||||
if (!frm.doc.parent_account) {
|
||||
frm.set_read_only();
|
||||
frm.set_intro(
|
||||
__("This is a root account and cannot be edited.")
|
||||
);
|
||||
frm.set_intro(__("This is a root account and cannot be edited."));
|
||||
} else {
|
||||
// credit days and type if customer or supplier
|
||||
frm.set_intro(null);
|
||||
frm.trigger("account_type");
|
||||
frm.trigger('account_type');
|
||||
// show / hide convert buttons
|
||||
frm.trigger("add_toolbar_buttons");
|
||||
frm.trigger('add_toolbar_buttons');
|
||||
}
|
||||
if (frm.has_perm("write")) {
|
||||
frm.add_custom_button(
|
||||
__("Merge Account"),
|
||||
function () {
|
||||
frm.trigger("merge_account");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
frm.add_custom_button(
|
||||
__("Update Account Name / Number"),
|
||||
function () {
|
||||
frm.trigger("update_account_number");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
if (frm.has_perm('write')) {
|
||||
frm.add_custom_button(__('Merge Account'), function () {
|
||||
frm.trigger("merge_account");
|
||||
}, __('Actions'));
|
||||
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
||||
frm.trigger("update_account_number");
|
||||
}, __('Actions'));
|
||||
}
|
||||
}
|
||||
},
|
||||
account_type: function (frm) {
|
||||
if (frm.doc.is_group == 0) {
|
||||
frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
|
||||
frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
|
||||
frm.toggle_display(['tax_rate'], frm.doc.account_type == 'Tax');
|
||||
frm.toggle_display('warehouse', frm.doc.account_type == 'Stock');
|
||||
}
|
||||
},
|
||||
add_toolbar_buttons: function (frm) {
|
||||
frm.add_custom_button(
|
||||
__("Chart of Accounts"),
|
||||
() => {
|
||||
frappe.set_route("Tree", "Account");
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
add_toolbar_buttons: function(frm) {
|
||||
frm.add_custom_button(__('Chart of Accounts'), () => {
|
||||
frappe.set_route("Tree", "Account");
|
||||
}, __('View'));
|
||||
|
||||
if (frm.doc.is_group == 1) {
|
||||
frm.add_custom_button(__('Convert to Non-Group'), function () {
|
||||
@@ -102,35 +86,31 @@ frappe.ui.form.on("Account", {
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
}, __('View'));
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Convert to Group"),
|
||||
function () {
|
||||
return frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "convert_ledger_to_group",
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
frm.add_custom_button(__('Convert to Group'), function () {
|
||||
return frappe.call({
|
||||
doc: frm.doc,
|
||||
method: 'convert_ledger_to_group',
|
||||
callback: function() {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
}, __('Actions'));
|
||||
}
|
||||
},
|
||||
|
||||
merge_account: function (frm) {
|
||||
merge_account: function(frm) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __("Merge with Existing Account"),
|
||||
title: __('Merge with Existing Account'),
|
||||
fields: [
|
||||
{
|
||||
label: "Name",
|
||||
fieldname: "name",
|
||||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
default: frm.doc.name,
|
||||
},
|
||||
"label" : "Name",
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"default": frm.doc.name
|
||||
}
|
||||
],
|
||||
primary_action: function () {
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.account.account.merge_account",
|
||||
@@ -139,47 +119,44 @@ frappe.ui.form.on("Account", {
|
||||
new: data.name,
|
||||
is_group: frm.doc.is_group,
|
||||
root_type: frm.doc.root_type,
|
||||
company: frm.doc.company,
|
||||
company: frm.doc.company
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message) {
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message) {
|
||||
frappe.set_route("Form", "Account", r.message);
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __("Merge"),
|
||||
primary_action_label: __('Merge')
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
||||
update_account_number: function (frm) {
|
||||
update_account_number: function(frm) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __("Update Account Number / Name"),
|
||||
title: __('Update Account Number / Name'),
|
||||
fields: [
|
||||
{
|
||||
label: "Account Name",
|
||||
fieldname: "account_name",
|
||||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
default: frm.doc.account_name,
|
||||
"label": "Account Name",
|
||||
"fieldname": "account_name",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"default": frm.doc.account_name
|
||||
},
|
||||
{
|
||||
label: "Account Number",
|
||||
fieldname: "account_number",
|
||||
fieldtype: "Data",
|
||||
default: frm.doc.account_number,
|
||||
},
|
||||
"label": "Account Number",
|
||||
"fieldname": "account_number",
|
||||
"fieldtype": "Data",
|
||||
"default": frm.doc.account_number
|
||||
}
|
||||
],
|
||||
primary_action: function () {
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
if (
|
||||
data.account_number === frm.doc.account_number &&
|
||||
data.account_name === frm.doc.account_name
|
||||
) {
|
||||
if(data.account_number === frm.doc.account_number && data.account_name === frm.doc.account_name) {
|
||||
d.hide();
|
||||
return;
|
||||
}
|
||||
@@ -189,29 +166,23 @@ frappe.ui.form.on("Account", {
|
||||
args: {
|
||||
account_number: data.account_number,
|
||||
account_name: data.account_name,
|
||||
name: frm.doc.name,
|
||||
name: frm.doc.name
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message) {
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message) {
|
||||
frappe.set_route("Form", "Account", r.message);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"account_number",
|
||||
data.account_number
|
||||
);
|
||||
frm.set_value(
|
||||
"account_name",
|
||||
data.account_name
|
||||
);
|
||||
frm.set_value("account_number", data.account_number);
|
||||
frm.set_value("account_name", data.account_name);
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __("Update"),
|
||||
primary_action_label: __('Update')
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -123,7 +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\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
||||
},
|
||||
{
|
||||
"description": "Rate at which this tax is applied",
|
||||
@@ -192,7 +192,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-20 18:18:44.405723",
|
||||
"modified": "2023-04-11 16:08:46.983677",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -243,6 +243,7 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ class Account(NestedSet):
|
||||
if frappe.local.flags.allow_unverified_charts:
|
||||
return
|
||||
self.validate_parent()
|
||||
self.validate_parent_child_account_type()
|
||||
self.validate_root_details()
|
||||
validate_field_number("Account", self.name, self.account_number, self.company, "account_number")
|
||||
self.validate_group_or_ledger()
|
||||
@@ -56,20 +55,6 @@ class Account(NestedSet):
|
||||
self.validate_account_currency()
|
||||
self.validate_root_company_and_sync_account_to_children()
|
||||
|
||||
def validate_parent_child_account_type(self):
|
||||
if self.parent_account:
|
||||
if self.account_type in [
|
||||
"Direct Income",
|
||||
"Indirect Income",
|
||||
"Current Asset",
|
||||
"Current Liability",
|
||||
"Direct Expense",
|
||||
"Indirect Expense",
|
||||
]:
|
||||
parent_account_type = frappe.db.get_value("Account", self.parent_account, ["account_type"])
|
||||
if parent_account_type == self.account_type:
|
||||
throw(_("Only Parent can be of type {0}").format(self.account_type))
|
||||
|
||||
def validate_parent(self):
|
||||
"""Fetch Parent Details and validate parent account"""
|
||||
if self.parent_account:
|
||||
|
||||
@@ -32,11 +32,7 @@
|
||||
"finance_book",
|
||||
"to_rename",
|
||||
"due_date",
|
||||
"is_cancelled",
|
||||
"transaction_currency",
|
||||
"debit_in_transaction_currency",
|
||||
"credit_in_transaction_currency",
|
||||
"transaction_exchange_rate"
|
||||
"is_cancelled"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -257,40 +253,15 @@
|
||||
"fieldname": "is_cancelled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Cancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Transaction Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Transaction Exchange Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_transaction_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Transaction Currency",
|
||||
"options": "transaction_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_transaction_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Transaction Currency",
|
||||
"options": "transaction_currency"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-16 21:38:44.072267",
|
||||
"modified": "2020-04-07 16:22:33.766994",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -319,6 +290,5 @@
|
||||
"quick_entry": 1,
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -58,13 +58,6 @@ class GLEntry(Document):
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Journal Entry"
|
||||
and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type")
|
||||
== "Exchange Gain Or Loss"
|
||||
):
|
||||
return
|
||||
|
||||
if frappe.get_cached_value("Account", self.account, "account_type") not in [
|
||||
"Receivable",
|
||||
"Payable",
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule'];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"entry_type_and_date",
|
||||
"is_system_generated",
|
||||
"title",
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
@@ -534,22 +533,13 @@
|
||||
"label": "Process Deferred Accounting",
|
||||
"options": "Process Deferred Accounting",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_system_generated == 1;",
|
||||
"fieldname": "is_system_generated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is System Generated",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 14:32:22.366895",
|
||||
"modified": "2023-03-01 14:58:59.286591",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -18,7 +18,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
@@ -88,16 +87,15 @@ class JournalEntry(AccountsController):
|
||||
self.update_invoice_discounting()
|
||||
|
||||
def on_cancel(self):
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super(JournalEntry, self).on_cancel()
|
||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.update_advance_paid()
|
||||
@@ -501,12 +499,11 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
|
||||
if not against_entries:
|
||||
if self.voucher_type != "Exchange Gain Or Loss":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Journal Entry {0} does not have account {1} or already matched against other voucher"
|
||||
).format(d.reference_name, d.account)
|
||||
)
|
||||
frappe.throw(
|
||||
_(
|
||||
"Journal Entry {0} does not have account {1} or already matched against other voucher"
|
||||
).format(d.reference_name, d.account)
|
||||
)
|
||||
else:
|
||||
dr_or_cr = "debit" if d.credit > 0 else "credit"
|
||||
valid = False
|
||||
@@ -589,9 +586,7 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
party_account = against_voucher[1]
|
||||
|
||||
if (
|
||||
against_voucher[0] != cstr(d.party) or party_account != d.account
|
||||
) and self.voucher_type != "Exchange Gain Or Loss":
|
||||
if against_voucher[0] != cstr(d.party) or party_account != d.account:
|
||||
frappe.throw(
|
||||
_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
|
||||
d.idx,
|
||||
@@ -773,23 +768,18 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
):
|
||||
|
||||
ignore_exchange_rate = False
|
||||
if self.get("flags") and self.flags.get("ignore_exchange_rate"):
|
||||
ignore_exchange_rate = True
|
||||
|
||||
if not ignore_exchange_rate:
|
||||
# Modified to include the posting date for which to retreive the exchange rate
|
||||
d.exchange_rate = get_exchange_rate(
|
||||
self.posting_date,
|
||||
d.account,
|
||||
d.account_currency,
|
||||
self.company,
|
||||
d.reference_type,
|
||||
d.reference_name,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.exchange_rate,
|
||||
)
|
||||
# Modified to include the posting date for which to retreive the exchange rate
|
||||
d.exchange_rate = get_exchange_rate(
|
||||
self.posting_date,
|
||||
d.account,
|
||||
d.account_currency,
|
||||
self.company,
|
||||
d.reference_type,
|
||||
d.reference_name,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.exchange_rate,
|
||||
)
|
||||
|
||||
if not d.exchange_rate:
|
||||
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
|
||||
@@ -797,9 +787,6 @@ class JournalEntry(AccountsController):
|
||||
def create_remarks(self):
|
||||
r = []
|
||||
|
||||
if self.flags.skip_remarks_creation:
|
||||
return
|
||||
|
||||
if self.user_remark:
|
||||
r.append(_("Note: {0}").format(self.user_remark))
|
||||
|
||||
@@ -948,8 +935,6 @@ class JournalEntry(AccountsController):
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance(self, difference_account=None):
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
@@ -14,7 +13,6 @@ from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
|
||||
class TestJournalEntry(unittest.TestCase):
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_journal_entry_with_against_jv(self):
|
||||
jv_invoice = frappe.copy_doc(test_records[2])
|
||||
base_jv = frappe.copy_doc(test_records[0])
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry"
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
@@ -284,7 +284,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-16 14:11:13.507807",
|
||||
"modified": "2022-10-26 20:03:10.906259",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -141,12 +141,12 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
|
||||
)
|
||||
|
||||
if points_to_redeem > loyalty_program_details.loyalty_points:
|
||||
frappe.throw(_("You don't have enough Loyalty Points to redeem"))
|
||||
frappe.throw(_("You don't have enought Loyalty Points to redeem"))
|
||||
|
||||
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
|
||||
|
||||
if loyalty_amount > ref_doc.rounded_total:
|
||||
frappe.throw(_("You can't redeem Loyalty Points having more value than the Rounded Total."))
|
||||
if loyalty_amount > ref_doc.grand_total:
|
||||
frappe.throw(_("You can't redeem Loyalty Points having more value than the Grand Total."))
|
||||
|
||||
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
|
||||
ref_doc.loyalty_amount = loyalty_amount
|
||||
|
||||
@@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
|
||||
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
|
||||
@@ -28,12 +28,7 @@ from erpnext.accounts.general_ledger import (
|
||||
process_gl_map,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
get_supplier_block_status,
|
||||
@@ -71,7 +66,7 @@ class PaymentEntry(AccountsController):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.set_liability_account()
|
||||
self.set_missing_ref_details(force=True)
|
||||
self.set_missing_ref_details()
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
self.set_exchange_rate()
|
||||
@@ -147,10 +142,7 @@ class PaymentEntry(AccountsController):
|
||||
"Payment Ledger Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.make_advance_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
@@ -230,88 +222,79 @@ class PaymentEntry(AccountsController):
|
||||
return False
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
if self.references:
|
||||
uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
|
||||
vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
"vouchers": vouchers,
|
||||
},
|
||||
validate=True,
|
||||
)
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
},
|
||||
validate=True,
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
)
|
||||
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
if d.payment_term and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(
|
||||
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
|
||||
)
|
||||
)
|
||||
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(
|
||||
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
|
||||
)
|
||||
)
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
@@ -416,7 +399,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
if ref_doc:
|
||||
if self.paid_from_account_currency == ref_doc.currency:
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate")
|
||||
|
||||
if not self.source_exchange_rate:
|
||||
self.source_exchange_rate = get_exchange_rate(
|
||||
@@ -429,7 +412,7 @@ class PaymentEntry(AccountsController):
|
||||
elif self.paid_to and not self.target_exchange_rate:
|
||||
if ref_doc:
|
||||
if self.paid_to_account_currency == ref_doc.currency:
|
||||
self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
self.target_exchange_rate = ref_doc.get("exchange_rate")
|
||||
|
||||
if not self.target_exchange_rate:
|
||||
self.target_exchange_rate = get_exchange_rate(
|
||||
@@ -694,9 +677,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
order_amount = self.get_order_net_total()
|
||||
|
||||
net_total = flt(order_amount) + flt(self.unallocated_amount)
|
||||
net_total = self.paid_amount
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
@@ -741,20 +722,6 @@ class PaymentEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def get_order_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
doctype = "Sales Order"
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
tax_withholding_net_total = frappe.db.get_value(
|
||||
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
|
||||
)
|
||||
|
||||
return tax_withholding_net_total
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
@@ -841,25 +808,10 @@ class PaymentEntry(AccountsController):
|
||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
else:
|
||||
|
||||
# Use source/target exchange rate, so no difference amount is calculated.
|
||||
# then update exchange gain/loss amount in reference table
|
||||
# if there is an exchange gain/loss amount in reference table, submit a JE for that
|
||||
|
||||
exchange_rate = 1
|
||||
if self.payment_type == "Receive":
|
||||
exchange_rate = self.source_exchange_rate
|
||||
elif self.payment_type == "Pay":
|
||||
exchange_rate = self.target_exchange_rate
|
||||
|
||||
base_allocated_amount += flt(
|
||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
|
||||
allocated_amount_in_pe_exchange_rate = flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
|
||||
|
||||
return base_allocated_amount
|
||||
|
||||
def set_total_allocated_amount(self):
|
||||
@@ -1050,10 +1002,6 @@ class PaymentEntry(AccountsController):
|
||||
gl_entries = self.build_gl_map()
|
||||
gl_entries = process_gl_map(gl_entries)
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
else:
|
||||
self.make_exchange_gain_loss_journal()
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
if self.party_account:
|
||||
@@ -1591,7 +1539,6 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
accounting_dimensions=accounting_dimensions_filter,
|
||||
vouchers=args.get("vouchers") or None,
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(
|
||||
@@ -2041,6 +1988,7 @@ def get_payment_entry(
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
):
|
||||
reference_doc = None
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
|
||||
@@ -2180,7 +2128,7 @@ def get_payment_entry(
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_exchange_rate(ref_doc=reference_doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
|
||||
@@ -31,16 +31,6 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def get_journals_for(self, voucher_type: str, voucher_no: str) -> list:
|
||||
journals = []
|
||||
if voucher_type and voucher_no:
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1},
|
||||
fields=["parent"],
|
||||
)
|
||||
return journals
|
||||
|
||||
def test_payment_entry_against_order(self):
|
||||
so = make_sales_order()
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
@@ -601,15 +591,21 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.target_exchange_rate = 45.263
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 94.80,
|
||||
},
|
||||
)
|
||||
|
||||
pe.save()
|
||||
|
||||
self.assertEqual(flt(pe.difference_amount, 2), 0.0)
|
||||
self.assertEqual(flt(pe.unallocated_amount, 2), 0.0)
|
||||
|
||||
# the exchange gain/loss amount is captured in reference table and a separate Journal will be submitted for them
|
||||
# payment entry will not be generating difference amount
|
||||
self.assertEqual(flt(pe.references[0].exchange_gain_loss, 2), -94.74)
|
||||
|
||||
def test_payment_entry_retrieves_last_exchange_rate(self):
|
||||
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||
save_new_records,
|
||||
@@ -796,28 +792,33 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
pe.source_exchange_rate = 55
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": -500,
|
||||
},
|
||||
)
|
||||
pe.save()
|
||||
|
||||
self.assertEqual(pe.unallocated_amount, 0)
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
self.assertEqual(pe.references[0].exchange_gain_loss, 500)
|
||||
|
||||
pe.submit()
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["_Test Receivable USD - _TC", 0, 5500, si.name],
|
||||
["_Test Receivable USD - _TC", 0, 5000, si.name],
|
||||
["_Test Bank USD - _TC", 5500, 0, None],
|
||||
["_Test Exchange Gain/Loss - _TC", 0, 500, None],
|
||||
]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
# Exchange gain/loss should have been posted through a journal
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_pe)
|
||||
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
|
||||
@@ -1155,52 +1156,6 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
si3.cancel()
|
||||
si3.delete()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||
"delete_linked_ledger_entries": 1,
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
},
|
||||
)
|
||||
def test_overallocation_validation_shouldnt_misfire(self):
|
||||
"""
|
||||
Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled
|
||||
|
||||
"""
|
||||
customer = create_customer()
|
||||
create_payment_terms_template()
|
||||
|
||||
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||
template.allocate_payment_based_on_payment_terms = 0
|
||||
template.save()
|
||||
|
||||
# Validate allocation on base/company currency
|
||||
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
si.payment_terms_template = "Test Receivable Template"
|
||||
si.save().submit()
|
||||
|
||||
si.reload()
|
||||
pe = get_payment_entry(si.doctype, si.name).save()
|
||||
# There will no term based allocation
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].payment_term, None)
|
||||
self.assertEqual(flt(pe.references[0].allocated_amount), flt(si.grand_total))
|
||||
pe.save()
|
||||
|
||||
# specify a term
|
||||
pe.references[0].payment_term = template.terms[0].payment_term
|
||||
# no validation error should be thrown
|
||||
pe.save()
|
||||
|
||||
pe.paid_amount = si.grand_total + 1
|
||||
pe.references[0].allocated_amount = si.grand_total + 1
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
|
||||
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||
template.allocate_payment_based_on_payment_terms = 1
|
||||
template.save()
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
@@ -14,7 +14,6 @@ from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_rec
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
create_gain_loss_journal,
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
)
|
||||
@@ -277,11 +276,6 @@ class PaymentReconciliation(Document):
|
||||
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"))
|
||||
if payment_entry[0].get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
|
||||
payment_entry[0]["exchange_rate"] = invoice_exchange_map.get(
|
||||
payment_entry[0].get("reference_name")
|
||||
)
|
||||
|
||||
new_difference_amount = self.get_difference_amount(
|
||||
payment_entry[0], invoice[0], allocated_amount
|
||||
)
|
||||
@@ -369,6 +363,12 @@ class PaymentReconciliation(Document):
|
||||
payment_details = self.get_payment_details(row, dr_or_cr)
|
||||
reconciled_entry.append(payment_details)
|
||||
|
||||
if payment_details.difference_amount and row.reference_type not in [
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
]:
|
||||
self.make_difference_entry(payment_details)
|
||||
|
||||
if entry_list:
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
||||
|
||||
@@ -656,8 +656,6 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
"reference_type": inv.against_voucher_type,
|
||||
"reference_name": inv.against_voucher,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
"exchange_rate": inv.exchange_rate,
|
||||
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}",
|
||||
},
|
||||
{
|
||||
"account": inv.account,
|
||||
@@ -671,42 +669,13 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
"reference_type": inv.voucher_type,
|
||||
"reference_name": inv.voucher_no,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
"exchange_rate": inv.exchange_rate,
|
||||
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if difference_entry := get_difference_row(inv):
|
||||
jv.append("accounts", difference_entry)
|
||||
|
||||
jv.flags.ignore_mandatory = True
|
||||
jv.flags.ignore_exchange_rate = True
|
||||
jv.remark = None
|
||||
jv.flags.skip_remarks_creation = True
|
||||
jv.is_system_generated = True
|
||||
jv.submit()
|
||||
|
||||
if inv.difference_amount != 0:
|
||||
# make gain/loss journal
|
||||
if inv.party_type == "Customer":
|
||||
dr_or_cr = "credit" if inv.difference_amount < 0 else "debit"
|
||||
else:
|
||||
dr_or_cr = "debit" if inv.difference_amount < 0 else "credit"
|
||||
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
create_gain_loss_journal(
|
||||
company,
|
||||
inv.party_type,
|
||||
inv.party,
|
||||
inv.account,
|
||||
inv.difference_account,
|
||||
inv.difference_amount,
|
||||
dr_or_cr,
|
||||
reverse_dr_or_cr,
|
||||
inv.voucher_type,
|
||||
inv.voucher_no,
|
||||
None,
|
||||
inv.against_voucher_type,
|
||||
inv.against_voucher,
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -686,24 +686,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
|
||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||
pr.reconcile()
|
||||
total_credit_amount = frappe.db.get_all(
|
||||
total_debit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||
"sum(credit) as amount",
|
||||
"sum(debit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
|
||||
# total credit includes the exchange gain/loss amount
|
||||
self.assertEqual(flt(total_credit_amount, 2), 8500)
|
||||
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
self.assertEqual(flt(total_debit_amount, 2), -500)
|
||||
|
||||
def test_difference_amount_via_payment_entry(self):
|
||||
# Make Sale Invoice
|
||||
|
||||
@@ -144,7 +144,8 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
|
||||
[pr.payment_account, 5000.0, 0, None],
|
||||
[pr.payment_account, 6290.0, 0, None],
|
||||
["_Test Exchange Gain/Loss - _TC", 0, 1290, None],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
frappe.ui.form.on('POS Closing Entry Detail', {
|
||||
closing_amount: (frm, cdt, cdn) => {
|
||||
const row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "difference", flt(row.closing_amount - row.expected_amount));
|
||||
frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount));
|
||||
}
|
||||
})
|
||||
|
||||
@@ -185,7 +185,6 @@ function refresh_payments(d, frm) {
|
||||
}
|
||||
if (payment) {
|
||||
payment.expected_amount += flt(p.amount);
|
||||
payment.closing_amount = payment.expected_amount;
|
||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||
} else {
|
||||
frm.add_child("payment_reconciliation", {
|
||||
|
||||
@@ -221,7 +221,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
@@ -236,7 +235,7 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2023-08-10 16:25:49.322697",
|
||||
"modified": "2022-08-01 11:37:14.991228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
|
||||
@@ -8,11 +8,9 @@ import frappe
|
||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
||||
make_closing_entry_from_opening,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.selling.page.point_of_sale.point_of_sale import get_items
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
@@ -69,36 +67,6 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
self.assertTrue(pcv_doc.name)
|
||||
|
||||
def test_pos_qty_for_item(self):
|
||||
"""
|
||||
Test if quantity is calculated correctly for an item in POS Closing Entry
|
||||
"""
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
test_item_qty = get_test_item_qty(pos_profile)
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.submit()
|
||||
|
||||
# make return entry of pos_inv2
|
||||
pos_return = make_sales_return(pos_inv2.name)
|
||||
pos_return.paid_amount = pos_return.grand_total
|
||||
pos_return.save()
|
||||
pos_return.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
pcv_doc.submit()
|
||||
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
test_item_qty_after_sales = get_test_item_qty(pos_profile)
|
||||
self.assertEqual(test_item_qty_after_sales, test_item_qty - 1)
|
||||
|
||||
def test_cancelling_of_pos_closing_entry(self):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
@@ -155,19 +123,3 @@ def init_user_and_profile(**args):
|
||||
pos_profile.save()
|
||||
|
||||
return test_user, pos_profile
|
||||
|
||||
|
||||
def get_test_item_qty(pos_profile):
|
||||
test_item_pos = get_items(
|
||||
start=0,
|
||||
page_length=5,
|
||||
price_list="Standard Selling",
|
||||
pos_profile=pos_profile.name,
|
||||
search_term="_Test Item",
|
||||
item_group="All Item Groups",
|
||||
)
|
||||
|
||||
test_item_qty = [item for item in test_item_pos["items"] if item["item_code"] == "_Test Item"][
|
||||
0
|
||||
].get("actual_qty")
|
||||
return test_item_qty
|
||||
|
||||
@@ -542,7 +542,6 @@ def get_stock_availability(item_code, warehouse):
|
||||
is_stock_item = True
|
||||
bin_qty = get_bin_qty(item_code, warehouse)
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
|
||||
return bin_qty - pos_sales_qty, is_stock_item
|
||||
else:
|
||||
is_stock_item = True
|
||||
@@ -596,6 +595,7 @@ def get_pos_reserved_qty(item_code, warehouse):
|
||||
.where(
|
||||
(p_inv.name == p_item.parent)
|
||||
& (IfNull(p_inv.consolidated_invoice, "") == "")
|
||||
& (p_inv.is_return == 0)
|
||||
& (p_item.docstatus == 1)
|
||||
& (p_item.item_code == item_code)
|
||||
& (p_item.warehouse == warehouse)
|
||||
|
||||
@@ -95,6 +95,7 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -107,6 +108,7 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
def process_merging_into_sales_invoice(self, data):
|
||||
sales_invoice = self.get_new_sales_invoice()
|
||||
|
||||
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
|
||||
|
||||
sales_invoice.is_consolidated = 1
|
||||
@@ -163,7 +165,8 @@ class POSInvoiceMergeLog(Document):
|
||||
for i in items:
|
||||
if (
|
||||
i.item_code == item.item_code
|
||||
and not i.serial_and_batch_bundle
|
||||
and not i.serial_no
|
||||
and not i.batch_no
|
||||
and i.uom == item.uom
|
||||
and i.net_rate == item.net_rate
|
||||
and i.warehouse == item.warehouse
|
||||
@@ -382,7 +385,6 @@ def split_invoices(invoices):
|
||||
for d in invoices
|
||||
if d.is_return and d.return_against
|
||||
]
|
||||
|
||||
for pos_invoice in pos_return_docs:
|
||||
for item in pos_invoice.items:
|
||||
if not item.serial_no and not item.serial_and_batch_bundle:
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-11 10:56:51.699137",
|
||||
"modified": "2023-04-21 17:19:30.912953",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
@@ -154,25 +154,15 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -35,7 +35,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
super.onload();
|
||||
|
||||
// Ignore linked advances
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
|
||||
@@ -167,7 +167,6 @@
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"subscription_section",
|
||||
"subscription",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"column_break_114",
|
||||
@@ -1424,12 +1423,6 @@
|
||||
"options": "Advance Tax",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription",
|
||||
"fieldtype": "Link",
|
||||
"label": "Subscription",
|
||||
"options": "Subscription"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_old_subcontracting_flow",
|
||||
@@ -1584,7 +1577,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-25 17:22:59.145031",
|
||||
"modified": "2023-07-04 17:22:59.145031",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -229,7 +229,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
|
||||
):
|
||||
@@ -536,7 +536,6 @@ class PurchaseInvoice(BuyingController):
|
||||
merge_entries=False,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
@@ -581,6 +580,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.get_asset_gl_entry(gl_entries)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_exchange_gain_loss_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
@@ -969,6 +969,30 @@ 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, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
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": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else 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")
|
||||
@@ -1415,8 +1439,6 @@ class PurchaseInvoice(BuyingController):
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Payment Ledger Entry",
|
||||
"Tax Withheld Vouchers",
|
||||
"Serial and Batch Bundle",
|
||||
|
||||
@@ -1273,11 +1273,10 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
creditors_account = pi.credit_to
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 37500.0],
|
||||
["_Test Payable USD - _TC", -37500.0],
|
||||
["_Test Payable USD - _TC", -35000.0],
|
||||
["Exchange Gain/Loss - _TC", -2500.0],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@@ -1294,31 +1293,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
||||
|
||||
pi.reload()
|
||||
self.assertEqual(pi.outstanding_amount, 0)
|
||||
|
||||
total_debit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": creditors_account, "docstatus": 1, "reference_name": pi.name},
|
||||
"sum(debit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
self.assertEqual(flt(total_debit_amount, 2), 2500)
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"account": creditors_account,
|
||||
"docstatus": 1,
|
||||
"reference_name": pi.name,
|
||||
"debit": 2500,
|
||||
"debit_in_account_currency": 0,
|
||||
},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
pi_2 = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD",
|
||||
currency="USD",
|
||||
@@ -1343,12 +1317,10 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
pi_2.save()
|
||||
pi_2.submit()
|
||||
|
||||
pi_2.reload()
|
||||
self.assertEqual(pi_2.outstanding_amount, 0)
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 36500.0],
|
||||
["_Test Payable USD - _TC", -36500.0],
|
||||
["_Test Payable USD - _TC", -35000.0],
|
||||
["Exchange Gain/Loss - _TC", -1500.0],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@@ -1379,39 +1351,12 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
||||
|
||||
total_debit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name},
|
||||
"sum(debit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
self.assertEqual(flt(total_debit_amount, 2), 1500)
|
||||
jea_parent_2 = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"account": creditors_account,
|
||||
"docstatus": 1,
|
||||
"reference_name": pi_2.name,
|
||||
"debit": 1500,
|
||||
"debit_in_account_currency": 0,
|
||||
},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent_2.parent, "voucher_type"),
|
||||
"Exchange Gain Or Loss",
|
||||
)
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent.parent, "docstatus"), 2)
|
||||
|
||||
pi_2.reload()
|
||||
pi_2.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent_2.parent, "docstatus"), 2)
|
||||
|
||||
pay.reload()
|
||||
pay.cancel()
|
||||
|
||||
@@ -1791,52 +1736,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
||||
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
0,
|
||||
)
|
||||
|
||||
po = create_purchase_order(do_not_save=1)
|
||||
po.payment_terms_template = "_Test Payment Term Template"
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
pr = create_pr_against_po(po.name, received_qty=4)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
1,
|
||||
)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
0,
|
||||
)
|
||||
|
||||
def test_offsetting_entries_for_accounting_dimensions(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.report.trial_balance.test_trial_balance import (
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<style>
|
||||
.print-format {
|
||||
padding: 4mm;
|
||||
font-size: 8.0pt !important;
|
||||
}
|
||||
.print-format td {
|
||||
vertical-align:middle !important;
|
||||
}
|
||||
.old {
|
||||
background-color: #FFB3C0;
|
||||
}
|
||||
.new {
|
||||
background-color: #B3FFCC;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<table class="table table-bordered table-condensed">
|
||||
<colgroup>
|
||||
{% for col in gl_columns%}
|
||||
<col style="width: 18mm;">
|
||||
{% endfor %}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in gl_columns%}
|
||||
<td>{{ col.label }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for gl in gl_data%}
|
||||
{% if gl["old"]%}
|
||||
<tr class="old">
|
||||
{% else %}
|
||||
<tr class="new">
|
||||
{% endif %}
|
||||
{% for col in gl_columns %}
|
||||
<td class="text-right">
|
||||
{{ gl[col.fieldname] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Repost Accounting Ledger", {
|
||||
setup: function(frm) {
|
||||
frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
|
||||
if (doc.company) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
docstatus: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__('Show Preview'), () => {
|
||||
frm.call({
|
||||
method: 'generate_preview',
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __('Generating Preview'),
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
let content = r.message;
|
||||
let opts = {
|
||||
title: "Preview",
|
||||
subtitle: "preview",
|
||||
content: content,
|
||||
print_settings: {orientation: "landscape"},
|
||||
columns: [],
|
||||
data: [],
|
||||
}
|
||||
frappe.render_grid(opts);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"column_break_vpup",
|
||||
"delete_cancelled_entries",
|
||||
"section_break_metl",
|
||||
"vouchers",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Repost Accounting Ledger",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "vouchers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Vouchers",
|
||||
"options": "Repost Accounting Ledger Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vpup",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_metl",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delete_cancelled_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Delete Cancelled Ledger Entries"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-27 15:47:58.975034",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
|
||||
class RepostAccountingLedger(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
|
||||
self._allowed_types = set(
|
||||
["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_vouchers()
|
||||
self.validate_for_closed_fiscal_year()
|
||||
self.validate_for_deferred_accounting()
|
||||
|
||||
def validate_for_deferred_accounting(self):
|
||||
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
|
||||
docs_with_deferred_revenue = frappe.db.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
|
||||
docs_with_deferred_expense = frappe.db.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
if docs_with_deferred_revenue or docs_with_deferred_expense:
|
||||
frappe.throw(
|
||||
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
|
||||
frappe.bold(
|
||||
comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_for_closed_fiscal_year(self):
|
||||
if self.vouchers:
|
||||
latest_pcv = (
|
||||
frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"company": self.company},
|
||||
order_by="posting_date desc",
|
||||
pluck="posting_date",
|
||||
limit=1,
|
||||
)
|
||||
or None
|
||||
)
|
||||
if not latest_pcv:
|
||||
return
|
||||
|
||||
for vtype in self._allowed_types:
|
||||
if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]:
|
||||
latest_voucher = frappe.db.get_all(
|
||||
vtype,
|
||||
filters={"name": ["in", names]},
|
||||
pluck="posting_date",
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
)[0]
|
||||
if latest_voucher and latest_pcv[0] >= latest_voucher:
|
||||
frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year."))
|
||||
|
||||
def validate_vouchers(self):
|
||||
if self.vouchers:
|
||||
# Validate voucher types
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
if disallowed_types := voucher_types.difference(self._allowed_types):
|
||||
frappe.throw(
|
||||
_("{0} types are not allowed. Only {1} are.").format(
|
||||
frappe.bold(comma_and(list(disallowed_types))),
|
||||
frappe.bold(comma_and(list(self._allowed_types))),
|
||||
)
|
||||
)
|
||||
|
||||
def get_existing_ledger_entries(self):
|
||||
vouchers = [x.voucher_no for x in self.vouchers]
|
||||
gl = qb.DocType("GL Entry")
|
||||
existing_gles = (
|
||||
qb.from_(gl)
|
||||
.select(gl.star)
|
||||
.where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
self.gles = frappe._dict({})
|
||||
|
||||
for gle in existing_gles:
|
||||
self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault(
|
||||
"existing", []
|
||||
).append(gle.update({"old": True}))
|
||||
|
||||
def generate_preview_data(self):
|
||||
self.gl_entries = []
|
||||
self.get_existing_ledger_entries()
|
||||
for x in self.vouchers:
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
gle_map = doc.build_gl_map()
|
||||
else:
|
||||
gle_map = doc.get_gl_entries()
|
||||
|
||||
old_entries = self.gles.get((x.voucher_type, x.voucher_no))
|
||||
if old_entries:
|
||||
self.gl_entries.extend(old_entries.existing)
|
||||
self.gl_entries.extend(gle_map)
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_preview(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
|
||||
|
||||
gl_columns = []
|
||||
gl_data = []
|
||||
|
||||
self.generate_preview_data()
|
||||
if self.gl_entries:
|
||||
filters = {"company": self.company, "include_dimensions": 1}
|
||||
for x in get_gl_columns(filters):
|
||||
if x["fieldname"] == "gl_entry":
|
||||
x["fieldname"] = "name"
|
||||
gl_columns.append(x)
|
||||
|
||||
gl_data = self.gl_entries
|
||||
rendered_page = frappe.render_template(
|
||||
"erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html",
|
||||
{"gl_columns": gl_columns, "gl_data": gl_data},
|
||||
)
|
||||
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
account_repost_doc=self.name,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
)
|
||||
frappe.msgprint(_("Repost has started in the background"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_repost(account_repost_doc=str) -> None:
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
|
||||
if repost_doc.docstatus == 1:
|
||||
# Prevent repost on invoices with deferred accounting
|
||||
repost_doc.validate_for_deferred_accounting()
|
||||
|
||||
for x in repost_doc.vouchers:
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
|
||||
if repost_doc.delete_cancelled_entries:
|
||||
frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name})
|
||||
frappe.db.delete(
|
||||
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
|
||||
)
|
||||
|
||||
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
@@ -1,202 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
|
||||
def teadDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
preq = frappe.get_doc(
|
||||
make_payment_request(
|
||||
dt=si.doctype,
|
||||
dn=si.name,
|
||||
payment_request_type="Inward",
|
||||
party_type="Customer",
|
||||
party=si.customer,
|
||||
)
|
||||
)
|
||||
preq.save().submit()
|
||||
|
||||
# Test Validation Error
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append(
|
||||
"vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name}
|
||||
) # this should throw validation error
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
ral.vouchers.pop()
|
||||
preq.cancel()
|
||||
preq.delete()
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# manually set an incorrect debit amount in DB
|
||||
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
|
||||
frappe.db.set_value("GL Entry", gle[0], "debit", 90)
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
# Assert incorrect ledger balance
|
||||
self.assertNotEqual(res[0], (si.name, 100, 100))
|
||||
|
||||
# Submit repost document
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
# Ledger should reflect correct amount post repost
|
||||
self.assertEqual(res[0], (si.name, 100, 100))
|
||||
|
||||
def test_02_deferred_accounting_valiations(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
si.items[0].enable_deferred_revenue = True
|
||||
si.items[0].deferred_revenue_account = self.deferred_revenue
|
||||
si.items[0].service_start_date = nowdate()
|
||||
si.items[0].service_end_date = add_days(nowdate(), 90)
|
||||
si.save().submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
|
||||
def test_04_pcv_validation(self):
|
||||
# Clear old GL entries so PCV can be submitted.
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
qb.from_(gl).delete().where(gl.company == self.company).run()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"posting_date": today(),
|
||||
"company": self.company,
|
||||
"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
|
||||
"cost_center": self.cost_center,
|
||||
"closing_account_head": self.retained_earnings,
|
||||
"remarks": "test",
|
||||
}
|
||||
)
|
||||
pcv.save().submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
pcv.reload()
|
||||
pcv.cancel()
|
||||
pcv.delete()
|
||||
|
||||
def test_03_deletion_flag_and_preview_function(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# assert preview data is generated
|
||||
preview = ral.generate_preview()
|
||||
self.assertIsNotNone(preview)
|
||||
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
start_repost(ral.name)
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-07-04 14:14:01.243848",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_no"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 14:15:51.165584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Items",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class RepostAccountingLedgerItems(Document):
|
||||
pass
|
||||
@@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
super.onload();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
|
||||
@@ -194,7 +194,6 @@
|
||||
"select_print_heading",
|
||||
"language",
|
||||
"subscription_section",
|
||||
"subscription",
|
||||
"from_date",
|
||||
"auto_repeat",
|
||||
"column_break_140",
|
||||
@@ -2018,12 +2017,6 @@
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription",
|
||||
"fieldtype": "Link",
|
||||
"label": "Subscription",
|
||||
"options": "Subscription"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.apply_discount_on == \"Grand Total\"",
|
||||
@@ -2164,7 +2157,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-07-25 16:02:18.988799",
|
||||
"modified": "2023-06-21 16:02:18.988799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -23,7 +23,7 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_disposal_account_and_cost_center,
|
||||
@@ -32,7 +32,6 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
reset_depreciation_schedule,
|
||||
reverse_depreciation_entry_made_after_disposal,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
||||
@@ -386,8 +385,6 @@ class SalesInvoice(SellingController):
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Payment Ledger Entry",
|
||||
"Serial and Batch Bundle",
|
||||
)
|
||||
@@ -1032,10 +1029,7 @@ class SalesInvoice(SellingController):
|
||||
merge_entries=False,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if update_outstanding == "No":
|
||||
@@ -1060,10 +1054,10 @@ class SalesInvoice(SellingController):
|
||||
self.make_customer_gl_entry(gl_entries)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_exchange_gain_loss_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
@@ -1182,13 +1176,12 @@ class SalesInvoice(SellingController):
|
||||
self.get("posting_date"),
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
add_asset_activity(asset.name, _("Asset returned"))
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was returned through Sales Invoice {1}."
|
||||
"This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
@@ -1216,7 +1209,6 @@ class SalesInvoice(SellingController):
|
||||
self.get("posting_date"),
|
||||
)
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
add_asset_activity(asset.name, _("Asset sold"))
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = self.customer
|
||||
@@ -1654,13 +1646,15 @@ class SalesInvoice(SellingController):
|
||||
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
|
||||
|
||||
def get_returned_amount(self):
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
|
||||
doc = frappe.qb.DocType(self.doctype)
|
||||
returned_amount = (
|
||||
frappe.qb.from_(doc)
|
||||
.select(Sum(doc.grand_total))
|
||||
.where((doc.docstatus == 1) & (doc.is_return == 1) & (doc.return_against == self.name))
|
||||
.where(
|
||||
(doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name)
|
||||
)
|
||||
).run()
|
||||
|
||||
return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
|
||||
|
||||
@@ -15,7 +15,6 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
"Delivery Note": ["items", "delivery_note"],
|
||||
"Timesheet": ["timesheets", "time_sheet"],
|
||||
},
|
||||
"transactions": [
|
||||
|
||||
@@ -2049,27 +2049,28 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
|
||||
expected_values = [
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["Round Off - _TC", 0.01, 0.01],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
]
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
]
|
||||
)
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[i][0], gle.account)
|
||||
self.assertEqual(expected_values[i][1], gle.debit)
|
||||
self.assertEqual(expected_values[i][2], gle.credit)
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
|
||||
def test_rounding_adjustment_3(self):
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
@@ -2124,14 +2125,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
["Round Off - _TC", 0.02, 0.01],
|
||||
["Round Off - _TC", 0.01, 0],
|
||||
]
|
||||
)
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
@@ -3213,10 +3213,15 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
account.disabled = 0
|
||||
account.save()
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
|
||||
|
||||
jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
|
||||
|
||||
jv.accounts[0].exchange_rate = 70
|
||||
@@ -3249,28 +3254,18 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Exchange Gain/Loss - _TC", 500.0, 0.0, nowdate()],
|
||||
["_Test Receivable USD - _TC", 7500.0, 0.0, nowdate()],
|
||||
["_Test Receivable USD - _TC", 0.0, 500.0, nowdate()],
|
||||
["Sales - _TC", 0.0, 7500.0, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, nowdate())
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": "Sales Invoice", "reference_name": si.name, "docstatus": 1},
|
||||
pluck="parent",
|
||||
)
|
||||
journals = [x for x in journals if x != jv.name]
|
||||
self.assertEqual(len(journals), 1)
|
||||
je_type = frappe.get_cached_value("Journal Entry", journals[0], "voucher_type")
|
||||
self.assertEqual(je_type, "Exchange Gain Or Loss")
|
||||
ledger_outstanding = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"against_voucher_no": si.name, "delinked": 0},
|
||||
fields=["sum(amount), sum(amount_in_account_currency)"],
|
||||
as_list=1,
|
||||
frappe.db.set_single_value(
|
||||
"Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
|
||||
)
|
||||
|
||||
def test_batch_expiry_for_sales_invoice_return(self):
|
||||
@@ -3376,14 +3371,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
||||
|
||||
@change_settings("Selling Settings", {"allow_negative_rates_for_items": 0})
|
||||
def test_sales_return_negative_rate(self):
|
||||
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
si.items[0].rate = 10
|
||||
si.save()
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Subscription', {
|
||||
setup: function (frm) {
|
||||
frm.set_query('party_type', function () {
|
||||
setup: function(frm) {
|
||||
frm.set_query('party_type', function() {
|
||||
return {
|
||||
filters: {
|
||||
filters : {
|
||||
name: ['in', ['Customer', 'Supplier']]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query('cost_center', function () {
|
||||
frm.set_query('cost_center', function() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company
|
||||
@@ -20,60 +20,76 @@ frappe.ui.form.on('Subscription', {
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.is_new()) return;
|
||||
|
||||
if (frm.doc.status !== 'Cancelled') {
|
||||
frm.add_custom_button(
|
||||
__('Fetch Subscription Updates'),
|
||||
() => frm.trigger('get_subscription_updates'),
|
||||
__('Actions')
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__('Cancel Subscription'),
|
||||
() => frm.trigger('cancel_this_subscription'),
|
||||
__('Actions')
|
||||
);
|
||||
} else if (frm.doc.status === 'Cancelled') {
|
||||
frm.add_custom_button(
|
||||
__('Restart Subscription'),
|
||||
() => frm.trigger('renew_this_subscription'),
|
||||
__('Actions')
|
||||
);
|
||||
refresh: function(frm) {
|
||||
if(!frm.is_new()){
|
||||
if(frm.doc.status !== 'Cancelled'){
|
||||
frm.add_custom_button(
|
||||
__('Cancel Subscription'),
|
||||
() => frm.events.cancel_this_subscription(frm)
|
||||
);
|
||||
frm.add_custom_button(
|
||||
__('Fetch Subscription Updates'),
|
||||
() => frm.events.get_subscription_updates(frm)
|
||||
);
|
||||
}
|
||||
else if(frm.doc.status === 'Cancelled'){
|
||||
frm.add_custom_button(
|
||||
__('Restart Subscription'),
|
||||
() => frm.events.renew_this_subscription(frm)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
cancel_this_subscription: function (frm) {
|
||||
cancel_this_subscription: function(frm) {
|
||||
const doc = frm.doc;
|
||||
frappe.confirm(
|
||||
__('This action will stop future billing. Are you sure you want to cancel this subscription?'),
|
||||
() => {
|
||||
frm.call('cancel_subscription').then(r => {
|
||||
if (!r.exec) {
|
||||
frm.reload_doc();
|
||||
function() {
|
||||
frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.subscription.subscription.cancel_subscription",
|
||||
args: {name: doc.name},
|
||||
callback: function(data){
|
||||
if(!data.exc){
|
||||
frm.reload_doc();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
renew_this_subscription: function (frm) {
|
||||
renew_this_subscription: function(frm) {
|
||||
const doc = frm.doc;
|
||||
frappe.confirm(
|
||||
__('Are you sure you want to restart this subscription?'),
|
||||
() => {
|
||||
frm.call('restart_subscription').then(r => {
|
||||
if (!r.exec) {
|
||||
frm.reload_doc();
|
||||
__('You will lose records of previously generated invoices. Are you sure you want to restart this subscription?'),
|
||||
function() {
|
||||
frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.subscription.subscription.restart_subscription",
|
||||
args: {name: doc.name},
|
||||
callback: function(data){
|
||||
if(!data.exc){
|
||||
frm.reload_doc();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
get_subscription_updates: function (frm) {
|
||||
frm.call('process').then(r => {
|
||||
if (!r.exec) {
|
||||
frm.reload_doc();
|
||||
get_subscription_updates: function(frm) {
|
||||
const doc = frm.doc;
|
||||
frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.subscription.subscription.get_subscription_updates",
|
||||
args: {name: doc.name},
|
||||
freeze: true,
|
||||
callback: function(data){
|
||||
if(!data.exc){
|
||||
frm.reload_doc();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"trial_period_end",
|
||||
"follow_calendar_months",
|
||||
"generate_new_invoices_past_due_date",
|
||||
"submit_invoice",
|
||||
"column_break_11",
|
||||
"current_invoice_start",
|
||||
"current_invoice_end",
|
||||
@@ -36,8 +35,12 @@
|
||||
"cb_2",
|
||||
"additional_discount_percentage",
|
||||
"additional_discount_amount",
|
||||
"sb_3",
|
||||
"submit_invoice",
|
||||
"invoices",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center"
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -159,12 +162,29 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional DIscount Amount"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.invoices",
|
||||
"fieldname": "sb_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Invoices"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "invoices",
|
||||
"fieldtype": "Table",
|
||||
"label": "Invoices",
|
||||
"options": "Subscription Invoice"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Link",
|
||||
@@ -239,27 +259,15 @@
|
||||
"default": "1",
|
||||
"fieldname": "submit_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit Generated Invoices"
|
||||
"label": "Submit Invoice Automatically"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"group": "Buying",
|
||||
"link_doctype": "Purchase Invoice",
|
||||
"link_fieldname": "subscription"
|
||||
},
|
||||
{
|
||||
"group": "Selling",
|
||||
"link_doctype": "Sales Invoice",
|
||||
"link_fieldname": "subscription"
|
||||
}
|
||||
],
|
||||
"modified": "2022-02-18 23:24:57.185054",
|
||||
"links": [],
|
||||
"modified": "2021-04-19 15:24:27.550797",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -301,6 +309,5 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -2,17 +2,14 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_to_date,
|
||||
cint,
|
||||
cstr,
|
||||
date_diff,
|
||||
flt,
|
||||
get_last_day,
|
||||
@@ -20,7 +17,8 @@ from frappe.utils.data import (
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext import get_default_company, get_default_cost_center
|
||||
import erpnext
|
||||
from erpnext import get_default_company
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
@@ -28,39 +26,33 @@ from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_pla
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
|
||||
|
||||
class InvoiceCancelled(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceNotCancelled(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Subscription(Document):
|
||||
def before_insert(self):
|
||||
# update start just before the subscription doc is created
|
||||
self.update_subscription_period(self.start_date)
|
||||
|
||||
def update_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
|
||||
def update_subscription_period(self, date=None, return_date=False):
|
||||
"""
|
||||
Subscription period is the period to be billed. This method updates the
|
||||
beginning of the billing period and end of the billing period.
|
||||
|
||||
The beginning of the billing period is represented in the doctype as
|
||||
`current_invoice_start` and the end of the billing period is represented
|
||||
as `current_invoice_end`.
|
||||
"""
|
||||
self.current_invoice_start = self.get_current_invoice_start(date)
|
||||
self.current_invoice_end = self.get_current_invoice_end(self.current_invoice_start)
|
||||
|
||||
def _get_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
|
||||
If return_date is True, it wont update the start and end dates.
|
||||
This is implemented to get the dates to check if is_current_invoice_generated
|
||||
"""
|
||||
_current_invoice_start = self.get_current_invoice_start(date)
|
||||
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
|
||||
|
||||
return _current_invoice_start, _current_invoice_end
|
||||
if return_date:
|
||||
return _current_invoice_start, _current_invoice_end
|
||||
|
||||
def get_current_invoice_start(
|
||||
self, date: Optional[Union[datetime.date, str]] = None
|
||||
) -> Union[datetime.date, str]:
|
||||
self.current_invoice_start = _current_invoice_start
|
||||
self.current_invoice_end = _current_invoice_end
|
||||
|
||||
def get_current_invoice_start(self, date=None):
|
||||
"""
|
||||
This returns the date of the beginning of the current billing period.
|
||||
If the `date` parameter is not given , it will be automatically set as today's
|
||||
@@ -83,13 +75,13 @@ class Subscription(Document):
|
||||
|
||||
return _current_invoice_start
|
||||
|
||||
def get_current_invoice_end(
|
||||
self, date: Optional[Union[datetime.date, str]] = None
|
||||
) -> Union[datetime.date, str]:
|
||||
def get_current_invoice_end(self, date=None):
|
||||
"""
|
||||
This returns the date of the end of the current billing period.
|
||||
|
||||
If the subscription is in trial period, it will be set as the end of the
|
||||
trial period.
|
||||
|
||||
If is not in a trial period, it will be `x` days from the beginning of the
|
||||
current billing period where `x` is the billing interval from the
|
||||
`Subscription Plan` in the `Subscription`.
|
||||
@@ -113,13 +105,24 @@ class Subscription(Document):
|
||||
_current_invoice_end = get_last_day(date)
|
||||
|
||||
if self.follow_calendar_months:
|
||||
# Sets the end date
|
||||
# eg if date is 17-Feb-2022, the invoice will be generated per month ie
|
||||
# the invoice will be created from 17 Feb to 28 Feb
|
||||
billing_info = self.get_billing_cycle_and_interval()
|
||||
billing_interval_count = billing_info[0]["billing_interval_count"]
|
||||
_end = add_months(getdate(date), billing_interval_count - 1)
|
||||
_current_invoice_end = get_last_day(_end)
|
||||
calendar_months = get_calendar_months(billing_interval_count)
|
||||
calendar_month = 0
|
||||
current_invoice_end_month = getdate(_current_invoice_end).month
|
||||
current_invoice_end_year = getdate(_current_invoice_end).year
|
||||
|
||||
for month in calendar_months:
|
||||
if month <= current_invoice_end_month:
|
||||
calendar_month = month
|
||||
|
||||
if cint(calendar_month - billing_interval_count) <= 0 and getdate(date).month != 1:
|
||||
calendar_month = 12
|
||||
current_invoice_end_year -= 1
|
||||
|
||||
_current_invoice_end = get_last_day(
|
||||
cstr(current_invoice_end_year) + "-" + cstr(calendar_month) + "-01"
|
||||
)
|
||||
|
||||
if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
|
||||
_current_invoice_end = self.end_date
|
||||
@@ -127,7 +130,7 @@ class Subscription(Document):
|
||||
return _current_invoice_end
|
||||
|
||||
@staticmethod
|
||||
def validate_plans_billing_cycle(billing_cycle_data: List[Dict[str, str]]) -> None:
|
||||
def validate_plans_billing_cycle(billing_cycle_data):
|
||||
"""
|
||||
Makes sure that all `Subscription Plan` in the `Subscription` have the
|
||||
same billing interval
|
||||
@@ -135,9 +138,10 @@ class Subscription(Document):
|
||||
if billing_cycle_data and len(billing_cycle_data) != 1:
|
||||
frappe.throw(_("You can only have Plans with the same billing cycle in a Subscription"))
|
||||
|
||||
def get_billing_cycle_and_interval(self) -> List[Dict[str, str]]:
|
||||
def get_billing_cycle_and_interval(self):
|
||||
"""
|
||||
Returns a dict representing the billing interval and cycle for this `Subscription`.
|
||||
|
||||
You shouldn't need to call this directly. Use `get_billing_cycle` instead.
|
||||
"""
|
||||
plan_names = [plan.plan for plan in self.plans]
|
||||
@@ -152,65 +156,72 @@ class Subscription(Document):
|
||||
|
||||
return billing_info
|
||||
|
||||
def get_billing_cycle_data(self) -> Dict[str, int]:
|
||||
def get_billing_cycle_data(self):
|
||||
"""
|
||||
Returns dict contain the billing cycle data.
|
||||
|
||||
You shouldn't need to call this directly. Use `get_billing_cycle` instead.
|
||||
"""
|
||||
billing_info = self.get_billing_cycle_and_interval()
|
||||
if not billing_info:
|
||||
return None
|
||||
|
||||
data = dict()
|
||||
interval = billing_info[0]["billing_interval"]
|
||||
interval_count = billing_info[0]["billing_interval_count"]
|
||||
self.validate_plans_billing_cycle(billing_info)
|
||||
|
||||
if interval not in ["Day", "Week"]:
|
||||
data["days"] = -1
|
||||
if billing_info:
|
||||
data = dict()
|
||||
interval = billing_info[0]["billing_interval"]
|
||||
interval_count = billing_info[0]["billing_interval_count"]
|
||||
if interval not in ["Day", "Week"]:
|
||||
data["days"] = -1
|
||||
if interval == "Day":
|
||||
data["days"] = interval_count - 1
|
||||
elif interval == "Month":
|
||||
data["months"] = interval_count
|
||||
elif interval == "Year":
|
||||
data["years"] = interval_count
|
||||
# todo: test week
|
||||
elif interval == "Week":
|
||||
data["days"] = interval_count * 7 - 1
|
||||
|
||||
if interval == "Day":
|
||||
data["days"] = interval_count - 1
|
||||
elif interval == "Week":
|
||||
data["days"] = interval_count * 7 - 1
|
||||
elif interval == "Month":
|
||||
data["months"] = interval_count
|
||||
elif interval == "Year":
|
||||
data["years"] = interval_count
|
||||
return data
|
||||
|
||||
return data
|
||||
def set_status_grace_period(self):
|
||||
"""
|
||||
Sets the `Subscription` `status` based on the preference set in `Subscription Settings`.
|
||||
|
||||
def set_subscription_status(self) -> None:
|
||||
Used when the `Subscription` needs to decide what to do after the current generated
|
||||
invoice is past it's due date and grace period.
|
||||
"""
|
||||
subscription_settings = frappe.get_single("Subscription Settings")
|
||||
if self.status == "Past Due Date" and self.is_past_grace_period():
|
||||
self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
|
||||
|
||||
def set_subscription_status(self):
|
||||
"""
|
||||
Sets the status of the `Subscription`
|
||||
"""
|
||||
if self.is_trialling():
|
||||
self.status = "Trialling"
|
||||
elif (
|
||||
self.status == "Active"
|
||||
and self.end_date
|
||||
and getdate(frappe.flags.current_date) > getdate(self.end_date)
|
||||
):
|
||||
elif self.status == "Active" and self.end_date and getdate() > getdate(self.end_date):
|
||||
self.status = "Completed"
|
||||
elif self.is_past_grace_period():
|
||||
self.status = self.get_status_for_past_grace_period()
|
||||
self.cancelation_date = (
|
||||
getdate(frappe.flags.current_date) if self.status == "Cancelled" else None
|
||||
)
|
||||
subscription_settings = frappe.get_single("Subscription Settings")
|
||||
self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
|
||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||
self.status = "Past Due Date"
|
||||
elif not self.has_outstanding_invoice() or self.is_new_subscription():
|
||||
elif not self.has_outstanding_invoice():
|
||||
self.status = "Active"
|
||||
elif self.is_new_subscription():
|
||||
self.status = "Active"
|
||||
|
||||
self.save()
|
||||
|
||||
def is_trialling(self) -> bool:
|
||||
def is_trialling(self):
|
||||
"""
|
||||
Returns `True` if the `Subscription` is in trial period.
|
||||
"""
|
||||
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
||||
|
||||
@staticmethod
|
||||
def period_has_passed(end_date: Union[str, datetime.date]) -> bool:
|
||||
def period_has_passed(end_date):
|
||||
"""
|
||||
Returns true if the given `end_date` has passed
|
||||
"""
|
||||
@@ -218,59 +229,61 @@ class Subscription(Document):
|
||||
if not end_date:
|
||||
return True
|
||||
|
||||
return getdate(frappe.flags.current_date) > getdate(end_date)
|
||||
end_date = getdate(end_date)
|
||||
return getdate() > getdate(end_date)
|
||||
|
||||
def get_status_for_past_grace_period(self) -> str:
|
||||
cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace"))
|
||||
status = "Unpaid"
|
||||
|
||||
if cancel_after_grace:
|
||||
status = "Cancelled"
|
||||
|
||||
return status
|
||||
|
||||
def is_past_grace_period(self) -> bool:
|
||||
def is_past_grace_period(self):
|
||||
"""
|
||||
Returns `True` if the grace period for the `Subscription` has passed
|
||||
"""
|
||||
if not self.current_invoice_is_past_due():
|
||||
return
|
||||
current_invoice = self.get_current_invoice()
|
||||
if self.current_invoice_is_past_due(current_invoice):
|
||||
subscription_settings = frappe.get_single("Subscription Settings")
|
||||
grace_period = cint(subscription_settings.grace_period)
|
||||
|
||||
grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period"))
|
||||
return getdate(frappe.flags.current_date) >= getdate(
|
||||
add_days(self.current_invoice.due_date, grace_period)
|
||||
)
|
||||
return getdate() > add_days(current_invoice.due_date, grace_period)
|
||||
|
||||
def current_invoice_is_past_due(self) -> bool:
|
||||
def current_invoice_is_past_due(self, current_invoice=None):
|
||||
"""
|
||||
Returns `True` if the current generated invoice is overdue
|
||||
"""
|
||||
if not self.current_invoice or self.is_paid(self.current_invoice):
|
||||
if not current_invoice:
|
||||
current_invoice = self.get_current_invoice()
|
||||
|
||||
if not current_invoice or self.is_paid(current_invoice):
|
||||
return False
|
||||
else:
|
||||
return getdate() > getdate(current_invoice.due_date)
|
||||
|
||||
return getdate(frappe.flags.current_date) >= getdate(self.current_invoice.due_date)
|
||||
def get_current_invoice(self):
|
||||
"""
|
||||
Returns the most recent generated invoice.
|
||||
"""
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
@property
|
||||
def invoice_document_type(self) -> str:
|
||||
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
if len(self.invoices):
|
||||
current = self.invoices[-1]
|
||||
if frappe.db.exists(doctype, current.get("invoice")):
|
||||
doc = frappe.get_doc(doctype, current.get("invoice"))
|
||||
return doc
|
||||
else:
|
||||
frappe.throw(_("Invoice {0} no longer exists").format(current.get("invoice")))
|
||||
|
||||
def is_new_subscription(self) -> bool:
|
||||
def is_new_subscription(self):
|
||||
"""
|
||||
Returns `True` if `Subscription` has never generated an invoice
|
||||
"""
|
||||
return self.is_new() or not frappe.db.exists(
|
||||
{"doctype": self.invoice_document_type, "subscription": self.name}
|
||||
)
|
||||
return len(self.invoices) == 0
|
||||
|
||||
def validate(self) -> None:
|
||||
def validate(self):
|
||||
self.validate_trial_period()
|
||||
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
||||
self.validate_end_date()
|
||||
self.validate_to_follow_calendar_months()
|
||||
if not self.cost_center:
|
||||
self.cost_center = get_default_cost_center(self.get("company"))
|
||||
self.cost_center = erpnext.get_default_cost_center(self.get("company"))
|
||||
|
||||
def validate_trial_period(self) -> None:
|
||||
def validate_trial_period(self):
|
||||
"""
|
||||
Runs sanity checks on trial period dates for the `Subscription`
|
||||
"""
|
||||
@@ -284,7 +297,7 @@ class Subscription(Document):
|
||||
if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date):
|
||||
frappe.throw(_("Trial Period Start date cannot be after Subscription Start Date"))
|
||||
|
||||
def validate_end_date(self) -> None:
|
||||
def validate_end_date(self):
|
||||
billing_cycle_info = self.get_billing_cycle_data()
|
||||
end_date = add_to_date(self.start_date, **billing_cycle_info)
|
||||
|
||||
@@ -293,53 +306,53 @@ class Subscription(Document):
|
||||
_("Subscription End Date must be after {0} as per the subscription plan").format(end_date)
|
||||
)
|
||||
|
||||
def validate_to_follow_calendar_months(self) -> None:
|
||||
if not self.follow_calendar_months:
|
||||
return
|
||||
def validate_to_follow_calendar_months(self):
|
||||
if self.follow_calendar_months:
|
||||
billing_info = self.get_billing_cycle_and_interval()
|
||||
|
||||
billing_info = self.get_billing_cycle_and_interval()
|
||||
if not self.end_date:
|
||||
frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
|
||||
|
||||
if not self.end_date:
|
||||
frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
|
||||
if billing_info[0]["billing_interval"] != "Month":
|
||||
frappe.throw(
|
||||
_("Billing Interval in Subscription Plan must be Month to follow calendar months")
|
||||
)
|
||||
|
||||
if billing_info[0]["billing_interval"] != "Month":
|
||||
frappe.throw(_("Billing Interval in Subscription Plan must be Month to follow calendar months"))
|
||||
|
||||
def after_insert(self) -> None:
|
||||
def after_insert(self):
|
||||
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
|
||||
self.set_subscription_status()
|
||||
|
||||
def generate_invoice(
|
||||
self,
|
||||
from_date: Optional[Union[str, datetime.date]] = None,
|
||||
to_date: Optional[Union[str, datetime.date]] = None,
|
||||
) -> Document:
|
||||
def generate_invoice(self, prorate=0):
|
||||
"""
|
||||
Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
|
||||
saves the `Subscription`.
|
||||
Backwards compatibility
|
||||
"""
|
||||
return self.create_invoice(from_date=from_date, to_date=to_date)
|
||||
|
||||
def create_invoice(
|
||||
self,
|
||||
from_date: Optional[Union[str, datetime.date]] = None,
|
||||
to_date: Optional[Union[str, datetime.date]] = None,
|
||||
) -> Document:
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
invoice = self.create_invoice(prorate)
|
||||
self.append("invoices", {"document_type": doctype, "invoice": invoice.name})
|
||||
|
||||
self.save()
|
||||
|
||||
return invoice
|
||||
|
||||
def create_invoice(self, prorate):
|
||||
"""
|
||||
Creates a `Invoice`, submits it and returns it
|
||||
"""
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
invoice = frappe.new_doc(doctype)
|
||||
|
||||
# For backward compatibility
|
||||
# Earlier subscription didn't had any company field
|
||||
company = self.get("company") or get_default_company()
|
||||
if not company:
|
||||
# fmt: off
|
||||
frappe.throw(
|
||||
_("Company is mandatory was generating invoice. Please set default company in Global Defaults.")
|
||||
_("Company is mandatory was generating invoice. Please set default company in Global Defaults")
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
invoice = frappe.new_doc(self.invoice_document_type)
|
||||
invoice.company = company
|
||||
invoice.set_posting_time = 1
|
||||
invoice.posting_date = (
|
||||
@@ -350,17 +363,17 @@ class Subscription(Document):
|
||||
|
||||
invoice.cost_center = self.cost_center
|
||||
|
||||
if self.invoice_document_type == "Sales Invoice":
|
||||
if doctype == "Sales Invoice":
|
||||
invoice.customer = self.party
|
||||
else:
|
||||
invoice.supplier = self.party
|
||||
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
|
||||
invoice.apply_tds = 1
|
||||
|
||||
# Add party currency to invoice
|
||||
### Add party currency to invoice
|
||||
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
|
||||
|
||||
# Add dimensions in invoice for subscription:
|
||||
## Add dimensions in invoice for subscription:
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
@@ -369,7 +382,7 @@ class Subscription(Document):
|
||||
|
||||
# Subscription is better suited for service items. I won't update `update_stock`
|
||||
# for that reason
|
||||
items_list = self.get_items_from_plans(self.plans, is_prorate())
|
||||
items_list = self.get_items_from_plans(self.plans, prorate)
|
||||
for item in items_list:
|
||||
item["cost_center"] = self.cost_center
|
||||
invoice.append("items", item)
|
||||
@@ -377,9 +390,9 @@ class Subscription(Document):
|
||||
# Taxes
|
||||
tax_template = ""
|
||||
|
||||
if self.invoice_document_type == "Sales Invoice" and self.sales_tax_template:
|
||||
if doctype == "Sales Invoice" and self.sales_tax_template:
|
||||
tax_template = self.sales_tax_template
|
||||
if self.invoice_document_type == "Purchase Invoice" and self.purchase_tax_template:
|
||||
if doctype == "Purchase Invoice" and self.purchase_tax_template:
|
||||
tax_template = self.purchase_tax_template
|
||||
|
||||
if tax_template:
|
||||
@@ -411,9 +424,8 @@ class Subscription(Document):
|
||||
invoice.apply_discount_on = discount_on if discount_on else "Grand Total"
|
||||
|
||||
# Subscription period
|
||||
invoice.subscription = self.name
|
||||
invoice.from_date = from_date or self.current_invoice_start
|
||||
invoice.to_date = to_date or self.current_invoice_end
|
||||
invoice.from_date = self.current_invoice_start
|
||||
invoice.to_date = self.current_invoice_end
|
||||
|
||||
invoice.flags.ignore_mandatory = True
|
||||
|
||||
@@ -425,20 +437,13 @@ class Subscription(Document):
|
||||
|
||||
return invoice
|
||||
|
||||
def get_items_from_plans(
|
||||
self, plans: List[Dict[str, str]], prorate: Optional[bool] = None
|
||||
) -> List[Dict]:
|
||||
def get_items_from_plans(self, plans, prorate=0):
|
||||
"""
|
||||
Returns the `Item`s linked to `Subscription Plan`
|
||||
"""
|
||||
if prorate is None:
|
||||
prorate = False
|
||||
|
||||
if prorate:
|
||||
prorate_factor = get_prorata_factor(
|
||||
self.current_invoice_end,
|
||||
self.current_invoice_start,
|
||||
cint(self.generate_invoice_at_period_start),
|
||||
self.current_invoice_end, self.current_invoice_start, self.generate_invoice_at_period_start
|
||||
)
|
||||
|
||||
items = []
|
||||
@@ -460,11 +465,7 @@ class Subscription(Document):
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
plan.plan, plan.qty, party, self.current_invoice_start, self.current_invoice_end
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
@@ -502,184 +503,254 @@ class Subscription(Document):
|
||||
|
||||
return items
|
||||
|
||||
@frappe.whitelist()
|
||||
def process(self) -> bool:
|
||||
def process(self):
|
||||
"""
|
||||
To be called by task periodically. It checks the subscription and takes appropriate action
|
||||
as need be. It calls either of these methods depending the `Subscription` status:
|
||||
1. `process_for_active`
|
||||
2. `process_for_past_due`
|
||||
"""
|
||||
if (
|
||||
not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
|
||||
and self.can_generate_new_invoice()
|
||||
):
|
||||
self.generate_invoice()
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
if self.cancel_at_period_end and (
|
||||
getdate(frappe.flags.current_date) >= getdate(self.current_invoice_end)
|
||||
or getdate(frappe.flags.current_date) >= getdate(self.end_date)
|
||||
):
|
||||
self.cancel_subscription()
|
||||
if self.status == "Active":
|
||||
self.process_for_active()
|
||||
elif self.status in ["Past Due Date", "Unpaid"]:
|
||||
self.process_for_past_due_date()
|
||||
|
||||
self.set_subscription_status()
|
||||
|
||||
self.save()
|
||||
|
||||
def can_generate_new_invoice(self) -> bool:
|
||||
if self.cancelation_date:
|
||||
return False
|
||||
elif self.generate_invoice_at_period_start and (
|
||||
getdate(frappe.flags.current_date) == getdate(self.current_invoice_start)
|
||||
or self.is_new_subscription()
|
||||
):
|
||||
return True
|
||||
elif getdate(frappe.flags.current_date) == getdate(self.current_invoice_end):
|
||||
if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
|
||||
return False
|
||||
def is_postpaid_to_invoice(self):
|
||||
return getdate() > getdate(self.current_invoice_end) or (
|
||||
getdate() >= getdate(self.current_invoice_end)
|
||||
and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)
|
||||
)
|
||||
|
||||
return True
|
||||
else:
|
||||
def is_prepaid_to_invoice(self):
|
||||
if not self.generate_invoice_at_period_start:
|
||||
return False
|
||||
|
||||
def is_current_invoice_generated(
|
||||
self,
|
||||
_current_start_date: Union[datetime.date, str] = None,
|
||||
_current_end_date: Union[datetime.date, str] = None,
|
||||
) -> bool:
|
||||
if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
|
||||
return True
|
||||
|
||||
# Check invoice dates and make sure it doesn't have outstanding invoices
|
||||
return getdate() >= getdate(self.current_invoice_start)
|
||||
|
||||
def is_current_invoice_generated(self, _current_start_date=None, _current_end_date=None):
|
||||
invoice = self.get_current_invoice()
|
||||
|
||||
if not (_current_start_date and _current_end_date):
|
||||
_current_start_date, _current_end_date = self._get_subscription_period(
|
||||
date=add_days(self.current_invoice_end, 1)
|
||||
_current_start_date, _current_end_date = self.update_subscription_period(
|
||||
date=add_days(self.current_invoice_end, 1), return_date=True
|
||||
)
|
||||
|
||||
if self.current_invoice and getdate(_current_start_date) <= getdate(
|
||||
self.current_invoice.posting_date
|
||||
) <= getdate(_current_end_date):
|
||||
if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(
|
||||
_current_end_date
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_invoice(self) -> Union[Document, None]:
|
||||
def process_for_active(self):
|
||||
"""
|
||||
Adds property for accessing the current_invoice
|
||||
"""
|
||||
return self.get_current_invoice()
|
||||
Called by `process` if the status of the `Subscription` is 'Active'.
|
||||
|
||||
def get_current_invoice(self) -> Union[Document, None]:
|
||||
The possible outcomes of this method are:
|
||||
1. Generate a new invoice
|
||||
2. Change the `Subscription` status to 'Past Due Date'
|
||||
3. Change the `Subscription` status to 'Cancelled'
|
||||
"""
|
||||
Returns the most recent generated invoice.
|
||||
"""
|
||||
invoice = frappe.get_all(
|
||||
self.invoice_document_type,
|
||||
{
|
||||
"subscription": self.name,
|
||||
},
|
||||
limit=1,
|
||||
order_by="to_date desc",
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if invoice:
|
||||
return frappe.get_doc(self.invoice_document_type, invoice[0])
|
||||
if not self.is_current_invoice_generated(
|
||||
self.current_invoice_start, self.current_invoice_end
|
||||
) and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
|
||||
def cancel_subscription_at_period_end(self) -> None:
|
||||
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
|
||||
self.generate_invoice(prorate)
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
|
||||
self.cancel_subscription_at_period_end()
|
||||
|
||||
def cancel_subscription_at_period_end(self):
|
||||
"""
|
||||
Called when `Subscription.cancel_at_period_end` is truthy
|
||||
"""
|
||||
self.status = "Cancelled"
|
||||
self.cancelation_date = nowdate()
|
||||
if self.end_date and getdate() < getdate(self.end_date):
|
||||
return
|
||||
|
||||
@property
|
||||
def invoices(self) -> List[Dict]:
|
||||
return frappe.get_all(
|
||||
self.invoice_document_type,
|
||||
filters={"subscription": self.name},
|
||||
order_by="from_date asc",
|
||||
)
|
||||
self.status = "Cancelled"
|
||||
if not self.cancelation_date:
|
||||
self.cancelation_date = nowdate()
|
||||
|
||||
def process_for_past_due_date(self):
|
||||
"""
|
||||
Called by `process` if the status of the `Subscription` is 'Past Due Date'.
|
||||
|
||||
The possible outcomes of this method are:
|
||||
1. Change the `Subscription` status to 'Active'
|
||||
2. Change the `Subscription` status to 'Cancelled'
|
||||
3. Change the `Subscription` status to 'Unpaid'
|
||||
"""
|
||||
current_invoice = self.get_current_invoice()
|
||||
if not current_invoice:
|
||||
frappe.throw(_("Current invoice {0} is missing").format(current_invoice.invoice))
|
||||
else:
|
||||
if not self.has_outstanding_invoice():
|
||||
self.status = "Active"
|
||||
else:
|
||||
self.set_status_grace_period()
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
# Generate invoices periodically even if current invoice are unpaid
|
||||
if (
|
||||
self.generate_new_invoices_past_due_date
|
||||
and not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
|
||||
and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice())
|
||||
):
|
||||
|
||||
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
|
||||
self.generate_invoice(prorate)
|
||||
|
||||
@staticmethod
|
||||
def is_paid(invoice: Document) -> bool:
|
||||
def is_paid(invoice):
|
||||
"""
|
||||
Return `True` if the given invoice is paid
|
||||
"""
|
||||
return invoice.status == "Paid"
|
||||
|
||||
def has_outstanding_invoice(self) -> int:
|
||||
def has_outstanding_invoice(self):
|
||||
"""
|
||||
Returns `True` if the most recent invoice for the `Subscription` is not paid
|
||||
"""
|
||||
return frappe.db.count(
|
||||
self.invoice_document_type,
|
||||
{
|
||||
"subscription": self.name,
|
||||
"status": ["!=", "Paid"],
|
||||
},
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
current_invoice = self.get_current_invoice()
|
||||
invoice_list = [d.invoice for d in self.invoices]
|
||||
|
||||
outstanding_invoices = frappe.get_all(
|
||||
doctype, fields=["name"], filters={"status": ("!=", "Paid"), "name": ("in", invoice_list)}
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_subscription(self) -> None:
|
||||
if outstanding_invoices:
|
||||
return True
|
||||
else:
|
||||
False
|
||||
|
||||
def cancel_subscription(self):
|
||||
"""
|
||||
This sets the subscription as cancelled. It will stop invoices from being generated
|
||||
but it will not affect already created invoices.
|
||||
"""
|
||||
if self.status == "Cancelled":
|
||||
frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
|
||||
if self.status != "Cancelled":
|
||||
to_generate_invoice = (
|
||||
True if self.status == "Active" and not self.generate_invoice_at_period_start else False
|
||||
)
|
||||
to_prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
|
||||
self.status = "Cancelled"
|
||||
self.cancelation_date = nowdate()
|
||||
if to_generate_invoice:
|
||||
self.generate_invoice(prorate=to_prorate)
|
||||
self.save()
|
||||
|
||||
to_generate_invoice = (
|
||||
True if self.status == "Active" and not self.generate_invoice_at_period_start else False
|
||||
)
|
||||
self.status = "Cancelled"
|
||||
self.cancelation_date = nowdate()
|
||||
|
||||
if to_generate_invoice:
|
||||
self.generate_invoice(self.current_invoice_start, self.cancelation_date)
|
||||
|
||||
self.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def restart_subscription(self) -> None:
|
||||
def restart_subscription(self):
|
||||
"""
|
||||
This sets the subscription as active. The subscription will be made to be like a new
|
||||
subscription and the `Subscription` will lose all the history of generated invoices
|
||||
it has.
|
||||
"""
|
||||
if not self.status == "Cancelled":
|
||||
frappe.throw(_("You cannot restart a Subscription that is not cancelled."), InvoiceNotCancelled)
|
||||
if self.status == "Cancelled":
|
||||
self.status = "Active"
|
||||
self.db_set("start_date", nowdate())
|
||||
self.update_subscription_period(nowdate())
|
||||
self.invoices = []
|
||||
self.save()
|
||||
else:
|
||||
frappe.throw(_("You cannot restart a Subscription that is not cancelled."))
|
||||
|
||||
self.status = "Active"
|
||||
self.cancelation_date = None
|
||||
self.update_subscription_period(frappe.flags.current_date or nowdate())
|
||||
self.save()
|
||||
def get_precision(self):
|
||||
invoice = self.get_current_invoice()
|
||||
if invoice:
|
||||
return invoice.precision("grand_total")
|
||||
|
||||
|
||||
def is_prorate() -> int:
|
||||
return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))
|
||||
def get_calendar_months(billing_interval):
|
||||
calendar_months = []
|
||||
start = 0
|
||||
while start < 12:
|
||||
start += billing_interval
|
||||
calendar_months.append(start)
|
||||
|
||||
return calendar_months
|
||||
|
||||
|
||||
def get_prorata_factor(
|
||||
period_end: Union[datetime.date, str],
|
||||
period_start: Union[datetime.date, str],
|
||||
is_prepaid: Optional[int] = None,
|
||||
) -> Union[int, float]:
|
||||
def get_prorata_factor(period_end, period_start, is_prepaid):
|
||||
if is_prepaid:
|
||||
return 1
|
||||
prorate_factor = 1
|
||||
else:
|
||||
diff = flt(date_diff(nowdate(), period_start) + 1)
|
||||
plan_days = flt(date_diff(period_end, period_start) + 1)
|
||||
prorate_factor = diff / plan_days
|
||||
|
||||
diff = flt(date_diff(nowdate(), period_start) + 1)
|
||||
plan_days = flt(date_diff(period_end, period_start) + 1)
|
||||
return diff / plan_days
|
||||
return prorate_factor
|
||||
|
||||
|
||||
def process_all() -> None:
|
||||
def process_all():
|
||||
"""
|
||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||
"""
|
||||
for subscription in frappe.get_all("Subscription", {"status": ("!=", "Cancelled")}, pluck="name"):
|
||||
subscriptions = get_all_subscriptions()
|
||||
for subscription in subscriptions:
|
||||
process(subscription)
|
||||
|
||||
|
||||
def get_all_subscriptions():
|
||||
"""
|
||||
Returns all `Subscription` documents
|
||||
"""
|
||||
return frappe.db.get_all("Subscription", {"status": ("!=", "Cancelled")})
|
||||
|
||||
|
||||
def process(data):
|
||||
"""
|
||||
Checks a `Subscription` and updates it status as necessary
|
||||
"""
|
||||
if data:
|
||||
try:
|
||||
subscription = frappe.get_doc("Subscription", subscription)
|
||||
subscription = frappe.get_doc("Subscription", data["name"])
|
||||
subscription.process()
|
||||
frappe.db.commit()
|
||||
except frappe.ValidationError:
|
||||
frappe.db.rollback()
|
||||
subscription.log_error("Subscription failed")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_subscription(name):
|
||||
"""
|
||||
Cancels a `Subscription`. This will stop the `Subscription` from further invoicing the
|
||||
`Subscriber` but all already outstanding invoices will not be affected.
|
||||
"""
|
||||
subscription = frappe.get_doc("Subscription", name)
|
||||
subscription.cancel_subscription()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def restart_subscription(name):
|
||||
"""
|
||||
Restarts a cancelled `Subscription`. The `Subscription` will 'forget' the history of
|
||||
all invoices it has generated
|
||||
"""
|
||||
subscription = frappe.get_doc("Subscription", name)
|
||||
subscription.restart_subscription()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_subscription_updates(name):
|
||||
"""
|
||||
Use this to get the latest state of the given `Subscription`
|
||||
"""
|
||||
subscription = frappe.get_doc("Subscription", name)
|
||||
subscription.process()
|
||||
|
||||
@@ -11,7 +11,6 @@ from frappe.utils.data import (
|
||||
date_diff,
|
||||
flt,
|
||||
get_date_str,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
@@ -91,18 +90,10 @@ def create_parties():
|
||||
customer.insert()
|
||||
|
||||
|
||||
def reset_settings():
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
settings.grace_period = 0
|
||||
settings.cancel_after_grace = 0
|
||||
settings.save()
|
||||
|
||||
|
||||
class TestSubscription(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_plan()
|
||||
create_parties()
|
||||
reset_settings()
|
||||
|
||||
def test_create_subscription_with_trial_with_correct_period(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
@@ -125,6 +116,8 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(subscription.invoices, [])
|
||||
self.assertEqual(subscription.status, "Trialling")
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_create_subscription_without_trial_with_correct_period(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Customer"
|
||||
@@ -140,6 +133,8 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_create_subscription_trial_with_wrong_dates(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Customer"
|
||||
@@ -149,6 +144,7 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, subscription.save)
|
||||
subscription.delete()
|
||||
|
||||
def test_create_subscription_multi_with_different_billing_fails(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
@@ -160,6 +156,7 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, subscription.save)
|
||||
subscription.delete()
|
||||
|
||||
def test_invoice_is_generated_at_end_of_billing_period(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
@@ -172,13 +169,13 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
self.assertEqual(subscription.current_invoice_start, "2018-01-01")
|
||||
self.assertEqual(subscription.current_invoice_end, "2018-01-31")
|
||||
frappe.flags.current_date = "2018-01-31"
|
||||
subscription.process()
|
||||
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.current_invoice_start, "2018-02-01")
|
||||
self.assertEqual(subscription.current_invoice_end, "2018-02-28")
|
||||
self.assertEqual(subscription.current_invoice_start, "2018-01-01")
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
subscription.delete()
|
||||
|
||||
def test_status_goes_back_to_active_after_invoice_is_paid(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
@@ -186,9 +183,7 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.party = "_Test Customer"
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
subscription.start_date = "2018-01-01"
|
||||
subscription.generate_invoice_at_period_start = True
|
||||
subscription.insert()
|
||||
frappe.flags.current_date = "2018-01-01"
|
||||
subscription.process() # generate first invoice
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
@@ -208,8 +203,11 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1))
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_cancel_after_grace_period(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
default_grace_period_action = settings.cancel_after_grace
|
||||
settings.cancel_after_grace = 1
|
||||
settings.save()
|
||||
|
||||
@@ -217,18 +215,20 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.party_type = "Customer"
|
||||
subscription.party = "_Test Customer"
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
# subscription.generate_invoice_at_period_start = True
|
||||
subscription.start_date = "2018-01-01"
|
||||
subscription.insert()
|
||||
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
|
||||
frappe.flags.current_date = "2018-01-31"
|
||||
subscription.process() # generate first invoice
|
||||
# This should change status to Cancelled since grace period is 0
|
||||
# And is backdated subscription so subscription will be cancelled after processing
|
||||
self.assertEqual(subscription.status, "Cancelled")
|
||||
|
||||
settings.cancel_after_grace = default_grace_period_action
|
||||
settings.save()
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_unpaid_after_grace_period(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
default_grace_period_action = settings.cancel_after_grace
|
||||
@@ -248,26 +248,21 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
settings.cancel_after_grace = default_grace_period_action
|
||||
settings.save()
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_invoice_days_until_due(self):
|
||||
_date = add_months(nowdate(), -1)
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Customer"
|
||||
subscription.party = "_Test Customer"
|
||||
subscription.days_until_due = 10
|
||||
subscription.start_date = _date
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
subscription.days_until_due = 10
|
||||
subscription.start_date = add_months(nowdate(), -1)
|
||||
subscription.insert()
|
||||
|
||||
frappe.flags.current_date = subscription.current_invoice_end
|
||||
|
||||
subscription.process() # generate first invoice
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
|
||||
frappe.flags.current_date = add_days(subscription.current_invoice_end, 3)
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_is_past_due_doesnt_change_within_grace_period(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
@@ -281,8 +276,6 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
subscription.start_date = add_days(nowdate(), -1000)
|
||||
subscription.insert()
|
||||
|
||||
frappe.flags.current_date = subscription.current_invoice_end
|
||||
subscription.process() # generate first invoice
|
||||
|
||||
self.assertEqual(subscription.status, "Past Due Date")
|
||||
@@ -299,6 +292,7 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
settings.grace_period = grace_period
|
||||
settings.save()
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_remains_active_during_invoice_period(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
@@ -325,6 +319,8 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_cancelation(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Customer"
|
||||
@@ -335,6 +331,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
self.assertEqual(subscription.status, "Cancelled")
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_cancellation_invoices(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
to_prorate = settings.prorate
|
||||
@@ -374,6 +372,7 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2))
|
||||
self.assertEqual(subscription.status, "Cancelled")
|
||||
|
||||
subscription.delete()
|
||||
settings.prorate = to_prorate
|
||||
settings.save()
|
||||
|
||||
@@ -396,6 +395,8 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.prorate = to_prorate
|
||||
settings.save()
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_cancellation_invoices_with_prorata_true(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
to_prorate = settings.prorate
|
||||
@@ -421,6 +422,8 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.prorate = to_prorate
|
||||
settings.save()
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_subcription_cancellation_and_process(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
default_grace_period_action = settings.cancel_after_grace
|
||||
@@ -434,22 +437,23 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.start_date = "2018-01-01"
|
||||
subscription.insert()
|
||||
subscription.process() # generate first invoice
|
||||
invoices = len(subscription.invoices)
|
||||
|
||||
# Generate an invoice for the cancelled period
|
||||
subscription.cancel_subscription()
|
||||
self.assertEqual(subscription.status, "Cancelled")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(len(subscription.invoices), invoices)
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, "Cancelled")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(len(subscription.invoices), invoices)
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, "Cancelled")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(len(subscription.invoices), invoices)
|
||||
|
||||
settings.cancel_after_grace = default_grace_period_action
|
||||
settings.save()
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_restart_and_process(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
@@ -464,7 +468,6 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
subscription.start_date = "2018-01-01"
|
||||
subscription.insert()
|
||||
frappe.flags.current_date = "2018-01-31"
|
||||
subscription.process() # generate first invoice
|
||||
|
||||
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||
@@ -475,18 +478,19 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
subscription.restart_subscription()
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
settings.cancel_after_grace = default_grace_period_action
|
||||
settings.save()
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_unpaid_back_to_active(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
@@ -499,11 +503,8 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.party = "_Test Customer"
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
subscription.start_date = "2018-01-01"
|
||||
subscription.generate_invoice_at_period_start = True
|
||||
subscription.insert()
|
||||
|
||||
frappe.flags.current_date = subscription.current_invoice_start
|
||||
|
||||
subscription.process() # generate first invoice
|
||||
# This should change status to Unpaid since grace period is 0
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
@@ -516,12 +517,12 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(subscription.status, "Active")
|
||||
|
||||
# A new invoice is generated
|
||||
frappe.flags.current_date = subscription.current_invoice_start
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
settings.cancel_after_grace = default_grace_period_action
|
||||
settings.save()
|
||||
subscription.delete()
|
||||
|
||||
def test_restart_active_subscription(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
@@ -532,6 +533,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_invoice_discount_percentage(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Customer"
|
||||
@@ -546,6 +549,8 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(invoice.additional_discount_percentage, 10)
|
||||
self.assertEqual(invoice.apply_discount_on, "Grand Total")
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_invoice_discount_amount(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Customer"
|
||||
@@ -560,6 +565,8 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(invoice.discount_amount, 11)
|
||||
self.assertEqual(invoice.apply_discount_on, "Grand Total")
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_prepaid_subscriptions(self):
|
||||
# Create a non pre-billed subscription, processing should not create
|
||||
# invoices.
|
||||
@@ -607,6 +614,8 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.prorate = to_prorate
|
||||
settings.save()
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_subscription_with_follow_calendar_months(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Supplier"
|
||||
@@ -614,14 +623,14 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
subscription.follow_calendar_months = 1
|
||||
|
||||
# select subscription start date as "2018-01-15"
|
||||
# select subscription start date as '2018-01-15'
|
||||
subscription.start_date = "2018-01-15"
|
||||
subscription.end_date = "2018-07-15"
|
||||
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
|
||||
subscription.save()
|
||||
|
||||
# even though subscription starts at "2018-01-15" and Billing interval is Month and count 3
|
||||
# First invoice will end at "2018-03-31" instead of "2018-04-14"
|
||||
# even though subscription starts at '2018-01-15' and Billing interval is Month and count 3
|
||||
# First invoice will end at '2018-03-31' instead of '2018-04-14'
|
||||
self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
|
||||
|
||||
def test_subscription_generate_invoice_past_due(self):
|
||||
@@ -630,12 +639,11 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.party = "_Test Supplier"
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
subscription.generate_new_invoices_past_due_date = 1
|
||||
# select subscription start date as "2018-01-15"
|
||||
# select subscription start date as '2018-01-15'
|
||||
subscription.start_date = "2018-01-01"
|
||||
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
|
||||
subscription.save()
|
||||
|
||||
frappe.flags.current_date = "2018-01-01"
|
||||
# Process subscription and create first invoice
|
||||
# Subscription status will be unpaid since due date has already passed
|
||||
subscription.process()
|
||||
@@ -644,8 +652,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
# Now the Subscription is unpaid
|
||||
# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
|
||||
# subscription and the interval between the subscriptions is 3 months
|
||||
frappe.flags.current_date = "2018-04-01"
|
||||
# subscription
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(len(subscription.invoices), 2)
|
||||
|
||||
@@ -654,7 +662,7 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.party_type = "Supplier"
|
||||
subscription.party = "_Test Supplier"
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
# select subscription start date as "2018-01-15"
|
||||
# select subscription start date as '2018-01-15'
|
||||
subscription.start_date = "2018-01-01"
|
||||
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
|
||||
subscription.save()
|
||||
@@ -674,7 +682,7 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.party = "_Test Subscription Customer"
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
subscription.company = "_Test Company"
|
||||
# select subscription start date as "2018-01-15"
|
||||
# select subscription start date as '2018-01-15'
|
||||
subscription.start_date = "2018-01-01"
|
||||
subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1})
|
||||
subscription.save()
|
||||
@@ -684,47 +692,5 @@ class TestSubscription(unittest.TestCase):
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
# Check the currency of the created invoice
|
||||
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
|
||||
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
|
||||
self.assertEqual(currency, "USD")
|
||||
|
||||
def test_subscription_recovery(self):
|
||||
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Customer"
|
||||
subscription.party = "_Test Subscription Customer"
|
||||
subscription.company = "_Test Company"
|
||||
subscription.start_date = "2021-12-01"
|
||||
subscription.generate_new_invoices_past_due_date = 1
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
subscription.submit_invoice = 0
|
||||
subscription.save()
|
||||
|
||||
# create invoices for the first two moths
|
||||
frappe.flags.current_date = "2021-12-31"
|
||||
subscription.process()
|
||||
|
||||
frappe.flags.current_date = "2022-01-31"
|
||||
subscription.process()
|
||||
|
||||
self.assertEqual(len(subscription.invoices), 2)
|
||||
self.assertEqual(
|
||||
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
|
||||
getdate("2021-12-01"),
|
||||
)
|
||||
self.assertEqual(
|
||||
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
|
||||
getdate("2022-01-01"),
|
||||
)
|
||||
|
||||
# recreate most recent invoice
|
||||
subscription.process()
|
||||
|
||||
self.assertEqual(len(subscription.invoices), 2)
|
||||
self.assertEqual(
|
||||
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
|
||||
getdate("2021-12-01"),
|
||||
)
|
||||
self.assertEqual(
|
||||
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
|
||||
getdate("2022-01-01"),
|
||||
)
|
||||
|
||||
@@ -100,14 +100,11 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company)
|
||||
|
||||
if not tax_details:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Skipping Tax Withholding Category {0} as there is no associated account set for Company {1} in it."
|
||||
).format(tax_withholding_category, inv.company)
|
||||
frappe.throw(
|
||||
_("Please set associated account in Tax Withholding Category {0} against Company {1}").format(
|
||||
tax_withholding_category, inv.company
|
||||
)
|
||||
)
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return {}, [], {}
|
||||
return {}
|
||||
|
||||
if party_type == "Customer" and not tax_details.cumulative_threshold:
|
||||
# TCS is only chargeable on sum of invoiced value
|
||||
@@ -265,20 +262,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
if ldc:
|
||||
limit_consumed = get_limit_consumed(ldc, parties)
|
||||
if is_valid_certificate(ldc, posting_date, limit_consumed):
|
||||
tax_amount = get_lower_deduction_amount(
|
||||
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
|
||||
)
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total)
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
|
||||
# once tds is deducted, not need to add vouchers in the invoice
|
||||
voucher_wise_amount = {}
|
||||
else:
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
|
||||
|
||||
elif party_type == "Customer":
|
||||
if tax_deducted:
|
||||
@@ -425,7 +416,7 @@ def get_deducted_tax(taxable_vouchers, tax_details):
|
||||
return sum(entries)
|
||||
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
tds_amount = 0
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
|
||||
@@ -485,12 +476,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
threshold = tax_details.get("threshold", 0)
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.base_tax_withholding_net_total
|
||||
else:
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
if (threshold and inv.tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and supp_credit_amt >= cumulative_threshold
|
||||
):
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
@@ -505,10 +491,15 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
net_total += inv.tax_withholding_net_total
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
tds_amount = get_lower_deduction_amount(
|
||||
supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details
|
||||
)
|
||||
if ldc and is_valid_certificate(
|
||||
ldc.valid_from,
|
||||
ldc.valid_upto,
|
||||
inv.get("posting_date") or inv.get("transaction_date"),
|
||||
tax_deducted,
|
||||
inv.tax_withholding_net_total,
|
||||
ldc.certificate_limit,
|
||||
):
|
||||
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
|
||||
else:
|
||||
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
|
||||
|
||||
@@ -586,7 +577,8 @@ def get_invoice_total_without_tcs(inv, tax_details):
|
||||
return inv.grand_total - tcs_tax_row_amount
|
||||
|
||||
|
||||
def get_limit_consumed(ldc, parties):
|
||||
def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
tds_amount = 0
|
||||
limit_consumed = frappe.db.get_value(
|
||||
"Purchase Invoice",
|
||||
{
|
||||
@@ -600,29 +592,37 @@ def get_limit_consumed(ldc, parties):
|
||||
"sum(tax_withholding_net_total)",
|
||||
)
|
||||
|
||||
return limit_consumed
|
||||
if is_valid_certificate(
|
||||
ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, ldc.certificate_limit
|
||||
):
|
||||
tds_amount = get_ltds_amount(
|
||||
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
|
||||
)
|
||||
|
||||
return tds_amount
|
||||
|
||||
|
||||
def get_lower_deduction_amount(
|
||||
current_amount, limit_consumed, certificate_limit, rate, tax_details
|
||||
):
|
||||
if certificate_limit - flt(limit_consumed) - flt(current_amount) >= 0:
|
||||
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
|
||||
if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
|
||||
return current_amount * rate / 100
|
||||
else:
|
||||
ltds_amount = certificate_limit - flt(limit_consumed)
|
||||
ltds_amount = certificate_limit - flt(deducted_amount)
|
||||
tds_amount = current_amount - ltds_amount
|
||||
|
||||
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
||||
|
||||
|
||||
def is_valid_certificate(ldc, posting_date, limit_consumed):
|
||||
available_amount = flt(ldc.certificate_limit) - flt(limit_consumed)
|
||||
if (
|
||||
getdate(ldc.valid_from) <= getdate(posting_date) <= getdate(ldc.valid_upto)
|
||||
) and available_amount > 0:
|
||||
return True
|
||||
def is_valid_certificate(
|
||||
valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit
|
||||
):
|
||||
valid = False
|
||||
|
||||
return False
|
||||
available_amount = flt(certificate_limit) - flt(deducted_amount)
|
||||
|
||||
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
|
||||
valid = True
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def normal_round(number):
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -18,7 +17,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
# create relevant supplier, etc
|
||||
create_records()
|
||||
create_tax_withholding_category_records()
|
||||
make_pan_no_field()
|
||||
|
||||
def tearDown(self):
|
||||
cancel_invoices()
|
||||
@@ -318,42 +316,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in reversed(orders):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True)
|
||||
|
||||
# Add some tax on the order
|
||||
order.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 8000,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
order.save()
|
||||
|
||||
order.apply_tds = 1
|
||||
order.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
order.submit()
|
||||
|
||||
self.assertEqual(order.taxes[0].tax_amount, 4000)
|
||||
|
||||
payment = get_payment_entry(order.doctype, order.name)
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
payment.submit()
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 4000)
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
|
||||
@@ -453,40 +415,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
pe2.cancel()
|
||||
pe3.cancel()
|
||||
|
||||
def test_lower_deduction_certificate_application(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
"Test LDC Supplier",
|
||||
{
|
||||
"tax_withholding_category": "Test Service Category",
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
|
||||
create_lower_deduction_certificate(
|
||||
supplier="Test LDC Supplier",
|
||||
certificate_no="1AE0423AAJ",
|
||||
tax_withholding_category="Test Service Category",
|
||||
tax_rate=2,
|
||||
limit=50000,
|
||||
)
|
||||
|
||||
pi1 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
|
||||
pi1.submit()
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 700)
|
||||
|
||||
pi2 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
|
||||
pi2.submit()
|
||||
self.assertEqual(pi2.taxes[0].tax_amount, 2300)
|
||||
|
||||
pi3 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
|
||||
pi3.submit()
|
||||
self.assertEqual(pi3.taxes[0].tax_amount, 3500)
|
||||
|
||||
pi1.cancel()
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all(
|
||||
@@ -645,8 +573,6 @@ def create_records():
|
||||
"Test TDS Supplier5",
|
||||
"Test TDS Supplier6",
|
||||
"Test TDS Supplier7",
|
||||
"Test TDS Supplier8",
|
||||
"Test LDC Supplier",
|
||||
]:
|
||||
if frappe.db.exists("Supplier", name):
|
||||
continue
|
||||
@@ -843,39 +769,3 @@ def create_tax_withholding_category(
|
||||
"accounts": [{"company": "_Test Company", "account": account}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def create_lower_deduction_certificate(
|
||||
supplier, tax_withholding_category, tax_rate, certificate_no, limit
|
||||
):
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Lower Deduction Certificate",
|
||||
"company": "_Test Company",
|
||||
"supplier": supplier,
|
||||
"certificate_no": certificate_no,
|
||||
"tax_withholding_category": tax_withholding_category,
|
||||
"fiscal_year": fiscal_year[0],
|
||||
"valid_from": fiscal_year[1],
|
||||
"valid_upto": fiscal_year[2],
|
||||
"rate": tax_rate,
|
||||
"certificate_limit": limit,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def make_pan_no_field():
|
||||
pan_field = {
|
||||
"Supplier": [
|
||||
{
|
||||
"fieldname": "pan",
|
||||
"label": "PAN",
|
||||
"fieldtype": "Data",
|
||||
"translatable": 0,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
create_custom_fields(pan_field, update=1)
|
||||
|
||||
@@ -14,7 +14,6 @@ from frappe.contacts.doctype.address.address import (
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -707,7 +706,6 @@ def get_payment_terms_template(party_name, party_type, company=None):
|
||||
if party_type not in ("Customer", "Supplier"):
|
||||
return
|
||||
template = None
|
||||
|
||||
if party_type == "Customer":
|
||||
customer = frappe.get_cached_value(
|
||||
"Customer", party_name, fieldname=["payment_terms", "customer_group"], as_dict=1
|
||||
@@ -922,35 +920,32 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None
|
||||
):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.party)
|
||||
.where(
|
||||
(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
|
||||
)
|
||||
.groupby(gle.party)
|
||||
)
|
||||
if account_type == "Receivable":
|
||||
query = query.select(Sum(gle.credit).as_("amount"))
|
||||
else:
|
||||
query = query.select(Sum(gle.debit).as_("amount"))
|
||||
|
||||
cond = "1=1"
|
||||
if posting_date:
|
||||
if future_payment:
|
||||
query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
|
||||
cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
|
||||
else:
|
||||
query = query.where(gle.posting_date <= posting_date)
|
||||
cond = "posting_date <= '{0}'".format(posting_date)
|
||||
|
||||
if company:
|
||||
query = query.where(gle.company == company)
|
||||
cond += "and company = {0}".format(frappe.db.escape(company))
|
||||
|
||||
if party:
|
||||
query = query.where(gle.party == party)
|
||||
cond += "and party = {0}".format(frappe.db.escape(party))
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
data = frappe.db.sql(
|
||||
""" SELECT party, sum({0}) as amount
|
||||
FROM `tabGL Entry`
|
||||
WHERE
|
||||
party_type = %s and against_voucher is null
|
||||
and is_cancelled = 0
|
||||
and {1} GROUP BY party""".format(
|
||||
("credit") if party_type == "Customer" else "debit", cond
|
||||
),
|
||||
party_type,
|
||||
)
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"account_type": "Payable",
|
||||
"party_type": "Supplier",
|
||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||
}
|
||||
return ReceivablePayableReport(filters).run(args)
|
||||
|
||||
@@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"account_type": "Payable",
|
||||
"party_type": "Supplier",
|
||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||
}
|
||||
return AccountsReceivableSummary(filters).run(args)
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.query_builder.functions import Date
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -34,7 +34,7 @@ from erpnext.accounts.utils import get_currency_precision
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"account_type": "Receivable",
|
||||
"party_type": "Customer",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
return ReceivablePayableReport(filters).run(args)
|
||||
@@ -70,11 +70,8 @@ class ReceivablePayableReport(object):
|
||||
"Company", self.filters.get("company"), "default_currency"
|
||||
)
|
||||
self.currency_precision = get_currency_precision() or 2
|
||||
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
|
||||
self.account_type = self.filters.account_type
|
||||
self.party_type = frappe.db.get_all(
|
||||
"Party Type", {"account_type": self.account_type}, pluck="name"
|
||||
)
|
||||
self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
|
||||
self.party_type = self.filters.party_type
|
||||
self.party_details = {}
|
||||
self.invoices = set()
|
||||
self.skip_total_row = 0
|
||||
@@ -200,7 +197,6 @@ class ReceivablePayableReport(object):
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||
|
||||
row.party_type = ple.party_type
|
||||
return row
|
||||
|
||||
def update_voucher_balance(self, ple):
|
||||
@@ -211,9 +207,8 @@ class ReceivablePayableReport(object):
|
||||
return
|
||||
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
if self.filters.get(scrub(self.party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
@@ -367,7 +362,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_invoice_details(self):
|
||||
self.invoice_details = frappe._dict()
|
||||
if self.account_type == "Receivable":
|
||||
if self.party_type == "Customer":
|
||||
si_list = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, po_no
|
||||
@@ -395,7 +390,7 @@ class ReceivablePayableReport(object):
|
||||
d.sales_person
|
||||
)
|
||||
|
||||
if self.account_type == "Payable":
|
||||
if self.party_type == "Supplier":
|
||||
for pi in frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
@@ -426,21 +421,20 @@ class ReceivablePayableReport(object):
|
||||
# customer / supplier name
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
row.currency = row.account_currency
|
||||
break
|
||||
if self.filters.get(scrub(self.filters.party_type)):
|
||||
row.currency = row.account_currency
|
||||
else:
|
||||
row.currency = self.company_currency
|
||||
|
||||
def allocate_outstanding_based_on_payment_terms(self, row):
|
||||
self.get_payment_terms(row)
|
||||
for term in row.payment_terms:
|
||||
# update "paid" and "outstanding" for this term
|
||||
|
||||
# update "paid" and "oustanding" for this term
|
||||
if not term.paid:
|
||||
self.allocate_closing_to_term(row, term, "paid")
|
||||
|
||||
# update "credit_note" and "outstanding" for this term
|
||||
# update "credit_note" and "oustanding" for this term
|
||||
if term.outstanding:
|
||||
self.allocate_closing_to_term(row, term, "credit_note")
|
||||
|
||||
@@ -452,8 +446,7 @@ class ReceivablePayableReport(object):
|
||||
"""
|
||||
select
|
||||
si.name, si.party_account_currency, si.currency, si.conversion_rate,
|
||||
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
|
||||
ps.description, ps.paid_amount, ps.discounted_amount
|
||||
ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
|
||||
from `tab{0}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
@@ -469,10 +462,6 @@ class ReceivablePayableReport(object):
|
||||
original_row = frappe._dict(row)
|
||||
row.payment_terms = []
|
||||
|
||||
# Advance allocated during invoicing is not considered in payment terms
|
||||
# Deduct that from paid amount pre allocation
|
||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||
|
||||
# If no or single payment terms, no need to split the row
|
||||
if len(payment_terms_details) <= 1:
|
||||
return
|
||||
@@ -487,7 +476,7 @@ class ReceivablePayableReport(object):
|
||||
) and d.currency == d.party_account_currency:
|
||||
invoiced = d.payment_amount
|
||||
else:
|
||||
invoiced = d.base_payment_amount
|
||||
invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision)
|
||||
|
||||
row.payment_terms.append(
|
||||
term.update(
|
||||
@@ -543,66 +532,64 @@ class ReceivablePayableReport(object):
|
||||
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
|
||||
|
||||
def get_future_payments_from_payment_entry(self):
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||
return (
|
||||
frappe.qb.from_(pe)
|
||||
.inner_join(pe_ref)
|
||||
.on(pe_ref.parent == pe.name)
|
||||
.select(
|
||||
(pe_ref.reference_name).as_("invoice_no"),
|
||||
pe.party,
|
||||
pe.party_type,
|
||||
(pe.posting_date).as_("future_date"),
|
||||
(pe_ref.allocated_amount).as_("future_amount"),
|
||||
(pe.reference_no).as_("future_ref"),
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus < 2)
|
||||
& (pe.posting_date > self.filters.report_date)
|
||||
& (pe.party_type.isin(self.party_type))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
def get_future_payments_from_journal_entry(self):
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
query = (
|
||||
frappe.qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
jea.reference_name.as_("invoice_no"),
|
||||
jea.party,
|
||||
jea.party_type,
|
||||
je.posting_date.as_("future_date"),
|
||||
je.cheque_no.as_("future_ref"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus < 2)
|
||||
& (je.posting_date > self.filters.report_date)
|
||||
& (jea.party_type.isin(self.party_type))
|
||||
& (jea.reference_name.isnotnull())
|
||||
& (jea.reference_name != "")
|
||||
)
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
ref.reference_name as invoice_no,
|
||||
payment_entry.party,
|
||||
payment_entry.party_type,
|
||||
payment_entry.posting_date as future_date,
|
||||
ref.allocated_amount as future_amount,
|
||||
payment_entry.reference_no as future_ref
|
||||
from
|
||||
`tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref
|
||||
on
|
||||
(ref.parent = payment_entry.name)
|
||||
where
|
||||
payment_entry.docstatus < 2
|
||||
and payment_entry.posting_date > %s
|
||||
and payment_entry.party_type = %s
|
||||
""",
|
||||
(self.filters.report_date, self.party_type),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
def get_future_payments_from_journal_entry(self):
|
||||
if self.filters.get("party"):
|
||||
if self.account_type == "Payable":
|
||||
query = query.select(
|
||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
|
||||
amount_field = (
|
||||
"jea.debit_in_account_currency - jea.credit_in_account_currency"
|
||||
if self.party_type == "Supplier"
|
||||
else "jea.credit_in_account_currency - jea.debit_in_account_currency"
|
||||
)
|
||||
else:
|
||||
amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit"
|
||||
|
||||
query = query.having(qb.Field("future_amount") > 0)
|
||||
return query.run(as_dict=True)
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
jea.reference_name as invoice_no,
|
||||
jea.party,
|
||||
jea.party_type,
|
||||
je.posting_date as future_date,
|
||||
sum('{0}') as future_amount,
|
||||
je.cheque_no as future_ref
|
||||
from
|
||||
`tabJournal Entry` as je inner join `tabJournal Entry Account` as jea
|
||||
on
|
||||
(jea.parent = je.name)
|
||||
where
|
||||
je.docstatus < 2
|
||||
and je.posting_date > %s
|
||||
and jea.party_type = %s
|
||||
and jea.reference_name is not null and jea.reference_name != ''
|
||||
group by je.name, jea.reference_name
|
||||
having future_amount > 0
|
||||
""".format(
|
||||
amount_field
|
||||
),
|
||||
(self.filters.report_date, self.party_type),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
def allocate_future_payments(self, row):
|
||||
# future payments are captured in additional columns
|
||||
@@ -632,17 +619,13 @@ class ReceivablePayableReport(object):
|
||||
row.future_ref = ", ".join(row.future_ref)
|
||||
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||
or_filters = {}
|
||||
for party_type in self.party_type:
|
||||
party_field = scrub(party_type)
|
||||
if self.filters.get(party_field):
|
||||
or_filters.update({party_field: self.filters.get(party_field)})
|
||||
party_field = scrub(self.filters.party_type)
|
||||
if self.filters.get(party_field):
|
||||
filters.update({party_field: self.filters.get(party_field)})
|
||||
self.return_entries = frappe._dict(
|
||||
frappe.get_all(
|
||||
doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1
|
||||
)
|
||||
frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1)
|
||||
)
|
||||
|
||||
def set_ageing(self, row):
|
||||
@@ -733,7 +716,6 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(self.qb_selection_filter))
|
||||
.where(Criterion.any(self.or_filters))
|
||||
)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -764,18 +746,16 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def prepare_conditions(self):
|
||||
self.qb_selection_filter = []
|
||||
self.or_filters = []
|
||||
for party_type in self.party_type:
|
||||
party_type_field = scrub(party_type)
|
||||
self.or_filters.append(self.ple.party_type == party_type)
|
||||
party_type_field = scrub(self.party_type)
|
||||
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
|
||||
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
|
||||
if party_type_field == "customer":
|
||||
self.add_customer_filters()
|
||||
if party_type_field == "customer":
|
||||
self.add_customer_filters()
|
||||
|
||||
elif party_type_field == "supplier":
|
||||
self.add_supplier_filters()
|
||||
elif party_type_field == "supplier":
|
||||
self.add_supplier_filters()
|
||||
|
||||
if self.filters.cost_center:
|
||||
self.get_cost_center_conditions()
|
||||
@@ -804,10 +784,11 @@ class ReceivablePayableReport(object):
|
||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||
else:
|
||||
# get GL with "receivable" or "payable" account_type
|
||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
||||
accounts = [
|
||||
d.name
|
||||
for d in frappe.get_all(
|
||||
"Account", filters={"account_type": self.account_type, "company": self.filters.company}
|
||||
"Account", filters={"account_type": account_type, "company": self.filters.company}
|
||||
)
|
||||
]
|
||||
|
||||
@@ -897,7 +878,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_party_details(self, party):
|
||||
if not party in self.party_details:
|
||||
if self.account_type == "Receivable":
|
||||
if self.party_type == "Customer":
|
||||
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
@@ -920,20 +901,14 @@ class ReceivablePayableReport(object):
|
||||
self.columns = []
|
||||
self.add_column("Posting Date", fieldtype="Date")
|
||||
self.add_column(
|
||||
label="Party Type",
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label="Party",
|
||||
label=_(self.party_type),
|
||||
fieldname="party",
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
fieldtype="Link",
|
||||
options=self.party_type,
|
||||
width=180,
|
||||
)
|
||||
self.add_column(
|
||||
label=self.account_type + " Account",
|
||||
label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
|
||||
fieldname="party_account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
@@ -941,19 +916,13 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
if self.account_type == "Payable":
|
||||
label = "Supplier Name"
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
label = "Customer Name"
|
||||
fieldname = "customer_name"
|
||||
self.add_column(
|
||||
label=label,
|
||||
fieldname=fieldname,
|
||||
_("{0} Name").format(self.party_type),
|
||||
fieldname=scrub(self.party_type) + "_name",
|
||||
fieldtype="Data",
|
||||
)
|
||||
|
||||
if self.account_type == "Receivable":
|
||||
if self.party_type == "Customer":
|
||||
self.add_column(
|
||||
_("Customer Contact"),
|
||||
fieldname="customer_primary_contact",
|
||||
@@ -973,7 +942,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
self.add_column(label="Due Date", fieldtype="Date")
|
||||
|
||||
if self.account_type == "Payable":
|
||||
if self.party_type == "Supplier":
|
||||
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
|
||||
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
|
||||
|
||||
@@ -983,7 +952,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
||||
self.add_column(_("Paid Amount"), fieldname="paid")
|
||||
if self.account_type == "Receivable":
|
||||
if self.party_type == "Customer":
|
||||
self.add_column(_("Credit Note"), fieldname="credit_note")
|
||||
else:
|
||||
# note: fieldname is still `credit_note`
|
||||
@@ -1001,7 +970,7 @@ class ReceivablePayableReport(object):
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||
|
||||
if self.filters.account_type == "Receivable":
|
||||
if self.filters.party_type == "Customer":
|
||||
self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
|
||||
|
||||
# comma separated list of linked delivery notes
|
||||
@@ -1022,7 +991,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.sales_partner:
|
||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
||||
|
||||
if self.filters.account_type == "Payable":
|
||||
if self.filters.party_type == "Supplier":
|
||||
self.add_column(
|
||||
label=_("Supplier Group"),
|
||||
fieldname="supplier_group",
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"account_type": "Receivable",
|
||||
"party_type": "Customer",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
|
||||
@@ -21,10 +21,7 @@ def execute(filters=None):
|
||||
|
||||
class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
def run(self, args):
|
||||
self.account_type = args.get("account_type")
|
||||
self.party_type = frappe.db.get_all(
|
||||
"Party Type", {"account_type": self.account_type}, pluck="name"
|
||||
)
|
||||
self.party_type = args.get("party_type")
|
||||
self.party_naming_by = frappe.db.get_value(
|
||||
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
||||
)
|
||||
@@ -38,19 +35,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
self.get_party_total(args)
|
||||
|
||||
party = None
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
party = self.filters.get(scrub(party_type))
|
||||
|
||||
party_advance_amount = (
|
||||
get_partywise_advanced_payment_amount(
|
||||
self.party_type,
|
||||
self.filters.report_date,
|
||||
self.filters.show_future_payments,
|
||||
self.filters.company,
|
||||
party=party,
|
||||
account_type=self.account_type,
|
||||
party=self.filters.get(scrub(self.party_type)),
|
||||
)
|
||||
or {}
|
||||
)
|
||||
@@ -66,13 +57,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
row.party = party
|
||||
if self.party_naming_by == "Naming Series":
|
||||
if self.account_type == "Payable":
|
||||
doctype = "Supplier"
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
doctype = "Customer"
|
||||
fieldname = "customer_name"
|
||||
row.party_name = frappe.get_cached_value(doctype, party, fieldname)
|
||||
row.party_name = frappe.get_cached_value(
|
||||
self.party_type, party, scrub(self.party_type) + "_name"
|
||||
)
|
||||
|
||||
row.update(party_dict)
|
||||
|
||||
@@ -106,7 +93,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
# set territory, customer_group, sales person etc
|
||||
self.set_party_details(d)
|
||||
self.party_total[d.party].update({"party_type": d.party_type})
|
||||
|
||||
def init_party_total(self, row):
|
||||
self.party_total.setdefault(
|
||||
@@ -145,27 +131,17 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
self.add_column(
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label=_("Party"),
|
||||
label=_(self.party_type),
|
||||
fieldname="party",
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
fieldtype="Link",
|
||||
options=self.party_type,
|
||||
width=180,
|
||||
)
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
self.add_column(
|
||||
label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"),
|
||||
fieldname="party_name",
|
||||
fieldtype="Data",
|
||||
)
|
||||
self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data")
|
||||
|
||||
credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note"
|
||||
credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note"
|
||||
|
||||
self.add_column(_("Advance Amount"), fieldname="advance")
|
||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
||||
@@ -183,7 +159,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||
|
||||
if self.account_type == "Receivable":
|
||||
if self.party_type == "Customer":
|
||||
self.add_column(
|
||||
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
|
||||
)
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/financial_statements.js", function () {
|
||||
frappe.query_reports["Balance Sheet"] = $.extend(
|
||||
{},
|
||||
erpnext.financial_statements
|
||||
);
|
||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements);
|
||||
|
||||
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
||||
erpnext.utils.add_dimensions('Balance Sheet', 10);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
"fieldname": "accumulated_values",
|
||||
"label": __("Accumulated Values"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
});
|
||||
console.log(frappe.query_reports["Balance Sheet"]["filters"]);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default Book Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
"fieldname": "include_default_book_entries",
|
||||
"label": __("Include Default Book Entries"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.report.balance_sheet.balance_sheet import execute
|
||||
|
||||
|
||||
class TestBalanceSheet(FrappeTestCase):
|
||||
def test_balance_sheet(self):
|
||||
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,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company="_Test Company 6",
|
||||
warehouse="Finished Goods - _TC6",
|
||||
expense_account="Cost of Goods Sold - _TC6",
|
||||
cost_center="Main - _TC6",
|
||||
qty=10,
|
||||
rate=100,
|
||||
)
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company 6",
|
||||
debit_to="Debtors - _TC6",
|
||||
income_account="Sales - _TC6",
|
||||
cost_center="Main - _TC6",
|
||||
qty=5,
|
||||
rate=110,
|
||||
)
|
||||
filters = frappe._dict(
|
||||
company="_Test Company 6",
|
||||
period_start_date=today(),
|
||||
period_end_date=today(),
|
||||
periodicity="Yearly",
|
||||
)
|
||||
result = execute(filters)[1]
|
||||
for account_dict in result:
|
||||
if account_dict.get("account") == "Current Liabilities - _TC6":
|
||||
self.assertEqual(account_dict.total, 1000)
|
||||
if account_dict.get("account") == "Current Assets - _TC6":
|
||||
self.assertEqual(account_dict.total, 550)
|
||||
@@ -6,7 +6,6 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
import erpnext
|
||||
@@ -360,7 +359,6 @@ def get_data(
|
||||
accounts_by_name,
|
||||
accounts,
|
||||
ignore_closing_entries=False,
|
||||
root_type=root_type,
|
||||
)
|
||||
|
||||
calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
|
||||
@@ -605,7 +603,6 @@ def set_gl_entries_by_account(
|
||||
accounts_by_name,
|
||||
accounts,
|
||||
ignore_closing_entries=False,
|
||||
root_type=None,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
|
||||
@@ -613,6 +610,7 @@ def set_gl_entries_by_account(
|
||||
"Company", filters.get("company"), ["lft", "rgt"]
|
||||
)
|
||||
|
||||
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
|
||||
companies = frappe.db.sql(
|
||||
""" select name, default_currency from `tabCompany`
|
||||
where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
|
||||
@@ -628,43 +626,27 @@ def set_gl_entries_by_account(
|
||||
)
|
||||
|
||||
for d in companies:
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
account = frappe.qb.DocType("Account")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.inner_join(account)
|
||||
.on(account.name == gle.account)
|
||||
.select(
|
||||
gle.posting_date,
|
||||
gle.account,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.is_opening,
|
||||
gle.company,
|
||||
gle.fiscal_year,
|
||||
gle.debit_in_account_currency,
|
||||
gle.credit_in_account_currency,
|
||||
gle.account_currency,
|
||||
account.account_name,
|
||||
account.account_number,
|
||||
)
|
||||
.where(
|
||||
(gle.company == d.name)
|
||||
& (gle.is_cancelled == 0)
|
||||
& (gle.posting_date <= to_date)
|
||||
& (account.lft >= root_lft)
|
||||
& (account.rgt <= root_rgt)
|
||||
)
|
||||
.orderby(gle.account, gle.posting_date)
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
|
||||
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
|
||||
acc.account_name, acc.account_number
|
||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
|
||||
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
|
||||
order by gl.account, gl.posting_date""".format(
|
||||
additional_conditions=additional_conditions
|
||||
),
|
||||
{
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"lft": root_lft,
|
||||
"rgt": root_rgt,
|
||||
"company": d.name,
|
||||
"finance_book": filters.get("finance_book"),
|
||||
"company_fb": frappe.get_cached_value("Company", d.name, "default_finance_book"),
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if root_type:
|
||||
query = query.where(account.root_type == root_type)
|
||||
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d)
|
||||
if additional_conditions:
|
||||
query = query.where(Criterion.all(additional_conditions))
|
||||
gl_entries = query.run(as_dict=True)
|
||||
|
||||
if filters and filters.get("presentation_currency") != d.default_currency:
|
||||
currency_info["company"] = d.name
|
||||
currency_info["company_currency"] = d.default_currency
|
||||
@@ -734,25 +716,23 @@ def validate_entries(key, entry, accounts_by_name, accounts):
|
||||
accounts.insert(idx + 1, args)
|
||||
|
||||
|
||||
def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
||||
additional_conditions = []
|
||||
|
||||
if ignore_closing_entries:
|
||||
additional_conditions.append((gle.voucher_type != "Period Closing Voucher"))
|
||||
additional_conditions.append("gl.voucher_type != 'Period Closing Voucher'")
|
||||
|
||||
if from_date:
|
||||
additional_conditions.append(gle.posting_date >= from_date)
|
||||
|
||||
finance_book = filters.get("finance_book")
|
||||
company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
|
||||
additional_conditions.append("gl.posting_date >= %(from_date)s")
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
|
||||
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
|
||||
return additional_conditions
|
||||
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
|
||||
|
||||
|
||||
def add_total_row(out, root_type, balance_must_be, companies, company_currency):
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Financial Ratios"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "from_fiscal_year",
|
||||
label: __("Start Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "to_fiscal_year",
|
||||
label: __("End Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "periodicity",
|
||||
label: __("Periodicity"),
|
||||
fieldtype: "Data",
|
||||
default: "Yearly",
|
||||
reqd: 1,
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "period_start_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "period_end_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
hidden: 1,
|
||||
},
|
||||
],
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
||||
let heading_ratios = ["Liquidity Ratios", "Solvency Ratios","Turnover Ratios"]
|
||||
|
||||
if (heading_ratios.includes(value)) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
let $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
}
|
||||
|
||||
if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
|
||||
column.fieldtype = "Data";
|
||||
}
|
||||
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-07-13 16:11:11.925096",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2023-07-13 16:11:11.925096",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Financial Ratios",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Account",
|
||||
"report_name": "Financial Ratios",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
},
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import get_data, get_period_list
|
||||
from erpnext.accounts.utils import get_balance_on, get_fiscal_year
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
filters["filter_based_on"] = "Fiscal Year"
|
||||
columns, data = [], []
|
||||
|
||||
setup_filters(filters)
|
||||
|
||||
period_list = get_period_list(
|
||||
filters.from_fiscal_year,
|
||||
filters.to_fiscal_year,
|
||||
filters.period_start_date,
|
||||
filters.period_end_date,
|
||||
filters.filter_based_on,
|
||||
filters.periodicity,
|
||||
company=filters.company,
|
||||
)
|
||||
|
||||
columns, years = get_columns(period_list)
|
||||
data = get_ratios_data(filters, period_list, years)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def setup_filters(filters):
|
||||
if not filters.get("period_start_date"):
|
||||
period_start_date = get_fiscal_year(fiscal_year=filters.from_fiscal_year)[1]
|
||||
filters["period_start_date"] = period_start_date
|
||||
|
||||
if not filters.get("period_end_date"):
|
||||
period_end_date = get_fiscal_year(fiscal_year=filters.to_fiscal_year)[2]
|
||||
filters["period_end_date"] = period_end_date
|
||||
|
||||
|
||||
def get_columns(period_list):
|
||||
years = []
|
||||
columns = [
|
||||
{
|
||||
"label": _("Ratios"),
|
||||
"fieldname": "ratio",
|
||||
"fieldtype": "Data",
|
||||
"width": 200,
|
||||
},
|
||||
]
|
||||
|
||||
for period in period_list:
|
||||
columns.append(
|
||||
{
|
||||
"fieldname": period.key,
|
||||
"label": period.label,
|
||||
"fieldtype": "Float",
|
||||
"width": 150,
|
||||
}
|
||||
)
|
||||
years.append(period.key)
|
||||
|
||||
return columns, years
|
||||
|
||||
|
||||
def get_ratios_data(filters, period_list, years):
|
||||
|
||||
data = []
|
||||
assets, liabilities, income, expense = get_gl_data(filters, period_list, years)
|
||||
|
||||
current_asset, total_asset = {}, {}
|
||||
current_liability, total_liability = {}, {}
|
||||
net_sales, total_income = {}, {}
|
||||
cogs, total_expense = {}, {}
|
||||
quick_asset = {}
|
||||
direct_expense = {}
|
||||
|
||||
for year in years:
|
||||
total_quick_asset = 0
|
||||
total_net_sales = 0
|
||||
total_cogs = 0
|
||||
|
||||
for d in [
|
||||
[
|
||||
current_asset,
|
||||
total_asset,
|
||||
"Current Asset",
|
||||
year,
|
||||
assets,
|
||||
"Asset",
|
||||
quick_asset,
|
||||
total_quick_asset,
|
||||
],
|
||||
[
|
||||
current_liability,
|
||||
total_liability,
|
||||
"Current Liability",
|
||||
year,
|
||||
liabilities,
|
||||
"Liability",
|
||||
{},
|
||||
0,
|
||||
],
|
||||
[cogs, total_expense, "Cost of Goods Sold", year, expense, "Expense", {}, total_cogs],
|
||||
[direct_expense, direct_expense, "Direct Expense", year, expense, "Expense", {}, 0],
|
||||
[net_sales, total_income, "Direct Income", year, income, "Income", {}, total_net_sales],
|
||||
]:
|
||||
update_balances(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7])
|
||||
add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset)
|
||||
add_solvency_ratios(
|
||||
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||||
)
|
||||
add_turnover_ratios(
|
||||
data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_gl_data(filters, period_list, years):
|
||||
data = {}
|
||||
|
||||
for d in [
|
||||
["Asset", "Debit"],
|
||||
["Liability", "Credit"],
|
||||
["Income", "Credit"],
|
||||
["Expense", "Debit"],
|
||||
]:
|
||||
data[frappe.scrub(d[0])] = get_data(
|
||||
filters.company,
|
||||
d[0],
|
||||
d[1],
|
||||
period_list,
|
||||
only_current_fiscal_year=False,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
assets, liabilities, income, expense = (
|
||||
data.get("asset"),
|
||||
data.get("liability"),
|
||||
data.get("income"),
|
||||
data.get("expense"),
|
||||
)
|
||||
|
||||
return assets, liabilities, income, expense
|
||||
|
||||
|
||||
def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Liquidity Ratios"})
|
||||
|
||||
ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
|
||||
|
||||
for d in ratio_data:
|
||||
row = {
|
||||
"ratio": d[0],
|
||||
}
|
||||
for year in years:
|
||||
row[year] = calculate_ratio(d[1].get(year, 0), current_liability.get(year, 0), precision)
|
||||
|
||||
data.append(row)
|
||||
|
||||
|
||||
def add_solvency_ratios(
|
||||
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||||
):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Solvency Ratios"})
|
||||
|
||||
debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
|
||||
gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
|
||||
net_profit_ratio = {"ratio": "Net Profit Ratio"}
|
||||
return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
|
||||
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
||||
|
||||
for year in years:
|
||||
profit_after_tax = total_income[year] + total_expense[year]
|
||||
share_holder_fund = total_asset[year] - total_liability[year]
|
||||
|
||||
debt_equity_ratio[year] = calculate_ratio(
|
||||
total_liability.get(year), share_holder_fund, precision
|
||||
)
|
||||
return_on_equity_ratio[year] = calculate_ratio(profit_after_tax, share_holder_fund, precision)
|
||||
|
||||
net_profit_ratio[year] = calculate_ratio(profit_after_tax, net_sales.get(year), precision)
|
||||
gross_profit_ratio[year] = calculate_ratio(
|
||||
net_sales.get(year, 0) - cogs.get(year, 0), net_sales.get(year), precision
|
||||
)
|
||||
return_on_asset_ratio[year] = calculate_ratio(profit_after_tax, total_asset.get(year), precision)
|
||||
|
||||
data.append(debt_equity_ratio)
|
||||
data.append(gross_profit_ratio)
|
||||
data.append(net_profit_ratio)
|
||||
data.append(return_on_asset_ratio)
|
||||
data.append(return_on_equity_ratio)
|
||||
|
||||
|
||||
def add_turnover_ratios(
|
||||
data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense
|
||||
):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Turnover Ratios"})
|
||||
|
||||
avg_data = {}
|
||||
for d in ["Receivable", "Payable", "Stock"]:
|
||||
avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters)
|
||||
|
||||
avg_debtors, avg_creditors, avg_stock = (
|
||||
avg_data.get("receivable"),
|
||||
avg_data.get("payable"),
|
||||
avg_data.get("stock"),
|
||||
)
|
||||
|
||||
ratio_data = [
|
||||
["Fixed Asset Turnover Ratio", net_sales, total_asset],
|
||||
["Debtor Turnover Ratio", net_sales, avg_debtors],
|
||||
["Creditor Turnover Ratio", direct_expense, avg_creditors],
|
||||
["Inventory Turnover Ratio", cogs, avg_stock],
|
||||
]
|
||||
for ratio in ratio_data:
|
||||
row = {
|
||||
"ratio": ratio[0],
|
||||
}
|
||||
for year in years:
|
||||
row[year] = calculate_ratio(ratio[1].get(year, 0), ratio[2].get(year, 0), precision)
|
||||
|
||||
data.append(row)
|
||||
|
||||
|
||||
def update_balances(
|
||||
ratio_dict,
|
||||
total_dict,
|
||||
account_type,
|
||||
year,
|
||||
root_type_data,
|
||||
root_type,
|
||||
net_dict=None,
|
||||
total_net=0,
|
||||
):
|
||||
|
||||
for entry in root_type_data:
|
||||
if not entry.get("parent_account") and entry.get("is_group"):
|
||||
total_dict[year] = entry[year]
|
||||
if account_type == "Direct Expense":
|
||||
total_dict[year] = entry[year] * -1
|
||||
|
||||
if root_type in ("Asset", "Liability"):
|
||||
if entry.get("account_type") == account_type and entry.get("is_group"):
|
||||
ratio_dict[year] = entry.get(year)
|
||||
if entry.get("account_type") in ["Bank", "Cash", "Receivable"] and not entry.get("is_group"):
|
||||
total_net += entry.get(year)
|
||||
net_dict[year] = total_net
|
||||
|
||||
elif root_type == "Income":
|
||||
if entry.get("account_type") == account_type and entry.get("is_group"):
|
||||
total_net += entry.get(year)
|
||||
ratio_dict[year] = total_net
|
||||
elif root_type == "Expense" and account_type == "Cost of Goods Sold":
|
||||
if entry.get("account_type") == account_type:
|
||||
total_net += entry.get(year)
|
||||
ratio_dict[year] = total_net
|
||||
else:
|
||||
if entry.get("account_type") == account_type and entry.get("is_group"):
|
||||
ratio_dict[year] = entry.get(year)
|
||||
|
||||
|
||||
def avg_ratio_balance(account_type, period_list, precision, filters):
|
||||
avg_ratio = {}
|
||||
for period in period_list:
|
||||
opening_date = add_days(period["from_date"], -1)
|
||||
closing_date = period["to_date"]
|
||||
|
||||
closing_balance = get_balance_on(
|
||||
date=closing_date,
|
||||
company=filters.company,
|
||||
account_type=account_type,
|
||||
)
|
||||
opening_balance = get_balance_on(
|
||||
date=opening_date,
|
||||
company=filters.company,
|
||||
account_type=account_type,
|
||||
)
|
||||
avg_ratio[period["key"]] = flt(
|
||||
(flt(closing_balance) + flt(opening_balance)) / 2, precision=precision
|
||||
)
|
||||
|
||||
return avg_ratio
|
||||
|
||||
|
||||
def calculate_ratio(value, denominator, precision):
|
||||
if flt(denominator):
|
||||
return flt(flt(value) / denominator, precision)
|
||||
return 0
|
||||
@@ -188,7 +188,6 @@ def get_data(
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
root_type=root_type,
|
||||
)
|
||||
|
||||
calculate_values(
|
||||
@@ -335,10 +334,12 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
|
||||
for period in period_list:
|
||||
total_row.setdefault(period.key, 0.0)
|
||||
total_row[period.key] += row.get(period.key, 0.0)
|
||||
row[period.key] = row.get(period.key, 0.0)
|
||||
|
||||
total_row.setdefault("total", 0.0)
|
||||
total_row["total"] += flt(row["total"])
|
||||
total_row["opening_balance"] += row["opening_balance"]
|
||||
row["total"] = ""
|
||||
|
||||
if "total" in total_row:
|
||||
out.append(total_row)
|
||||
@@ -416,28 +417,13 @@ def set_gl_entries_by_account(
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=False,
|
||||
ignore_opening_entries=False,
|
||||
root_type=None,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
gl_entries = []
|
||||
|
||||
account_filters = {
|
||||
"company": company,
|
||||
"is_group": 0,
|
||||
"lft": (">=", root_lft),
|
||||
"rgt": ("<=", root_rgt),
|
||||
}
|
||||
|
||||
if root_type:
|
||||
account_filters.update(
|
||||
{
|
||||
"root_type": root_type,
|
||||
}
|
||||
)
|
||||
|
||||
accounts_list = frappe.db.get_all(
|
||||
"Account",
|
||||
filters=account_filters,
|
||||
filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
@@ -637,13 +623,7 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None):
|
||||
if periodicity != "Yearly":
|
||||
if not accumulated_values:
|
||||
columns.append(
|
||||
{
|
||||
"fieldname": "total",
|
||||
"label": _("Total"),
|
||||
"fieldtype": "Currency",
|
||||
"width": 150,
|
||||
"options": "currency",
|
||||
}
|
||||
{"fieldname": "total", "label": _("Total"), "fieldtype": "Currency", "width": 150}
|
||||
)
|
||||
|
||||
return columns
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname":"account",
|
||||
"label": __("Account"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"options": "Account",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Account', txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
account_type: ['in', ["Receivable", "Payable"]]
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"voucher_no",
|
||||
"label": __("Voucher No"),
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["General and Payment Ledger Comparison"] = {
|
||||
"filters": get_filters()
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-08-02 17:30:29.494907",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2023-08-02 17:30:29.494907",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General and Payment Ledger Comparison",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "General and Payment Ledger Comparison",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
class General_Payment_Ledger_Comparison(object):
|
||||
"""
|
||||
A Utility report to compare Voucher-wise balance between General and Payment Ledger
|
||||
"""
|
||||
|
||||
def __init__(self, filters=None):
|
||||
self.filters = filters
|
||||
self.gle = []
|
||||
self.ple = []
|
||||
|
||||
def get_accounts(self):
|
||||
receivable_accounts = [
|
||||
x[0]
|
||||
for x in frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": self.filters.company, "account_type": "Receivable"},
|
||||
as_list=True,
|
||||
)
|
||||
]
|
||||
payable_accounts = [
|
||||
x[0]
|
||||
for x in frappe.db.get_all(
|
||||
"Account", filters={"company": self.filters.company, "account_type": "Payable"}, as_list=True
|
||||
)
|
||||
]
|
||||
|
||||
self.account_types = frappe._dict(
|
||||
{
|
||||
"receivable": frappe._dict({"accounts": receivable_accounts, "gle": [], "ple": []}),
|
||||
"payable": frappe._dict({"accounts": payable_accounts, "gle": [], "ple": []}),
|
||||
}
|
||||
)
|
||||
|
||||
def generate_filters(self):
|
||||
if self.filters.account:
|
||||
self.account_types.receivable.accounts = []
|
||||
self.account_types.payable.accounts = []
|
||||
|
||||
for acc in frappe.db.get_all(
|
||||
"Account", filters={"name": ["in", self.filters.account]}, fields=["name", "account_type"]
|
||||
):
|
||||
if acc.account_type == "Receivable":
|
||||
self.account_types.receivable.accounts.append(acc.name)
|
||||
else:
|
||||
self.account_types.payable.accounts.append(acc.name)
|
||||
|
||||
def get_gle(self):
|
||||
gle = qb.DocType("GL Entry")
|
||||
|
||||
for acc_type, val in self.account_types.items():
|
||||
if val.accounts:
|
||||
|
||||
filter_criterion = []
|
||||
if self.filters.voucher_no:
|
||||
filter_criterion.append((gle.voucher_no == self.filters.voucher_no))
|
||||
|
||||
if self.filters.period_start_date:
|
||||
filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date))
|
||||
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
if acc_type == "receivable":
|
||||
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
|
||||
else:
|
||||
outstanding = (Sum(gle.credit) - Sum(gle.debit)).as_("outstanding")
|
||||
|
||||
self.account_types[acc_type].gle = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.company,
|
||||
gle.account,
|
||||
gle.voucher_no,
|
||||
gle.party,
|
||||
outstanding,
|
||||
)
|
||||
.where(
|
||||
(gle.company == self.filters.company)
|
||||
& (gle.is_cancelled == 0)
|
||||
& (gle.account.isin(val.accounts))
|
||||
)
|
||||
.where(Criterion.all(filter_criterion))
|
||||
.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
|
||||
.run()
|
||||
)
|
||||
|
||||
def get_ple(self):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
for acc_type, val in self.account_types.items():
|
||||
if val.accounts:
|
||||
|
||||
filter_criterion = []
|
||||
if self.filters.voucher_no:
|
||||
filter_criterion.append((ple.voucher_no == self.filters.voucher_no))
|
||||
|
||||
if self.filters.period_start_date:
|
||||
filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date))
|
||||
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
self.account_types[acc_type].ple = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
|
||||
)
|
||||
.where(
|
||||
(ple.company == self.filters.company)
|
||||
& (ple.delinked == 0)
|
||||
& (ple.account.isin(val.accounts))
|
||||
)
|
||||
.where(Criterion.all(filter_criterion))
|
||||
.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
|
||||
.run()
|
||||
)
|
||||
|
||||
def compare(self):
|
||||
self.gle_balances = set()
|
||||
self.ple_balances = set()
|
||||
|
||||
# consolidate both receivable and payable balances in one set
|
||||
for acc_type, val in self.account_types.items():
|
||||
self.gle_balances = set(val.gle) | self.gle_balances
|
||||
self.ple_balances = set(val.ple) | self.ple_balances
|
||||
|
||||
self.diff1 = self.gle_balances.difference(self.ple_balances)
|
||||
self.diff2 = self.ple_balances.difference(self.gle_balances)
|
||||
self.diff = frappe._dict({})
|
||||
|
||||
for x in self.diff1:
|
||||
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
||||
|
||||
for x in self.diff2:
|
||||
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
|
||||
|
||||
def generate_data(self):
|
||||
self.data = []
|
||||
for key, val in self.diff.items():
|
||||
self.data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_no": key[2],
|
||||
"party": key[3],
|
||||
"gl_balance": val.gl_balance,
|
||||
"pl_balance": val.pl_balance,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Voucher No"),
|
||||
fieldname="voucher_no",
|
||||
fieldtype="Data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("GL Balance"),
|
||||
fieldname="gl_balance",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Payment Ledger Balance"),
|
||||
fieldname="pl_balance",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.get_accounts()
|
||||
self.generate_filters()
|
||||
self.get_gle()
|
||||
self.get_ple()
|
||||
self.compare()
|
||||
self.generate_data()
|
||||
self.get_columns()
|
||||
|
||||
return self.columns, self.data
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
|
||||
rpt = General_Payment_Ledger_Comparison(filters)
|
||||
columns, data = rpt.run()
|
||||
|
||||
return columns, data
|
||||
@@ -1,100 +0,0 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_and_payment_ledger_comparison.general_and_payment_ledger_comparison import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.cleanup()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def cleanup(self):
|
||||
doctypes = []
|
||||
doctypes.append(qb.DocType("GL Entry"))
|
||||
doctypes.append(qb.DocType("Payment Ledger Entry"))
|
||||
doctypes.append(qb.DocType("Sales Invoice"))
|
||||
|
||||
for doctype in doctypes:
|
||||
qb.from_(doctype).delete().where(doctype.company == self.company).run()
|
||||
|
||||
def test_01_basic_report_functionality(self):
|
||||
sinv = create_sales_invoice(
|
||||
company=self.company,
|
||||
debit_to=self.debit_to,
|
||||
expense_account=self.expense_account,
|
||||
cost_center=self.cost_center,
|
||||
income_account=self.income_account,
|
||||
warehouse=self.warehouse,
|
||||
)
|
||||
|
||||
# manually edit the payment ledger entry
|
||||
ple = frappe.db.get_all(
|
||||
"Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0}
|
||||
)[0]
|
||||
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1)
|
||||
|
||||
filters = frappe._dict({"company": self.company})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
|
||||
expected = {
|
||||
"voucher_no": sinv.name,
|
||||
"party": sinv.customer,
|
||||
"gl_balance": sinv.grand_total,
|
||||
"pl_balance": sinv.grand_total - 1,
|
||||
}
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
# account filter
|
||||
filters = frappe._dict({"company": self.company, "account": self.debit_to})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict({"company": self.company, "account": self.creditors})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
|
||||
# voucher_no filter
|
||||
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name + "-1"})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
|
||||
# date range filter
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"period_start_date": sinv.posting_date,
|
||||
"period_end_date": sinv.posting_date,
|
||||
}
|
||||
)
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"period_start_date": add_days(sinv.posting_date, -1),
|
||||
"period_end_date": add_days(sinv.posting_date, -1),
|
||||
}
|
||||
)
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
@@ -188,11 +188,6 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname": "show_net_values_in_party_account",
|
||||
"label": __("Show Net Values in Party Account"),
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"fieldname": "add_values_in_transaction_currency",
|
||||
"label": __("Add Columns in Transaction Currency"),
|
||||
"fieldtype": "Check"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -182,18 +182,12 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
if accounting_dimensions:
|
||||
dimension_fields = ", ".join(accounting_dimensions) + ","
|
||||
|
||||
transaction_currency_fields = ""
|
||||
if filters.get("add_values_in_transaction_currency"):
|
||||
transaction_currency_fields = (
|
||||
"debit_in_transaction_currency, credit_in_transaction_currency, transaction_currency,"
|
||||
)
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name as gl_entry, posting_date, account, party_type, party,
|
||||
voucher_type, voucher_no, {dimension_fields}
|
||||
cost_center, project, {transaction_currency_fields}
|
||||
cost_center, project,
|
||||
against_voucher_type, against_voucher, account_currency,
|
||||
remarks, against, is_opening, creation {select_fields}
|
||||
from `tabGL Entry`
|
||||
@@ -201,7 +195,6 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
{order_by_statement}
|
||||
""".format(
|
||||
dimension_fields=dimension_fields,
|
||||
transaction_currency_fields=transaction_currency_fields,
|
||||
select_fields=select_fields,
|
||||
conditions=get_conditions(filters),
|
||||
order_by_statement=order_by_statement,
|
||||
@@ -569,34 +562,6 @@ def get_columns(filters):
|
||||
"fieldtype": "Float",
|
||||
"width": 130,
|
||||
},
|
||||
]
|
||||
|
||||
if filters.get("add_values_in_transaction_currency"):
|
||||
columns += [
|
||||
{
|
||||
"label": _("Debit (Transaction)"),
|
||||
"fieldname": "debit_in_transaction_currency",
|
||||
"fieldtype": "Currency",
|
||||
"width": 130,
|
||||
"options": "transaction_currency",
|
||||
},
|
||||
{
|
||||
"label": _("Credit (Transaction)"),
|
||||
"fieldname": "credit_in_transaction_currency",
|
||||
"fieldtype": "Currency",
|
||||
"width": 130,
|
||||
"options": "transaction_currency",
|
||||
},
|
||||
{
|
||||
"label": "Transaction Currency",
|
||||
"fieldname": "transaction_currency",
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"width": 70,
|
||||
},
|
||||
]
|
||||
|
||||
columns += [
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
|
||||
{
|
||||
"label": _("Voucher No"),
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/financial_statements.js", function () {
|
||||
frappe.query_reports["Profit and Loss Statement"] = $.extend(
|
||||
{},
|
||||
erpnext.financial_statements
|
||||
|
||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
frappe.query_reports["Profit and Loss Statement"] = $.extend({},
|
||||
erpnext.financial_statements);
|
||||
|
||||
erpnext.utils.add_dimensions('Profit and Loss Statement', 10);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
||||
{
|
||||
"fieldname": "include_default_book_entries",
|
||||
"label": __("Include Default Book Entries"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
}
|
||||
);
|
||||
|
||||
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,14 +33,7 @@ frappe.query_reports["Tax Withholding Details"] = {
|
||||
frappe.throw(__("Please select Party Type first"));
|
||||
}
|
||||
return party_type;
|
||||
},
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"tax_withholding_category": ["!=",""],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
|
||||
@@ -7,26 +7,19 @@ from frappe import _
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if filters.get("party_type") == "Customer":
|
||||
party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
else:
|
||||
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
filters.update({"naming_series": party_naming_by})
|
||||
|
||||
validate_filters(filters)
|
||||
(
|
||||
tds_docs,
|
||||
tds_accounts,
|
||||
tax_category_map,
|
||||
journal_entry_party_map,
|
||||
net_total_map,
|
||||
invoice_net_total_map,
|
||||
) = get_tds_docs(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
|
||||
res = get_result(
|
||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
|
||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
|
||||
)
|
||||
return columns, res
|
||||
|
||||
@@ -38,7 +31,7 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_result(
|
||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
|
||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
|
||||
):
|
||||
party_map = get_party_pan_map(filters.get("party_type"))
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
@@ -46,7 +39,7 @@ def get_result(
|
||||
|
||||
out = []
|
||||
for name, details in gle_map.items():
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
tax_amount, total_amount = 0, 0
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
@@ -67,8 +60,8 @@ def get_result(
|
||||
if entry.account in tds_accounts:
|
||||
tax_amount += entry.credit - entry.debit
|
||||
|
||||
if net_total_map.get(name):
|
||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||
if invoice_net_total_map.get(name):
|
||||
total_amount = invoice_net_total_map.get(name)
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
|
||||
@@ -76,13 +69,15 @@ def get_result(
|
||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
||||
party_name = "supplier_name"
|
||||
party_type = "supplier_type"
|
||||
table_name = "Supplier"
|
||||
else:
|
||||
party_name = "customer_name"
|
||||
party_type = "customer_type"
|
||||
table_name = "Customer"
|
||||
|
||||
row = {
|
||||
"pan"
|
||||
if frappe.db.has_column(filters.party_type, "pan")
|
||||
if frappe.db.has_column(table_name, "pan")
|
||||
else "tax_id": party_map.get(party, {}).get("pan"),
|
||||
"party": party_map.get(party, {}).get("name"),
|
||||
}
|
||||
@@ -96,8 +91,6 @@ def get_result(
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
"base_total": base_total,
|
||||
"tax_amount": tax_amount,
|
||||
"transaction_date": posting_date,
|
||||
"transaction_type": voucher_type,
|
||||
@@ -151,9 +144,9 @@ def get_gle_map(documents):
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
|
||||
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
|
||||
columns = [
|
||||
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
|
||||
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
|
||||
{
|
||||
"label": _(filters.get("party_type")),
|
||||
"fieldname": "party",
|
||||
@@ -165,36 +158,19 @@ def get_columns(filters):
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
columns.append(
|
||||
{
|
||||
"label": _(filters.party_type + " Name"),
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
}
|
||||
{"label": _("Party Name"), "fieldname": "party_name", "fieldtype": "Data", "width": 180}
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("Date of Transaction"),
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Section Code"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "section_code",
|
||||
"fieldtype": "Link",
|
||||
"width": 90,
|
||||
},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
"width": 180,
|
||||
},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 120},
|
||||
{
|
||||
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
|
||||
"fieldname": "rate",
|
||||
@@ -202,21 +178,21 @@ def get_columns(filters):
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"label": _("Total Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Base Total"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Float",
|
||||
"label": _("Date of Transaction"),
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 90,
|
||||
},
|
||||
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
|
||||
@@ -240,7 +216,7 @@ def get_tds_docs(filters):
|
||||
payment_entries = []
|
||||
journal_entries = []
|
||||
tax_category_map = frappe._dict()
|
||||
net_total_map = frappe._dict()
|
||||
invoice_net_total_map = frappe._dict()
|
||||
or_filters = frappe._dict()
|
||||
journal_entry_party_map = frappe._dict()
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
@@ -284,13 +260,13 @@ def get_tds_docs(filters):
|
||||
tds_documents.append(d.voucher_no)
|
||||
|
||||
if purchase_invoices:
|
||||
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map)
|
||||
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, invoice_net_total_map)
|
||||
|
||||
if sales_invoices:
|
||||
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map)
|
||||
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, invoice_net_total_map)
|
||||
|
||||
if payment_entries:
|
||||
get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map)
|
||||
get_doc_info(payment_entries, "Payment Entry", tax_category_map)
|
||||
|
||||
if journal_entries:
|
||||
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
|
||||
@@ -301,7 +277,7 @@ def get_tds_docs(filters):
|
||||
tds_accounts,
|
||||
tax_category_map,
|
||||
journal_entry_party_map,
|
||||
net_total_map,
|
||||
invoice_net_total_map,
|
||||
)
|
||||
|
||||
|
||||
@@ -319,25 +295,11 @@ def get_journal_entry_party_map(journal_entries):
|
||||
return journal_entry_party_map
|
||||
|
||||
|
||||
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None):
|
||||
if doctype == "Purchase Invoice":
|
||||
fields = [
|
||||
"name",
|
||||
"tax_withholding_category",
|
||||
"base_tax_withholding_net_total",
|
||||
"grand_total",
|
||||
"base_total",
|
||||
]
|
||||
elif doctype == "Sales Invoice":
|
||||
fields = ["name", "base_net_total", "grand_total", "base_total"]
|
||||
elif doctype == "Payment Entry":
|
||||
fields = [
|
||||
"name",
|
||||
"tax_withholding_category",
|
||||
"paid_amount",
|
||||
"paid_amount_after_tax",
|
||||
"base_paid_amount",
|
||||
]
|
||||
fields = ["name", "tax_withholding_category", "base_tax_withholding_net_total"]
|
||||
if doctype == "Sales Invoice":
|
||||
fields = ["name", "base_net_total"]
|
||||
else:
|
||||
fields = ["name", "tax_withholding_category"]
|
||||
|
||||
@@ -346,15 +308,9 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
for entry in entries:
|
||||
tax_category_map.update({entry.name: entry.tax_withholding_category})
|
||||
if doctype == "Purchase Invoice":
|
||||
net_total_map.update(
|
||||
{entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]}
|
||||
)
|
||||
elif doctype == "Sales Invoice":
|
||||
net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]})
|
||||
elif doctype == "Payment Entry":
|
||||
net_total_map.update(
|
||||
{entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]}
|
||||
)
|
||||
invoice_net_total_map.update({entry.name: entry.base_tax_withholding_net_total})
|
||||
if doctype == "Sales Invoice":
|
||||
invoice_net_total_map.update({entry.name: entry.base_net_total})
|
||||
|
||||
|
||||
def get_tax_rate_map(filters):
|
||||
|
||||
@@ -12,35 +12,17 @@ frappe.query_reports["TDS Computation Summary"] = {
|
||||
"default": frappe.defaults.get_default('company')
|
||||
},
|
||||
{
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Supplier", "Customer"],
|
||||
"reqd": 1,
|
||||
"default": "Supplier",
|
||||
"on_change": function(){
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"get_options": function() {
|
||||
var party_type = frappe.query_report.get_filter_value('party_type');
|
||||
var party = frappe.query_report.get_filter_value('party');
|
||||
if(party && !party_type) {
|
||||
frappe.throw(__("Please select Party Type first"));
|
||||
}
|
||||
return party_type;
|
||||
},
|
||||
"fieldname":"supplier",
|
||||
"label": __("Supplier"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"tax_withholding_category": ["!=",""],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
|
||||
@@ -9,15 +9,10 @@ from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if filters.get("party_type") == "Customer":
|
||||
party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
else:
|
||||
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
filters.update({"naming_series": party_naming_by})
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
columns = get_columns(filters)
|
||||
(
|
||||
tds_docs,
|
||||
@@ -30,7 +25,7 @@ def execute(filters=None):
|
||||
res = get_result(
|
||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map
|
||||
)
|
||||
final_result = group_by_party_and_category(res, filters)
|
||||
final_result = group_by_supplier_and_category(res)
|
||||
|
||||
return columns, final_result
|
||||
|
||||
@@ -48,67 +43,60 @@ def validate_filters(filters):
|
||||
filters["fiscal_year"] = from_year
|
||||
|
||||
|
||||
def group_by_party_and_category(data, filters):
|
||||
party_category_wise_map = {}
|
||||
def group_by_supplier_and_category(data):
|
||||
supplier_category_wise_map = {}
|
||||
|
||||
for row in data:
|
||||
party_category_wise_map.setdefault(
|
||||
(row.get("party"), row.get("section_code")),
|
||||
supplier_category_wise_map.setdefault(
|
||||
(row.get("supplier"), row.get("section_code")),
|
||||
{
|
||||
"pan": row.get("pan"),
|
||||
"tax_id": row.get("tax_id"),
|
||||
"party": row.get("party"),
|
||||
"party_name": row.get("party_name"),
|
||||
"supplier": row.get("supplier"),
|
||||
"supplier_name": row.get("supplier_name"),
|
||||
"section_code": row.get("section_code"),
|
||||
"entity_type": row.get("entity_type"),
|
||||
"rate": row.get("rate"),
|
||||
"total_amount": 0.0,
|
||||
"tax_amount": 0.0,
|
||||
"tds_rate": row.get("tds_rate"),
|
||||
"total_amount_credited": 0.0,
|
||||
"tds_deducted": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
party_category_wise_map.get((row.get("party"), row.get("section_code")))[
|
||||
"total_amount"
|
||||
] += row.get("total_amount", 0.0)
|
||||
supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[
|
||||
"total_amount_credited"
|
||||
] += row.get("total_amount_credited", 0.0)
|
||||
|
||||
party_category_wise_map.get((row.get("party"), row.get("section_code")))[
|
||||
"tax_amount"
|
||||
] += row.get("tax_amount", 0.0)
|
||||
supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[
|
||||
"tds_deducted"
|
||||
] += row.get("tds_deducted", 0.0)
|
||||
|
||||
final_result = get_final_result(party_category_wise_map)
|
||||
final_result = get_final_result(supplier_category_wise_map)
|
||||
|
||||
return final_result
|
||||
|
||||
|
||||
def get_final_result(party_category_wise_map):
|
||||
def get_final_result(supplier_category_wise_map):
|
||||
out = []
|
||||
for key, value in party_category_wise_map.items():
|
||||
for key, value in supplier_category_wise_map.items():
|
||||
out.append(value)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
|
||||
columns = [
|
||||
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
|
||||
{"label": _("PAN"), "fieldname": "pan", "fieldtype": "Data", "width": 90},
|
||||
{
|
||||
"label": _(filters.get("party_type")),
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"label": _("Supplier"),
|
||||
"options": "Supplier",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"width": 180,
|
||||
},
|
||||
]
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
columns.append(
|
||||
{
|
||||
"label": _(filters.party_type + " Name"),
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
}
|
||||
{"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180}
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
@@ -121,23 +109,18 @@ def get_columns(filters):
|
||||
"width": 180,
|
||||
},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
|
||||
{"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90},
|
||||
{
|
||||
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"width": 120,
|
||||
"label": _("Total Amount Credited"),
|
||||
"fieldname": "total_amount_credited",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"label": _("Amount of TDS Deducted"),
|
||||
"fieldname": "tds_deducted",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
"width": 90,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class AccountsTestMixin:
|
||||
def create_customer(self, customer_name="_Test Customer", currency=None):
|
||||
def create_customer(self, customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = customer_name
|
||||
@@ -17,7 +17,7 @@ class AccountsTestMixin:
|
||||
else:
|
||||
self.customer = customer_name
|
||||
|
||||
def create_supplier(self, supplier_name="_Test Supplier", currency=None):
|
||||
def create_supplier(self, supplier_name, currency=None):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = supplier_name
|
||||
@@ -31,7 +31,7 @@ class AccountsTestMixin:
|
||||
else:
|
||||
self.supplier = supplier_name
|
||||
|
||||
def create_item(self, item_name="_Test Item", is_stock=0, warehouse=None, company=None):
|
||||
def create_item(self, item_name, is_stock=0, warehouse=None, company=None):
|
||||
item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
|
||||
self.item = item.name
|
||||
|
||||
@@ -62,44 +62,19 @@ class AccountsTestMixin:
|
||||
self.debit_usd = "Debtors USD - " + abbr
|
||||
self.cash = "Cash - " + abbr
|
||||
self.creditors = "Creditors - " + abbr
|
||||
self.retained_earnings = "Retained Earnings - " + abbr
|
||||
|
||||
# Deferred revenue, expense and bank accounts
|
||||
other_accounts = [
|
||||
frappe._dict(
|
||||
# create bank account
|
||||
bank_account = "HDFC - " + abbr
|
||||
if frappe.db.exists("Account", bank_account):
|
||||
self.bank = bank_account
|
||||
else:
|
||||
bank_acc = frappe.get_doc(
|
||||
{
|
||||
"attribute_name": "deferred_revenue",
|
||||
"account_name": "Deferred Revenue",
|
||||
"parent_account": "Current Liabilities - " + abbr,
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "deferred_expense",
|
||||
"account_name": "Deferred Expense",
|
||||
"parent_account": "Current Assets - " + abbr,
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "bank",
|
||||
"doctype": "Account",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - " + abbr,
|
||||
"company": self.company,
|
||||
}
|
||||
),
|
||||
]
|
||||
for acc in other_accounts:
|
||||
acc_name = acc.account_name + " - " + abbr
|
||||
if frappe.db.exists("Account", acc_name):
|
||||
setattr(self, acc.attribute_name, acc_name)
|
||||
else:
|
||||
new_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": acc.account_name,
|
||||
"parent_account": acc.parent_account,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
new_acc.save()
|
||||
setattr(self, acc.attribute_name, new_acc.name)
|
||||
)
|
||||
bank_acc.save()
|
||||
self.bank = bank_acc.name
|
||||
|
||||
@@ -80,27 +80,18 @@ class TestUtils(unittest.TestCase):
|
||||
item = make_item().name
|
||||
|
||||
purchase_invoice = make_purchase_invoice(
|
||||
item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32, do_not_submit=1
|
||||
item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32
|
||||
)
|
||||
purchase_invoice.credit_to = "_Test Payable USD - _TC"
|
||||
purchase_invoice.submit()
|
||||
|
||||
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
|
||||
payment_entry.target_exchange_rate = 62.9
|
||||
payment_entry.paid_amount = 15725
|
||||
payment_entry.deductions = []
|
||||
payment_entry.save()
|
||||
|
||||
# below is the difference between base_received_amount and base_paid_amount
|
||||
self.assertEqual(payment_entry.difference_amount, -4855.0)
|
||||
|
||||
payment_entry.target_exchange_rate = 62.9
|
||||
payment_entry.save()
|
||||
|
||||
# below is due to change in exchange rate
|
||||
self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0)
|
||||
payment_entry.insert()
|
||||
|
||||
self.assertEqual(payment_entry.difference_amount, -4855.00)
|
||||
payment_entry.references = []
|
||||
self.assertEqual(payment_entry.difference_amount, 0.0)
|
||||
payment_entry.submit()
|
||||
|
||||
payment_reconciliation = frappe.new_doc("Payment Reconciliation")
|
||||
|
||||
@@ -179,7 +179,6 @@ def get_balance_on(
|
||||
in_account_currency=True,
|
||||
cost_center=None,
|
||||
ignore_account_permission=False,
|
||||
account_type=None,
|
||||
):
|
||||
if not account and frappe.form_dict.get("account"):
|
||||
account = frappe.form_dict.get("account")
|
||||
@@ -255,21 +254,6 @@ def get_balance_on(
|
||||
else:
|
||||
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
|
||||
|
||||
if account_type:
|
||||
accounts = frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": company, "account_type": account_type, "is_group": 0},
|
||||
pluck="name",
|
||||
order_by="lft",
|
||||
)
|
||||
|
||||
cond.append(
|
||||
"""
|
||||
gle.account in (%s)
|
||||
"""
|
||||
% (", ".join([frappe.db.escape(account) for account in accounts]))
|
||||
)
|
||||
|
||||
if party_type and party:
|
||||
cond.append(
|
||||
"""gle.party_type = %s and gle.party = %s """
|
||||
@@ -279,8 +263,7 @@ def get_balance_on(
|
||||
if company:
|
||||
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
|
||||
|
||||
if account or (party_type and party) or account_type:
|
||||
|
||||
if account or (party_type and party):
|
||||
if in_account_currency:
|
||||
select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
|
||||
else:
|
||||
@@ -293,6 +276,7 @@ def get_balance_on(
|
||||
select_field, " and ".join(cond)
|
||||
)
|
||||
)[0][0]
|
||||
|
||||
# if bal is None, return 0
|
||||
return flt(bal)
|
||||
|
||||
@@ -475,9 +459,6 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
|
||||
# update ref in advance entry
|
||||
if voucher_type == "Journal Entry":
|
||||
update_reference_in_journal_entry(entry, doc, do_not_save=True)
|
||||
# advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss
|
||||
# amount and account in args
|
||||
doc.make_exchange_gain_loss_journal(args)
|
||||
else:
|
||||
update_reference_in_payment_entry(
|
||||
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
|
||||
@@ -637,7 +618,9 @@ def update_reference_in_payment_entry(
|
||||
"total_amount": d.grand_total,
|
||||
"outstanding_amount": d.outstanding_amount,
|
||||
"allocated_amount": d.allocated_amount,
|
||||
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
||||
"exchange_rate": d.exchange_rate
|
||||
if not d.exchange_gain_loss
|
||||
else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation
|
||||
"account": d.account,
|
||||
}
|
||||
@@ -659,48 +642,28 @@ def update_reference_in_payment_entry(
|
||||
new_row.docstatus = 1
|
||||
new_row.update(reference_details)
|
||||
|
||||
if d.difference_amount and d.difference_account:
|
||||
account_details = {
|
||||
"account": d.difference_account,
|
||||
"cost_center": payment_entry.cost_center
|
||||
or frappe.get_cached_value("Company", payment_entry.company, "cost_center"),
|
||||
}
|
||||
if d.difference_amount:
|
||||
account_details["amount"] = d.difference_amount
|
||||
|
||||
payment_entry.set_gain_or_loss(account_details=account_details)
|
||||
|
||||
payment_entry.flags.ignore_validate_update_after_submit = True
|
||||
payment_entry.setup_party_account_field()
|
||||
payment_entry.set_missing_values()
|
||||
if not skip_ref_details_update_for_pe:
|
||||
payment_entry.set_missing_ref_details()
|
||||
payment_entry.set_amounts()
|
||||
payment_entry.make_exchange_gain_loss_journal()
|
||||
|
||||
if not do_not_save:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
|
||||
"""
|
||||
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
|
||||
"""
|
||||
if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"reference_type": parent_doc.doctype,
|
||||
"reference_name": parent_doc.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
if journals:
|
||||
gain_loss_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"name": ["in", [x[0] for x in journals]],
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"docstatus": 1,
|
||||
},
|
||||
as_list=1,
|
||||
)
|
||||
for doc in gain_loss_journals:
|
||||
frappe.get_doc("Journal Entry", doc[0]).cancel()
|
||||
|
||||
|
||||
def unlink_ref_doc_from_payment_entries(ref_doc):
|
||||
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
|
||||
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
|
||||
@@ -908,7 +871,6 @@ def get_outstanding_invoices(
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
accounting_dimensions=None,
|
||||
vouchers=None,
|
||||
):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@@ -934,7 +896,6 @@ def get_outstanding_invoices(
|
||||
|
||||
ple_query = QueryPaymentLedger()
|
||||
invoice_list = ple_query.get_voucher_outstandings(
|
||||
vouchers=vouchers,
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_date,
|
||||
min_outstanding=min_outstanding,
|
||||
@@ -1859,74 +1820,3 @@ class QueryPaymentLedger(object):
|
||||
self.query_for_outstanding()
|
||||
|
||||
return self.voucher_outstandings
|
||||
|
||||
|
||||
def create_gain_loss_journal(
|
||||
company,
|
||||
party_type,
|
||||
party,
|
||||
party_account,
|
||||
gain_loss_account,
|
||||
exc_gain_loss,
|
||||
dr_or_cr,
|
||||
reverse_dr_or_cr,
|
||||
ref1_dt,
|
||||
ref1_dn,
|
||||
ref1_detail_no,
|
||||
ref2_dt,
|
||||
ref2_dn,
|
||||
ref2_detail_no,
|
||||
) -> str:
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
journal_entry.company = company
|
||||
journal_entry.posting_date = nowdate()
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency")
|
||||
|
||||
if not gain_loss_account:
|
||||
frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}").format(company))
|
||||
gain_loss_account_currency = get_account_currency(gain_loss_account)
|
||||
company_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
|
||||
if gain_loss_account_currency != company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency))
|
||||
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": party_account,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"account_currency": party_account_currency,
|
||||
"exchange_rate": 0,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
"reference_type": ref1_dt,
|
||||
"reference_name": ref1_dn,
|
||||
"reference_detail_no": ref1_detail_no,
|
||||
dr_or_cr: abs(exc_gain_loss),
|
||||
dr_or_cr + "_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": gain_loss_account,
|
||||
"account_currency": gain_loss_account_currency,
|
||||
"exchange_rate": 1,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
"reference_type": ref2_dt,
|
||||
"reference_name": ref2_dn,
|
||||
"reference_detail_no": ref2_detail_no,
|
||||
reverse_dr_or_cr + "_in_account_currency": 0,
|
||||
reverse_dr_or_cr: abs(exc_gain_loss),
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_entry.save()
|
||||
journal_entry.submit()
|
||||
return journal_entry.name
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"label": "Profit and Loss"
|
||||
}
|
||||
],
|
||||
"content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Payment\",\"col\":3}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"iAwpe-Chra\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Accounting\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
|
||||
"content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"iAwpe-Chra\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Accounting\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:41:59.515192",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -1061,28 +1061,11 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2023-08-10 17:41:14.059005",
|
||||
"modified": "2023-07-04 14:32:15.842044",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
"number_cards": [
|
||||
{
|
||||
"label": "Total Outgoing Bills",
|
||||
"number_card_name": "Total Outgoing Bills"
|
||||
},
|
||||
{
|
||||
"label": "Total Incoming Bills",
|
||||
"number_card_name": "Total Incoming Bills"
|
||||
},
|
||||
{
|
||||
"label": "Total Incoming Payment",
|
||||
"number_card_name": "Total Incoming Payment"
|
||||
},
|
||||
{
|
||||
"label": "Total Outgoing Payment",
|
||||
"number_card_name": "Total Outgoing Payment"
|
||||
}
|
||||
],
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
|
||||
@@ -207,39 +207,34 @@ frappe.ui.form.on('Asset', {
|
||||
},
|
||||
|
||||
render_depreciation_schedule_view: function(frm, depr_schedule) {
|
||||
let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
|
||||
var wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
|
||||
|
||||
let data = [];
|
||||
let table = $(`<table class="table table-bordered" style="margin-top:0px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<td align="center">${__("No.")}</td>
|
||||
<td>${__("Schedule Date")}</td>
|
||||
<td align="right">${__("Depreciation Amount")}</td>
|
||||
<td align="right">${__("Accumulated Depreciation Amount")}</td>
|
||||
<td>${__("Journal Entry")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>`);
|
||||
|
||||
depr_schedule.forEach((sch) => {
|
||||
const row = [
|
||||
sch['idx'],
|
||||
frappe.format(sch['schedule_date'], { fieldtype: 'Date' }),
|
||||
frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' }),
|
||||
frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' }),
|
||||
sch['journal_entry'] || ''
|
||||
];
|
||||
data.push(row);
|
||||
const row = $(`<tr>
|
||||
<td align="center">${sch['idx']}</td>
|
||||
<td><b>${frappe.format(sch['schedule_date'], { fieldtype: 'Date' })}</b></td>
|
||||
<td><b>${frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' })}</b></td>
|
||||
<td>${frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' })}</td>
|
||||
<td><a href="/app/journal-entry/${sch['journal_entry'] || ''}">${sch['journal_entry'] || ''}</a></td>
|
||||
</tr>`);
|
||||
table.find("tbody").append(row);
|
||||
});
|
||||
|
||||
let datatable = new frappe.DataTable(wrapper.get(0), {
|
||||
columns: [
|
||||
{name: __("No."), editable: false, resizable: false, format: value => value, width: 60},
|
||||
{name: __("Schedule Date"), editable: false, resizable: false, width: 270},
|
||||
{name: __("Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||
{name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||
{name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 312}
|
||||
],
|
||||
data: data,
|
||||
serialNoColumn: false,
|
||||
checkboxColumn: true,
|
||||
cellHeight: 35
|
||||
});
|
||||
wrapper.append(table);
|
||||
|
||||
datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem'});
|
||||
datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'});
|
||||
datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600});
|
||||
datatable.style.setStyle(`.dt-cell--col-3`, {'font-weight': 600});
|
||||
},
|
||||
|
||||
setup_chart_and_depr_schedule_view: async function(frm) {
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"column_break_33",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
"is_fully_depreciated",
|
||||
"section_break_36",
|
||||
"finance_books",
|
||||
"section_break_33",
|
||||
@@ -206,7 +205,6 @@
|
||||
"fieldname": "disposal_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Disposal Date",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -246,17 +244,19 @@
|
||||
"label": "Is Existing Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"depends_on": "is_existing_asset",
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Opening Accumulated Depreciation",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"depends_on": "eval:(doc.is_existing_asset && doc.opening_accumulated_depreciation)",
|
||||
"fieldname": "number_of_depreciations_booked",
|
||||
"fieldtype": "Int",
|
||||
"label": "Number of Depreciations Booked"
|
||||
"label": "Number of Depreciations Booked",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -500,13 +500,6 @@
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 1,
|
||||
"label": "Depreciation Schedule View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "is_fully_depreciated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Fully Depreciated"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -533,11 +526,6 @@
|
||||
"link_doctype": "Asset Depreciation Schedule",
|
||||
"link_fieldname": "asset"
|
||||
},
|
||||
{
|
||||
"group": "Activity",
|
||||
"link_doctype": "Asset Activity",
|
||||
"link_fieldname": "asset"
|
||||
},
|
||||
{
|
||||
"group": "Journal Entry",
|
||||
"link_doctype": "Journal Entry",
|
||||
@@ -545,7 +533,7 @@
|
||||
"table_fieldname": "accounts"
|
||||
}
|
||||
],
|
||||
"modified": "2023-07-28 20:12:44.819616",
|
||||
"modified": "2023-07-26 13:33:36.821534",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -589,4 +577,4 @@
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_depreciation_accounts,
|
||||
get_disposal_account_and_cost_center,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
cancel_asset_depr_schedules,
|
||||
@@ -60,7 +59,7 @@ class Asset(AccountsController):
|
||||
self.make_asset_movement()
|
||||
if not self.booked_fixed_asset and self.validate_make_gl_entry():
|
||||
self.make_gl_entries()
|
||||
if self.calculate_depreciation and not self.split_from:
|
||||
if not self.split_from:
|
||||
asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self)
|
||||
convert_draft_asset_depr_schedules_into_active(self)
|
||||
if asset_depr_schedules_names:
|
||||
@@ -72,7 +71,6 @@ class Asset(AccountsController):
|
||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||
).format(asset_depr_schedules_links)
|
||||
)
|
||||
add_asset_activity(self.name, _("Asset submitted"))
|
||||
|
||||
def on_cancel(self):
|
||||
self.validate_cancellation()
|
||||
@@ -83,10 +81,9 @@ class Asset(AccountsController):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
|
||||
self.db_set("booked_fixed_asset", 0)
|
||||
add_asset_activity(self.name, _("Asset cancelled"))
|
||||
|
||||
def after_insert(self):
|
||||
if self.calculate_depreciation and not self.split_from:
|
||||
if not self.split_from:
|
||||
asset_depr_schedules_names = make_draft_asset_depr_schedules(self)
|
||||
asset_depr_schedules_links = get_comma_separated_links(
|
||||
asset_depr_schedules_names, "Asset Depreciation Schedule"
|
||||
@@ -96,16 +93,6 @@ class Asset(AccountsController):
|
||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||
).format(asset_depr_schedules_links)
|
||||
)
|
||||
if not frappe.db.exists(
|
||||
{
|
||||
"doctype": "Asset Activity",
|
||||
"asset": self.name,
|
||||
}
|
||||
):
|
||||
add_asset_activity(self.name, _("Asset created"))
|
||||
|
||||
def after_delete(self):
|
||||
add_asset_activity(self.name, _("Asset deleted"))
|
||||
|
||||
def validate_asset_and_reference(self):
|
||||
if self.purchase_invoice or self.purchase_receipt:
|
||||
@@ -148,33 +135,17 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
|
||||
|
||||
def validate_cost_center(self):
|
||||
if self.cost_center:
|
||||
cost_center_company, cost_center_is_group = frappe.db.get_value(
|
||||
"Cost Center", self.cost_center, ["company", "is_group"]
|
||||
)
|
||||
if cost_center_company != self.company:
|
||||
frappe.throw(
|
||||
_("Cost Center {} doesn't belong to Company {}").format(
|
||||
frappe.bold(self.cost_center), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
if cost_center_is_group:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cost Center {} is a group cost center and group cost centers cannot be used in transactions"
|
||||
).format(frappe.bold(self.cost_center)),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
else:
|
||||
if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
|
||||
).format(frappe.bold(self.company)),
|
||||
title=_("Missing Cost Center"),
|
||||
)
|
||||
cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company")
|
||||
if cost_center_company != self.company:
|
||||
frappe.throw(
|
||||
_("Selected Cost Center {} doesn't belongs to {}").format(
|
||||
frappe.bold(self.cost_center), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
|
||||
def validate_in_use_date(self):
|
||||
if not self.available_for_use_date:
|
||||
@@ -223,11 +194,8 @@ class Asset(AccountsController):
|
||||
|
||||
if not self.calculate_depreciation:
|
||||
return
|
||||
else:
|
||||
if not self.finance_books:
|
||||
frappe.throw(_("Enter depreciation details"))
|
||||
if self.is_fully_depreciated:
|
||||
frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
|
||||
elif not self.finance_books:
|
||||
frappe.throw(_("Enter depreciation details"))
|
||||
|
||||
if self.is_existing_asset:
|
||||
return
|
||||
@@ -308,7 +276,7 @@ class Asset(AccountsController):
|
||||
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
frappe.throw(
|
||||
_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
|
||||
_("Opening Accumulated Depreciation must be less than equal to {0}").format(
|
||||
depreciable_amount
|
||||
)
|
||||
)
|
||||
@@ -444,9 +412,7 @@ class Asset(AccountsController):
|
||||
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
|
||||
value_after_depreciation = self.finance_books[idx].value_after_depreciation
|
||||
|
||||
if (
|
||||
flt(value_after_depreciation) <= expected_value_after_useful_life or self.is_fully_depreciated
|
||||
):
|
||||
if flt(value_after_depreciation) <= expected_value_after_useful_life:
|
||||
status = "Fully Depreciated"
|
||||
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
|
||||
status = "Partially Depreciated"
|
||||
@@ -478,9 +444,7 @@ class Asset(AccountsController):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_manual_depreciation_entries(self):
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(
|
||||
self.asset_category, self.company
|
||||
)
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(self)
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
|
||||
@@ -823,10 +787,10 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
|
||||
def make_journal_entry(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
_,
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
) = get_depreciation_accounts(asset)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
@@ -934,13 +898,6 @@ def update_existing_asset(asset, remaining_qty, new_asset_name):
|
||||
},
|
||||
)
|
||||
|
||||
add_asset_activity(
|
||||
asset.name,
|
||||
_("Asset updated after being split into Asset {0}").format(
|
||||
get_link_to_form("Asset", new_asset_name)
|
||||
),
|
||||
)
|
||||
|
||||
for row in asset.get("finance_books"):
|
||||
value_after_depreciation = flt(
|
||||
(row.value_after_depreciation * remaining_qty) / asset.asset_quantity
|
||||
@@ -1008,15 +965,6 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
(row.expected_value_after_useful_life * split_qty) / asset.asset_quantity
|
||||
)
|
||||
|
||||
new_asset.insert()
|
||||
|
||||
add_asset_activity(
|
||||
new_asset.name,
|
||||
_("Asset created after being split from Asset {0}").format(
|
||||
get_link_to_form("Asset", asset.name)
|
||||
),
|
||||
)
|
||||
|
||||
new_asset.submit()
|
||||
new_asset.set_status()
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Max, Min
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
@@ -23,7 +21,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
get_asset_depr_schedule_name,
|
||||
@@ -45,48 +42,11 @@ def post_depreciation_entries(date=None):
|
||||
failed_asset_names = []
|
||||
error_log_names = []
|
||||
|
||||
depreciable_asset_depr_schedules_data = get_depreciable_asset_depr_schedules_data(date)
|
||||
|
||||
credit_and_debit_accounts_for_asset_category_and_company = {}
|
||||
depreciation_cost_center_and_depreciation_series_for_company = (
|
||||
get_depreciation_cost_center_and_depreciation_series_for_company()
|
||||
)
|
||||
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
|
||||
for asset_depr_schedule_data in depreciable_asset_depr_schedules_data:
|
||||
(
|
||||
asset_depr_schedule_name,
|
||||
asset_name,
|
||||
asset_category,
|
||||
asset_company,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
) = asset_depr_schedule_data
|
||||
|
||||
if (
|
||||
asset_category,
|
||||
asset_company,
|
||||
) not in credit_and_debit_accounts_for_asset_category_and_company:
|
||||
credit_and_debit_accounts_for_asset_category_and_company.update(
|
||||
{
|
||||
(asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company(
|
||||
asset_category, asset_company
|
||||
),
|
||||
}
|
||||
)
|
||||
for asset_name in get_depreciable_assets(date):
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
try:
|
||||
make_depreciation_entry(
|
||||
asset_depr_schedule_name,
|
||||
date,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)],
|
||||
depreciation_cost_center_and_depreciation_series_for_company[asset_company],
|
||||
accounting_dimensions,
|
||||
)
|
||||
|
||||
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
@@ -101,36 +61,18 @@ def post_depreciation_entries(date=None):
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_depreciable_asset_depr_schedules_data(date):
|
||||
a = frappe.qb.DocType("Asset")
|
||||
ads = frappe.qb.DocType("Asset Depreciation Schedule")
|
||||
ds = frappe.qb.DocType("Depreciation Schedule")
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(ads)
|
||||
.join(a)
|
||||
.on(ads.asset == a.name)
|
||||
.join(ds)
|
||||
.on(ads.name == ds.parent)
|
||||
.select(ads.name, a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx))
|
||||
.where(a.calculate_depreciation == 1)
|
||||
.where(a.docstatus == 1)
|
||||
.where(ads.docstatus == 1)
|
||||
.where(a.status.isin(["Submitted", "Partially Depreciated"]))
|
||||
.where(ds.journal_entry.isnull())
|
||||
.where(ds.schedule_date <= date)
|
||||
.groupby(ads.name)
|
||||
.orderby(a.creation, order=Order.desc)
|
||||
def get_depreciable_assets(date):
|
||||
return frappe.db.sql_list(
|
||||
"""select distinct a.name
|
||||
from tabAsset a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
|
||||
where a.name = ads.asset and ads.name = ds.parent and a.docstatus=1 and ads.docstatus=1
|
||||
and a.status in ('Submitted', 'Partially Depreciated')
|
||||
and a.calculate_depreciation = 1
|
||||
and ds.schedule_date<=%s
|
||||
and ifnull(ds.journal_entry, '')=''""",
|
||||
date,
|
||||
)
|
||||
|
||||
acc_frozen_upto = get_acc_frozen_upto()
|
||||
if acc_frozen_upto:
|
||||
res = res.where(ds.schedule_date > acc_frozen_upto)
|
||||
|
||||
res = res.run()
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None):
|
||||
for row in asset_doc.get("finance_books"):
|
||||
@@ -140,60 +82,8 @@ def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None):
|
||||
make_depreciation_entry(asset_depr_schedule_name, date)
|
||||
|
||||
|
||||
def get_acc_frozen_upto():
|
||||
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
if not acc_frozen_upto:
|
||||
return
|
||||
|
||||
frozen_accounts_modifier = frappe.db.get_single_value(
|
||||
"Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
|
||||
return getdate(acc_frozen_upto)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company):
|
||||
(
|
||||
_,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset_category, company)
|
||||
|
||||
credit_account, debit_account = get_credit_and_debit_accounts(
|
||||
accumulated_depreciation_account, depreciation_expense_account
|
||||
)
|
||||
|
||||
return (credit_account, debit_account)
|
||||
|
||||
|
||||
def get_depreciation_cost_center_and_depreciation_series_for_company():
|
||||
company_names = frappe.db.get_all("Company", pluck="name")
|
||||
|
||||
res = {}
|
||||
|
||||
for company_name in company_names:
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
res.update({company_name: (depreciation_cost_center, depreciation_series)})
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_depreciation_entry(
|
||||
asset_depr_schedule_name,
|
||||
date=None,
|
||||
sch_start_idx=None,
|
||||
sch_end_idx=None,
|
||||
credit_and_debit_accounts=None,
|
||||
depreciation_cost_center_and_depreciation_series=None,
|
||||
accounting_dimensions=None,
|
||||
):
|
||||
def make_depreciation_entry(asset_depr_schedule_name, date=None):
|
||||
frappe.has_permission("Journal Entry", throw=True)
|
||||
|
||||
if not date:
|
||||
@@ -201,144 +91,100 @@ def make_depreciation_entry(
|
||||
|
||||
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", asset_depr_schedule_doc.asset)
|
||||
asset_name = asset_depr_schedule_doc.asset
|
||||
|
||||
if credit_and_debit_accounts:
|
||||
credit_account, debit_account = credit_and_debit_accounts
|
||||
else:
|
||||
credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company(
|
||||
asset.asset_category, asset.company
|
||||
)
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset)
|
||||
|
||||
if depreciation_cost_center_and_depreciation_series:
|
||||
depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series
|
||||
else:
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
if not accounting_dimensions:
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
|
||||
depreciation_posting_error = None
|
||||
for d in asset_depr_schedule_doc.get("depreciation_schedule"):
|
||||
if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = d.schedule_date
|
||||
je.company = asset.company
|
||||
je.finance_book = asset_depr_schedule_doc.finance_book
|
||||
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
|
||||
|
||||
for d in asset_depr_schedule_doc.get("depreciation_schedule")[
|
||||
sch_start_idx or 0 : sch_end_idx or len(asset_depr_schedule_doc.get("depreciation_schedule"))
|
||||
]:
|
||||
try:
|
||||
_make_journal_entry_for_depreciation(
|
||||
asset_depr_schedule_doc,
|
||||
asset,
|
||||
date,
|
||||
d,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
depreciation_cost_center,
|
||||
depreciation_series,
|
||||
credit_account,
|
||||
debit_account,
|
||||
accounting_dimensions,
|
||||
credit_account, debit_account = get_credit_and_debit_accounts(
|
||||
accumulated_depreciation_account, depreciation_expense_account
|
||||
)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
depreciation_posting_error = e
|
||||
|
||||
credit_entry = {
|
||||
"account": credit_account,
|
||||
"credit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"debit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
|
||||
credit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
|
||||
debit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
je.append("accounts", credit_entry)
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
|
||||
d.db_set("journal_entry", je.name)
|
||||
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
idx = cint(asset_depr_schedule_doc.finance_book_id)
|
||||
row = asset.get("finance_books")[idx - 1]
|
||||
row.value_after_depreciation -= d.depreciation_amount
|
||||
row.db_update()
|
||||
|
||||
asset.db_set("depr_entry_posting_status", "Successful")
|
||||
|
||||
asset.set_status()
|
||||
|
||||
if not depreciation_posting_error:
|
||||
asset.db_set("depr_entry_posting_status", "Successful")
|
||||
return asset_depr_schedule_doc
|
||||
|
||||
raise depreciation_posting_error
|
||||
return asset_depr_schedule_doc
|
||||
|
||||
|
||||
def _make_journal_entry_for_depreciation(
|
||||
asset_depr_schedule_doc,
|
||||
asset,
|
||||
date,
|
||||
depr_schedule,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
depreciation_cost_center,
|
||||
depreciation_series,
|
||||
credit_account,
|
||||
debit_account,
|
||||
accounting_dimensions,
|
||||
):
|
||||
if not (sch_start_idx and sch_end_idx) and not (
|
||||
not depr_schedule.journal_entry and getdate(depr_schedule.schedule_date) <= getdate(date)
|
||||
):
|
||||
return
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = depr_schedule.schedule_date
|
||||
je.company = asset.company
|
||||
je.finance_book = asset_depr_schedule_doc.finance_book
|
||||
je.remark = "Depreciation Entry against {0} worth {1}".format(
|
||||
asset.name, depr_schedule.depreciation_amount
|
||||
)
|
||||
|
||||
credit_entry = {
|
||||
"account": credit_account,
|
||||
"credit_in_account_currency": depr_schedule.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"debit_in_account_currency": depr_schedule.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
|
||||
credit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
|
||||
debit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
je.append("accounts", credit_entry)
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
|
||||
depr_schedule.db_set("journal_entry", je.name)
|
||||
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
idx = cint(asset_depr_schedule_doc.finance_book_id)
|
||||
row = asset.get("finance_books")[idx - 1]
|
||||
row.value_after_depreciation -= depr_schedule.depreciation_amount
|
||||
row.db_update()
|
||||
|
||||
|
||||
def get_depreciation_accounts(asset_category, company):
|
||||
def get_depreciation_accounts(asset):
|
||||
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
|
||||
|
||||
accounts = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": asset_category, "company_name": company},
|
||||
filters={"parent": asset.asset_category, "company_name": asset.company},
|
||||
fieldname=[
|
||||
"fixed_asset_account",
|
||||
"accumulated_depreciation_account",
|
||||
@@ -354,7 +200,7 @@ def get_depreciation_accounts(asset_category, company):
|
||||
|
||||
if not accumulated_depreciation_account or not depreciation_expense_account:
|
||||
accounts = frappe.get_cached_value(
|
||||
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
"Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
)
|
||||
|
||||
if not accumulated_depreciation_account:
|
||||
@@ -369,7 +215,7 @@ def get_depreciation_accounts(asset_category, company):
|
||||
):
|
||||
frappe.throw(
|
||||
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
|
||||
asset_category, company
|
||||
asset.asset_category, asset.company
|
||||
)
|
||||
)
|
||||
|
||||
@@ -479,8 +325,6 @@ def scrap_asset(asset_name):
|
||||
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
|
||||
asset.set_status("Scrapped")
|
||||
|
||||
add_asset_activity(asset_name, _("Asset scrapped"))
|
||||
|
||||
frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
|
||||
|
||||
|
||||
@@ -505,8 +349,6 @@ def restore_asset(asset_name):
|
||||
|
||||
asset.set_status()
|
||||
|
||||
add_asset_activity(asset_name, _("Asset restored"))
|
||||
|
||||
|
||||
def depreciate_asset(asset_doc, date, notes):
|
||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
@@ -556,15 +398,6 @@ def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
|
||||
for account in reverse_journal_entry.accounts:
|
||||
account.update(
|
||||
{
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
@@ -718,8 +551,8 @@ def get_gl_entries_on_asset_disposal(
|
||||
|
||||
|
||||
def get_asset_details(asset, finance_book=None):
|
||||
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
||||
asset.asset_category, asset.company
|
||||
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(
|
||||
asset
|
||||
)
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
@@ -754,40 +754,6 @@ class TestDepreciationMethods(AssetSetup):
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_for_straight_line_method_with_daily_depreciation(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2023-01-01",
|
||||
purchase_date="2023-01-01",
|
||||
gross_purchase_amount=12000,
|
||||
depreciation_start_date="2023-01-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
daily_depreciation=1,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2023-01-31", 1019.18, 1019.18],
|
||||
["2023-02-28", 920.55, 1939.73],
|
||||
["2023-03-31", 1019.18, 2958.91],
|
||||
["2023-04-30", 986.3, 3945.21],
|
||||
["2023-05-31", 1019.18, 4964.39],
|
||||
["2023-06-30", 986.3, 5950.69],
|
||||
["2023-07-31", 1019.18, 6969.87],
|
||||
["2023-08-31", 1019.18, 7989.05],
|
||||
["2023-09-30", 986.3, 8975.35],
|
||||
["2023-10-31", 1019.18, 9994.53],
|
||||
["2023-11-30", 986.3, 10980.83],
|
||||
["2023-12-31", 1019.17, 12000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_for_straight_line_method_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
@@ -1758,7 +1724,6 @@ def create_asset(**args):
|
||||
"total_number_of_depreciations": args.total_number_of_depreciations or 5,
|
||||
"expected_value_after_useful_life": args.expected_value_after_useful_life or 0,
|
||||
"depreciation_start_date": args.depreciation_start_date,
|
||||
"daily_depreciation": args.daily_depreciation or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Asset Activity", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2023-07-28 12:41:13.232505",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"asset",
|
||||
"column_break_vkdy",
|
||||
"date",
|
||||
"column_break_kkxv",
|
||||
"user",
|
||||
"section_break_romx",
|
||||
"subject"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Asset",
|
||||
"options": "Asset",
|
||||
"print_width": "165",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "165"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vkdy",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_romx",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"print_width": "518",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "518"
|
||||
},
|
||||
{
|
||||
"default": "now",
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"print_width": "158",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "158"
|
||||
},
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"print_width": "150",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "150"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_kkxv",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-01 11:09:52.584482",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Activity",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Quality Manager",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssetActivity(Document):
|
||||
pass
|
||||
|
||||
|
||||
def add_asset_activity(asset, subject):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Asset Activity",
|
||||
"asset": asset,
|
||||
"subject": subject,
|
||||
"user": frappe.session.user,
|
||||
}
|
||||
).insert(ignore_permissions=True, ignore_links=True)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestAssetActivity(FrappeTestCase):
|
||||
pass
|
||||
@@ -18,7 +18,6 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
reset_depreciation_schedule,
|
||||
reverse_depreciation_entry_made_after_disposal,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.setup.doctype.brand.brand import get_brand_defaults
|
||||
@@ -330,7 +329,7 @@ class AssetCapitalization(StockController):
|
||||
gl_entries = self.get_gl_entries()
|
||||
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, merge_entries=False, from_repost=from_repost)
|
||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||
elif self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
@@ -360,6 +359,9 @@ class AssetCapitalization(StockController):
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
|
||||
if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable:
|
||||
return []
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
|
||||
|
||||
return gl_entries
|
||||
@@ -517,13 +519,6 @@ class AssetCapitalization(StockController):
|
||||
"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
|
||||
)
|
||||
|
||||
add_asset_activity(
|
||||
asset_doc.name,
|
||||
_("Asset created after Asset Capitalization {0} was submitted").format(
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Asset {0} has been created. Please set the depreciation details if any and submit it."
|
||||
@@ -547,30 +542,9 @@ class AssetCapitalization(StockController):
|
||||
|
||||
def set_consumed_asset_status(self, asset):
|
||||
if self.docstatus == 1:
|
||||
if self.target_is_fixed_asset:
|
||||
asset.set_status("Capitalized")
|
||||
add_asset_activity(
|
||||
asset.name,
|
||||
_("Asset capitalized after Asset Capitalization {0} was submitted").format(
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
else:
|
||||
asset.set_status("Decapitalized")
|
||||
add_asset_activity(
|
||||
asset.name,
|
||||
_("Asset decapitalized after Asset Capitalization {0} was submitted").format(
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized")
|
||||
else:
|
||||
asset.set_status()
|
||||
add_asset_activity(
|
||||
asset.name,
|
||||
_("Asset restored after Asset Capitalization {0} was cancelled").format(
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -33,7 +33,6 @@ frappe.ui.form.on('Asset Category', {
|
||||
var d = locals[cdt][cdn];
|
||||
return {
|
||||
"filters": {
|
||||
"account_type": "Depreciation",
|
||||
"root_type": ["in", ["Expense", "Income"]],
|
||||
"is_group": 0,
|
||||
"company": d.company_name
|
||||
|
||||
@@ -53,7 +53,7 @@ class AssetCategory(Document):
|
||||
account_type_map = {
|
||||
"fixed_asset_account": {"account_type": ["Fixed Asset"]},
|
||||
"accumulated_depreciation_account": {"account_type": ["Accumulated Depreciation"]},
|
||||
"depreciation_expense_account": {"account_type": ["Depreciation"]},
|
||||
"depreciation_expense_account": {"root_type": ["Expense", "Income"]},
|
||||
"capital_work_in_progress_account": {"account_type": ["Capital Work in Progress"]},
|
||||
}
|
||||
for d in self.accounts:
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"rate_of_depreciation",
|
||||
"daily_depreciation",
|
||||
"column_break_8",
|
||||
"frequency_of_depreciation",
|
||||
"expected_value_after_useful_life",
|
||||
@@ -175,21 +174,12 @@
|
||||
"label": "Number of Depreciations Booked",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
|
||||
"fieldname": "daily_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Daily Depreciation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 22:22:09.722968",
|
||||
"modified": "2023-02-26 16:37:23.734806",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Depreciation Schedule",
|
||||
|
||||
@@ -153,7 +153,6 @@ class AssetDepreciationSchedule(Document):
|
||||
self.frequency_of_depreciation = row.frequency_of_depreciation
|
||||
self.rate_of_depreciation = row.rate_of_depreciation
|
||||
self.expected_value_after_useful_life = row.expected_value_after_useful_life
|
||||
self.daily_depreciation = row.daily_depreciation
|
||||
self.status = "Draft"
|
||||
|
||||
def make_depr_schedule(
|
||||
@@ -500,36 +499,29 @@ def get_total_days(date, frequency):
|
||||
return date_diff(date, period_start_date)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_depreciation_amount(
|
||||
asset,
|
||||
depreciable_value,
|
||||
fb_row,
|
||||
row,
|
||||
schedule_idx=0,
|
||||
prev_depreciation_amount=0,
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
):
|
||||
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
||||
return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx)
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
return get_straight_line_or_manual_depr_amount(asset, row)
|
||||
else:
|
||||
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset, depreciable_value, fb_row
|
||||
)
|
||||
return get_wdv_or_dd_depr_amount(
|
||||
depreciable_value,
|
||||
rate_of_depreciation,
|
||||
fb_row.frequency_of_depreciation,
|
||||
row.rate_of_depreciation,
|
||||
row.frequency_of_depreciation,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row):
|
||||
return fb_row.rate_of_depreciation
|
||||
|
||||
|
||||
def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
|
||||
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)) / (
|
||||
@@ -542,30 +534,11 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
|
||||
)
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
if row.daily_depreciation:
|
||||
daily_depr_amount = (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / date_diff(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
* row.frequency_of_depreciation,
|
||||
),
|
||||
row.depreciation_start_date,
|
||||
)
|
||||
to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
from_date = add_months(
|
||||
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
|
||||
)
|
||||
return daily_depr_amount * date_diff(to_date, from_date)
|
||||
else:
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
|
||||
def get_wdv_or_dd_depr_amount(
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"finance_book",
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"daily_depreciation",
|
||||
"column_break_5",
|
||||
"frequency_of_depreciation",
|
||||
"depreciation_start_date",
|
||||
@@ -18,7 +17,6 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -34,7 +32,6 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "total_number_of_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
@@ -46,7 +43,6 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "frequency_of_depreciation",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
@@ -61,7 +57,6 @@
|
||||
"mandatory_depends_on": "eval:parent.doctype == 'Asset'"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.doctype == 'Asset'",
|
||||
"fieldname": "expected_value_after_useful_life",
|
||||
@@ -84,19 +79,12 @@
|
||||
"fieldname": "rate_of_depreciation",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Rate of Depreciation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
|
||||
"fieldname": "daily_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Daily Depreciation"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 22:10:36.576199",
|
||||
"modified": "2021-06-17 12:59:05.743683",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
@@ -105,6 +93,5 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -5,9 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
|
||||
|
||||
class AssetMovement(Document):
|
||||
@@ -131,24 +128,5 @@ class AssetMovement(Document):
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
|
||||
frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False)
|
||||
frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False)
|
||||
|
||||
if current_location and current_employee:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset received at Location {0} and issued to Employee {1}").format(
|
||||
get_link_to_form("Location", current_location),
|
||||
get_link_to_form("Employee", current_employee),
|
||||
),
|
||||
)
|
||||
elif current_location:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset transferred to Location {0}").format(get_link_to_form("Location", current_location)),
|
||||
)
|
||||
elif current_employee:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
|
||||
)
|
||||
frappe.db.set_value("Asset", d.asset, "location", current_location)
|
||||
frappe.db.set_value("Asset", d.asset, "custodian", current_employee)
|
||||
|
||||
@@ -8,7 +8,6 @@ from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_depr_schedule,
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones,
|
||||
@@ -26,14 +25,8 @@ class AssetRepair(AccountsController):
|
||||
self.calculate_total_repair_cost()
|
||||
|
||||
def update_status(self):
|
||||
if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
|
||||
if self.repair_status == "Pending":
|
||||
frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset out of order due to Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.asset_doc.set_status()
|
||||
|
||||
@@ -75,13 +68,6 @@ class AssetRepair(AccountsController):
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||
self.asset_doc.save()
|
||||
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset updated after completion of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
def before_cancel(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
@@ -109,13 +95,6 @@ class AssetRepair(AccountsController):
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||
self.asset_doc.save()
|
||||
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset updated after cancellation of Asset Repair {0}").format(
|
||||
get_link_to_form("Asset Repair", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
def after_delete(self):
|
||||
frappe.get_doc("Asset", self.asset).set_status()
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
get_depreciation_amount,
|
||||
@@ -28,21 +27,9 @@ class AssetValueAdjustment(Document):
|
||||
def on_submit(self):
|
||||
self.make_depreciation_entry()
|
||||
self.reschedule_depreciations(self.new_asset_value)
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
|
||||
get_link_to_form("Asset Value Adjustment", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.reschedule_depreciations(self.current_asset_value)
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
|
||||
get_link_to_form("Asset Value Adjustment", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
def validate_date(self):
|
||||
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
|
||||
@@ -64,10 +51,10 @@ class AssetValueAdjustment(Document):
|
||||
def make_depreciation_entry(self):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
(
|
||||
_,
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
) = get_depreciation_accounts(asset)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
@@ -78,23 +65,21 @@ class AssetValueAdjustment(Document):
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = self.date
|
||||
je.company = self.company
|
||||
je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount)
|
||||
je.remark = _("Depreciation Entry against {0} worth {1}").format(
|
||||
self.asset, self.difference_amount
|
||||
)
|
||||
je.finance_book = self.finance_book
|
||||
|
||||
credit_entry = {
|
||||
"account": accumulated_depreciation_account,
|
||||
"credit_in_account_currency": self.difference_amount,
|
||||
"cost_center": depreciation_cost_center or self.cost_center,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": self.asset,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": depreciation_expense_account,
|
||||
"debit_in_account_currency": self.difference_amount,
|
||||
"cost_center": depreciation_cost_center or self.cost_center,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": self.asset,
|
||||
}
|
||||
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-08-01 11:14:46.581234",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"json": "{}",
|
||||
"letterhead": null,
|
||||
"modified": "2023-08-01 11:14:46.581234",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Activity",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Asset Activity",
|
||||
"report_name": "Asset Activity",
|
||||
"report_type": "Report Builder",
|
||||
"roles": [
|
||||
{
|
||||
"role": "System Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,14 +7,13 @@ from itertools import chain
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import add_months, cstr, flt, formatdate, getdate, nowdate, today
|
||||
from frappe.utils import cstr, flt, formatdate, getdate
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
get_fiscal_year_data,
|
||||
get_period_list,
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
|
||||
|
||||
@@ -38,26 +37,15 @@ def get_conditions(filters):
|
||||
|
||||
if filters.get("company"):
|
||||
conditions["company"] = filters.company
|
||||
|
||||
if filters.filter_based_on == "Date Range":
|
||||
if not filters.from_date and not filters.to_date:
|
||||
filters.from_date = add_months(nowdate(), -12)
|
||||
filters.to_date = nowdate()
|
||||
|
||||
conditions[date_field] = ["between", [filters.from_date, filters.to_date]]
|
||||
elif filters.filter_based_on == "Fiscal Year":
|
||||
if not filters.from_fiscal_year and not filters.to_fiscal_year:
|
||||
default_fiscal_year = get_fiscal_year(today())[0]
|
||||
filters.from_fiscal_year = default_fiscal_year
|
||||
filters.to_fiscal_year = default_fiscal_year
|
||||
|
||||
if filters.filter_based_on == "Fiscal Year":
|
||||
fiscal_year = get_fiscal_year_data(filters.from_fiscal_year, filters.to_fiscal_year)
|
||||
validate_fiscal_year(fiscal_year, filters.from_fiscal_year, filters.to_fiscal_year)
|
||||
filters.year_start_date = getdate(fiscal_year.year_start_date)
|
||||
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("only_existing_assets"):
|
||||
conditions["is_existing_asset"] = filters.get("only_existing_assets")
|
||||
if filters.get("asset_category"):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user